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/NOTICE.txt b/NOTICE.txt index 4ede43610ca7b..1694193892e16 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -295,7 +295,7 @@ MIT License http://www.opensource.org/licenses/mit-license --- This product includes code that is adapted from mapbox-gl-js, which is available under a "BSD-3-Clause" license. -https://github.com/mapbox/mapbox-gl-js/blob/master/src/util/image.js +https://github.com/mapbox/mapbox-gl-js/blob/v1.13.2/src/util/image.js Copyright (c) 2016, Mapbox diff --git a/docs/developer/getting-started/debugging.asciidoc b/docs/developer/getting-started/debugging.asciidoc index f3308a1267386..1254462d2e4ea 100644 --- a/docs/developer/getting-started/debugging.asciidoc +++ b/docs/developer/getting-started/debugging.asciidoc @@ -130,71 +130,3 @@ Once you're finished, you can stop Kibana normally, then stop the {es} and APM s ---- ./scripts/compose.py stop ---- - -=== Using {kib} server logs -{kib} Logs is a great way to see what's going on in your application and to debug performance issues. Navigating through a large number of generated logs can be overwhelming, and following are some techniques that you can use to optimize the process. - -Start by defining a problem area that you are interested in. For example, you might be interested in seeing how a particular {kib} Plugin is performing, so no need to gather logs for all of {kib}. Or you might want to focus on a particular feature, such as requests from the {kib} server to the {es} server. -Depending on your needs, you can configure {kib} to generate logs for a specific feature. -[source,yml] ----- -logging: - appenders: - file: - type: file - fileName: ./kibana.log - layout: - type: json - -### gather all the Kibana logs into a file -logging.root: - appenders: [file] - level: all - -### or gather a subset of the logs -logging.loggers: - ### responses to an HTTP request - - name: http.server.response - level: debug - appenders: [file] - ### result of a query to the Elasticsearch server - - name: elasticsearch.query - level: debug - appenders: [file] - ### logs generated by my plugin - - name: plugins.myPlugin - level: debug - appenders: [file] ----- -WARNING: Kibana's `file` appender is configured to produce logs in https://www.elastic.co/guide/en/ecs/master/ecs-reference.html[ECS JSON] format. It's the only format that includes the meta information necessary for https://www.elastic.co/guide/en/apm/agent/nodejs/current/log-correlation.html[log correlation] out-of-the-box. - -The next step is to define what https://www.elastic.co/observability[observability tools] are available. -For a better experience, set up an https://www.elastic.co/guide/en/apm/get-started/current/observability-integrations.html[Observability integration] provided by Elastic to debug your application with the <> -To debug something quickly without setting up additional tooling, you can work with <> - -[[debugging-logs-apm-ui]] -==== APM UI -*Prerequisites* {kib} logs are configured to be in https://www.elastic.co/guide/en/ecs/master/ecs-reference.html[ECS JSON] format to include tracing identifiers. - -To debug {kib} with the APM UI, you must set up the APM infrastructure. You can find instructions for the setup process -https://www.elastic.co/guide/en/apm/get-started/current/observability-integrations.html[on the Observability integrations page]. - -Once you set up the APM infrastructure, you can enable the APM agent and put {kib} under load to collect APM events. To analyze the collected metrics and logs, use the APM UI as demonstrated https://www.elastic.co/guide/en/kibana/master/transactions.html#transaction-trace-sample[in the docs]. - -[[plain-kibana-logs]] -==== Plain {kib} logs -*Prerequisites* {kib} logs are configured to be in https://www.elastic.co/guide/en/ecs/master/ecs-reference.html[ECS JSON] format to include tracing identifiers. - -Open {kib} Logs and search for an operation you are interested in. -For example, suppose you want to investigate the response times for queries to the `/api/telemetry/v2/clusters/_stats` {kib} endpoint. -Open Kibana Logs and search for the HTTP server response for the endpoint. It looks similar to the following (some fields are omitted for brevity). -[source,json] ----- -{ - "message":"POST /api/telemetry/v2/clusters/_stats 200 1014ms - 43.2KB", - "log":{"level":"DEBUG","logger":"http.server.response"}, - "trace":{"id":"9b99131a6f66587971ef085ef97dfd07"}, - "transaction":{"id":"d0c5bbf14f5febca"} -} ----- -You are interested in the https://www.elastic.co/guide/en/ecs/current/ecs-tracing.html#field-trace-id[trace.id] field, which is a unique identifier of a trace. The `trace.id` provides a way to group multiple events, like transactions, which belong together. You can search for `"trace":{"id":"9b99131a6f66587971ef085ef97dfd07"}` to get all the logs that belong to the same trace. This enables you to see how many {es} requests were triggered during the `9b99131a6f66587971ef085ef97dfd07` trace, what they looked like, what {es} endpoints were hit, and so on. 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..63c29df44019d 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; @@ -289,7 +290,14 @@ readonly links: { }>; readonly watcher: Record; readonly ccs: Record; - readonly plugins: Record; + readonly plugins: { + azureRepo: string; + gcsRepo: string; + hdfsRepo: string; + s3Repo: string; + snapshotRestoreRepos: string; + mapperSize: string; + }; readonly snapshotRestore: Record; readonly ingest: Record; readonly fleet: Readonly<{ 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/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 77a250a14f929..27ea7f4dc7cd0 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -101,8 +101,8 @@ Changing these settings may disable features of the APM App. | `xpack.apm.indices.sourcemap` {ess-icon} | Matcher for all source map indices. Defaults to `apm-*`. -| `xpack.apm.autocreateApmIndexPattern` {ess-icon} - | Set to `false` to disable the automatic creation of the APM index pattern when the APM app is opened. Defaults to `true`. +| `xpack.apm.autoCreateApmDataView` {ess-icon} + | Set to `false` to disable the automatic creation of the APM data view when the APM app is opened. Defaults to `true`. |=== -// end::general-apm-settings[] \ No newline at end of file +// end::general-apm-settings[] diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc index f0dfeb619bb38..a088f31937cc8 100644 --- a/docs/settings/fleet-settings.asciidoc +++ b/docs/settings/fleet-settings.asciidoc @@ -87,6 +87,7 @@ Optional properties are: `data_output_id`:: ID of the output to send data (Need to be identical to `monitoring_output_id`) `monitoring_output_id`:: ID of the output to send monitoring data. (Need to be identical to `data_output_id`) `package_policies`:: List of integration policies to add to this policy. + `id`::: Unique ID of the integration policy. The ID may be a number or string. `name`::: (required) Name of the integration policy. `package`::: (required) Integration that this policy configures `name`:::: Name of the integration associated with this policy. @@ -128,6 +129,7 @@ xpack.fleet.agentPolicies: - package: name: system name: System Integration + id: preconfigured-system inputs: - type: system/metrics enabled: true 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/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc index a139b8a50ca4d..c828b837d8efd 100644 --- a/docs/setup/upgrade.asciidoc +++ b/docs/setup/upgrade.asciidoc @@ -44,13 +44,20 @@ a| [[upgrade-before-you-begin]] === Before you begin -WARNING: {kib} automatically runs upgrade migrations when required. To roll back to an earlier version in case of an upgrade failure, you **must** have a {ref}/snapshot-restore.html[backup snapshot] available. This snapshot must include the `kibana` feature state or all `kibana*` indices. For more information see <>. +[WARNING] +==== +{kib} automatically runs upgrade migrations when required. To roll back to an +earlier version in case of an upgrade failure, you **must** have a +{ref}/snapshot-restore.html[backup snapshot] that includes the `kibana` feature +state. Snapshots include this feature state by default. + +For more information, refer to <>. +==== Before you upgrade {kib}: * Consult the <>. -* {ref}/snapshots-take-snapshot.html[Take a snapshot] of your data. To roll back to an earlier version, the snapshot must include the `kibana` feature state or all `.kibana*` indices. -* Although not a requirement for rollbacks, we recommend taking a snapshot of all {kib} indices created by the plugins you use such as the `.reporting*` indices created by the reporting plugin. +* {ref}/snapshots-take-snapshot.html[Take a snapshot] of your data. To roll back to an earlier version, the snapshot must include the `kibana` feature state. * Before you upgrade production servers, test the upgrades in a dev environment. * See <> for common reasons upgrades fail and how to prevent these. * If you are using custom plugins, check that a compatible version is diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index c47c2c1745e94..e9e1b757fd71d 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -151,17 +151,18 @@ In order to rollback after a failed upgrade migration, the saved object indices [float] ===== Rollback by restoring a backup snapshot: -1. Before proceeding, {ref}/snapshots-take-snapshot.html[take a snapshot] that contains the `kibana` feature state or all `.kibana*` indices. +1. Before proceeding, {ref}/snapshots-take-snapshot.html[take a snapshot] that contains the `kibana` feature state. + Snapshots include this feature state by default. 2. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. 3. Delete all saved object indices with `DELETE /.kibana*` -4. {ref}/snapshots-restore-snapshot.html[Restore] the `kibana` feature state or all `.kibana* indices and their aliases from the snapshot. +4. {ref}/snapshots-restore-snapshot.html[Restore] the `kibana` feature state from the snapshot. 5. Start up all {kib} instances on the older version you wish to rollback to. [float] ===== (Not recommended) Rollback without a backup snapshot: 1. Shutdown all {kib} instances to be 100% sure that there are no {kib} instances currently performing a migration. -2. {ref}/snapshots-take-snapshot.html[Take a snapshot] that includes the `kibana` feature state or all `.kibana*` indices. +2. {ref}/snapshots-take-snapshot.html[Take a snapshot] that includes the `kibana` feature state. Snapshots include this feature state by default. 3. Delete the version specific indices created by the failed upgrade migration. E.g. if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` 4. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. 5. Remove the write block from the rollback indices. `PUT /.kibana,.kibana_task_manager/_settings {"index.blocks.write": false}` diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index 75d0da1c597b6..57668b3f5bccf 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -45,3 +45,5 @@ include::management.asciidoc[] include::api.asciidoc[] include::plugins.asciidoc[] + +include::troubleshooting.asciidoc[] diff --git a/docs/user/troubleshooting.asciidoc b/docs/user/troubleshooting.asciidoc new file mode 100644 index 0000000000000..8b32471c98d86 --- /dev/null +++ b/docs/user/troubleshooting.asciidoc @@ -0,0 +1,70 @@ +[[kibana-troubleshooting]] +== Troubleshooting + +=== Using {kib} server logs +{kib} Logs is a great way to see what's going on in your application and to debug performance issues. Navigating through a large number of generated logs can be overwhelming, and following are some techniques that you can use to optimize the process. + +Start by defining a problem area that you are interested in. For example, you might be interested in seeing how a particular {kib} Plugin is performing, so no need to gather logs for all of {kib}. Or you might want to focus on a particular feature, such as requests from the {kib} server to the {es} server. +Depending on your needs, you can configure {kib} to generate logs for a specific feature. +[source,yml] +---- +logging: + appenders: + file: + type: file + fileName: ./kibana.log + layout: + type: json + +### gather all the Kibana logs into a file +logging.root: + appenders: [file] + level: all + +### or gather a subset of the logs +logging.loggers: + ### responses to an HTTP request + - name: http.server.response + level: debug + appenders: [file] + ### result of a query to the Elasticsearch server + - name: elasticsearch.query + level: debug + appenders: [file] + ### logs generated by my plugin + - name: plugins.myPlugin + level: debug + appenders: [file] +---- +WARNING: Kibana's `file` appender is configured to produce logs in https://www.elastic.co/guide/en/ecs/master/ecs-reference.html[ECS JSON] format. It's the only format that includes the meta information necessary for https://www.elastic.co/guide/en/apm/agent/nodejs/current/log-correlation.html[log correlation] out-of-the-box. + +The next step is to define what https://www.elastic.co/observability[observability tools] are available. +For a better experience, set up an https://www.elastic.co/guide/en/apm/get-started/current/observability-integrations.html[Observability integration] provided by Elastic to debug your application with the <> +To debug something quickly without setting up additional tooling, you can work with <> + +[[debugging-logs-apm-ui]] +==== APM UI +*Prerequisites* {kib} logs are configured to be in https://www.elastic.co/guide/en/ecs/master/ecs-reference.html[ECS JSON] format to include tracing identifiers. + +To debug {kib} with the APM UI, you must set up the APM infrastructure. You can find instructions for the setup process +https://www.elastic.co/guide/en/apm/get-started/current/observability-integrations.html[on the Observability integrations page]. + +Once you set up the APM infrastructure, you can enable the APM agent and put {kib} under load to collect APM events. To analyze the collected metrics and logs, use the APM UI as demonstrated https://www.elastic.co/guide/en/kibana/master/transactions.html#transaction-trace-sample[in the docs]. + +[[plain-kibana-logs]] +==== Plain {kib} logs +*Prerequisites* {kib} logs are configured to be in https://www.elastic.co/guide/en/ecs/master/ecs-reference.html[ECS JSON] format to include tracing identifiers. + +Open {kib} Logs and search for an operation you are interested in. +For example, suppose you want to investigate the response times for queries to the `/api/telemetry/v2/clusters/_stats` {kib} endpoint. +Open Kibana Logs and search for the HTTP server response for the endpoint. It looks similar to the following (some fields are omitted for brevity). +[source,json] +---- +{ + "message":"POST /api/telemetry/v2/clusters/_stats 200 1014ms - 43.2KB", + "log":{"level":"DEBUG","logger":"http.server.response"}, + "trace":{"id":"9b99131a6f66587971ef085ef97dfd07"}, + "transaction":{"id":"d0c5bbf14f5febca"} +} +---- +You are interested in the https://www.elastic.co/guide/en/ecs/current/ecs-tracing.html#field-trace-id[trace.id] field, which is a unique identifier of a trace. The `trace.id` provides a way to group multiple events, like transactions, which belong together. You can search for `"trace":{"id":"9b99131a6f66587971ef085ef97dfd07"}` to get all the logs that belong to the same trace. This enables you to see how many {es} requests were triggered during the `9b99131a6f66587971ef085ef97dfd07` trace, what they looked like, what {es} endpoints were hit, and so on. diff --git a/package.json b/package.json index 374ccee71ec6a..a4f8ae69eda39 100644 --- a/package.json +++ b/package.json @@ -103,13 +103,12 @@ "@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", - "@elastic/eui": "41.0.0", + "@elastic/eui": "41.2.3", "@elastic/filesaver": "1.1.2", - "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.1", "@elastic/react-search-ui": "^1.6.0", @@ -196,8 +195,10 @@ "archiver": "^5.2.0", "axios": "^0.21.1", "base64-js": "^1.3.1", + "bitmap-sdf": "^1.0.3", "brace": "0.11.1", - "broadcast-channel": "^4.5.0", + "broadcast-channel": "^4.7.0", + "canvg": "^3.0.9", "chalk": "^4.1.0", "cheerio": "^1.0.0-rc.10", "chokidar": "^3.4.3", @@ -368,7 +369,7 @@ "redux-thunks": "^1.0.0", "regenerator-runtime": "^0.13.3", "remark-parse": "^8.0.3", - "remark-stringify": "^9.0.0", + "remark-stringify": "^8.0.3", "require-in-the-middle": "^5.1.0", "reselect": "^4.0.0", "resize-observer-polyfill": "^1.5.1", @@ -520,7 +521,6 @@ "@types/ejs": "^3.0.6", "@types/elastic__apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace/npm_module_types", "@types/elastic__datemath": "link:bazel-bin/packages/elastic-datemath/npm_module_types", - "@types/elasticsearch": "^5.0.33", "@types/enzyme": "^3.10.8", "@types/eslint": "^7.28.0", "@types/express": "^4.17.13", @@ -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,10 @@ "@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__dev-utils": "link:bazel-bin/packages/kbn-dev-utils/npm_module_types", + "@types/kbn__docs-utils": "link:bazel-bin/packages/kbn-docs-utils/npm_module_types", + "@types/kbn__es-archiver": "link:bazel-bin/packages/kbn-es-archiver/npm_module_types", + "@types/kbn__es-query": "link:bazel-bin/packages/kbn-es-query/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 +680,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..5fdaa9931bc4d 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -86,6 +86,10 @@ filegroup( "//packages/kbn-config:build_types", "//packages/kbn-config-schema:build_types", "//packages/kbn-crypto:build_types", + "//packages/kbn-dev-utils:build_types", + "//packages/kbn-docs-utils:build_types", + "//packages/kbn-es-archiver:build_types", + "//packages/kbn-es-query:build_types", "//packages/kbn-i18n:build_types", "//packages/kbn-i18n-react:build_types", ], diff --git a/packages/elastic-eslint-config-kibana/react.js b/packages/elastic-eslint-config-kibana/react.js index 29000bdb15684..0b1cce15de9ad 100644 --- a/packages/elastic-eslint-config-kibana/react.js +++ b/packages/elastic-eslint-config-kibana/react.js @@ -1,5 +1,5 @@ const semver = require('semver'); -const { kibanaPackageJson: PKG } = require('@kbn/dev-utils'); +const { kibanaPackageJson: PKG } = require('@kbn/utils'); module.exports = { plugins: [ diff --git a/packages/elastic-eslint-config-kibana/typescript.js b/packages/elastic-eslint-config-kibana/typescript.js index 1a0ef81ae2f1e..3ada725cb1805 100644 --- a/packages/elastic-eslint-config-kibana/typescript.js +++ b/packages/elastic-eslint-config-kibana/typescript.js @@ -4,7 +4,7 @@ // as this package was moved from typescript-eslint-parser to @typescript-eslint/parser const semver = require('semver'); -const { kibanaPackageJson: PKG } = require('@kbn/dev-utils'); +const { kibanaPackageJson: PKG } = require('@kbn/utils'); const eslintConfigPrettierTypescriptEslintRules = require('eslint-config-prettier/@typescript-eslint').rules; 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..cdc40e85c972a 100644 --- a/packages/kbn-cli-dev-mode/BUILD.bazel +++ b/packages/kbn-cli-dev-mode/BUILD.bazel @@ -50,7 +50,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/kbn-config:npm_module_types", "//packages/kbn-config-schema:npm_module_types", - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-logging", "//packages/kbn-optimizer", "//packages/kbn-server-http-tools", @@ -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/cli_dev_mode.test.ts b/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts index e5e009e51e69e..0066644d0825a 100644 --- a/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts +++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts @@ -8,11 +8,9 @@ import Path from 'path'; import * as Rx from 'rxjs'; -import { - REPO_ROOT, - createAbsolutePathSerializer, - createAnyInstanceSerializer, -} from '@kbn/dev-utils'; +import { createAbsolutePathSerializer, createAnyInstanceSerializer } from '@kbn/dev-utils'; + +import { REPO_ROOT } from '@kbn/utils'; import { TestLog } from './log'; import { CliDevMode, SomeCliArgs } from './cli_dev_mode'; diff --git a/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts index 2396b316aa3a2..9cf688b675e67 100644 --- a/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts +++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts @@ -22,7 +22,8 @@ import { takeUntil, } from 'rxjs/operators'; import { CliArgs } from '@kbn/config'; -import { REPO_ROOT, CiStatsReporter } from '@kbn/dev-utils'; +import { CiStatsReporter } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Log, CliLog } from './log'; import { Optimizer } from './optimizer'; 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..25bc59bf78458 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 @@ -8,7 +8,8 @@ import Path from 'path'; -import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { getServerWatchPaths } from './get_server_watch_paths'; @@ -65,7 +66,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..acfc9aeecdc80 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 @@ -9,7 +9,7 @@ import Path from 'path'; import Fs from 'fs'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; interface Options { pluginPaths: string[]; @@ -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-config-schema/src/byte_size_value/index.test.ts b/packages/kbn-config-schema/src/byte_size_value/index.test.ts index a5d0142853416..7a2e3a5d6cb0f 100644 --- a/packages/kbn-config-schema/src/byte_size_value/index.test.ts +++ b/packages/kbn-config-schema/src/byte_size_value/index.test.ts @@ -30,6 +30,11 @@ describe('parsing units', () => { expect(ByteSizeValue.parse('1gb').getValueInBytes()).toBe(1073741824); }); + test('case insensitive units', () => { + expect(ByteSizeValue.parse('1KB').getValueInBytes()).toBe(1024); + expect(ByteSizeValue.parse('1Mb').getValueInBytes()).toBe(1024 * 1024); + }); + test('throws an error when unsupported unit specified', () => { expect(() => ByteSizeValue.parse('1tb')).toThrowErrorMatchingInlineSnapshot( `"Failed to parse value as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."` diff --git a/packages/kbn-config-schema/src/byte_size_value/index.ts b/packages/kbn-config-schema/src/byte_size_value/index.ts index fb90bd70ed5c6..6fabe35b30024 100644 --- a/packages/kbn-config-schema/src/byte_size_value/index.ts +++ b/packages/kbn-config-schema/src/byte_size_value/index.ts @@ -22,7 +22,7 @@ function renderUnit(value: number, unit: string) { export class ByteSizeValue { public static parse(text: string): ByteSizeValue { - const match = /([1-9][0-9]*)(b|kb|mb|gb)/.exec(text); + const match = /([1-9][0-9]*)(b|kb|mb|gb)/i.exec(text); if (!match) { const number = Number(text); if (typeof number !== 'number' || isNaN(number)) { @@ -35,7 +35,7 @@ export class ByteSizeValue { } const value = parseInt(match[1], 10); - const unit = match[2]; + const unit = match[2].toLowerCase(); return new ByteSizeValue(value * unitMultiplier[unit]); } diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index 81ee6d770103c..f71c8b866fd5d 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -34,7 +34,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "@npm//@types/flot", "@npm//@types/jest", "@npm//@types/node", diff --git a/packages/kbn-dev-utils/BUILD.bazel b/packages/kbn-dev-utils/BUILD.bazel index 4fd99e0144cb6..89df1870a3cec 100644 --- a/packages/kbn-dev-utils/BUILD.bazel +++ b/packages/kbn-dev-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-dev-utils" PKG_REQUIRE_NAME = "@kbn/dev-utils" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__dev-utils" SOURCE_FILES = glob( [ @@ -43,7 +44,6 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ - "//packages/kbn-std", "//packages/kbn-utils", "@npm//@babel/core", "@npm//axios", @@ -66,7 +66,6 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-std", "//packages/kbn-utils", "@npm//@babel/parser", "@npm//@babel/types", @@ -124,7 +123,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"], ) @@ -143,3 +142,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-dev-utils/package.json b/packages/kbn-dev-utils/package.json index 9d6e6dde86fac..ab4f489e7d345 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -4,7 +4,6 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "./target_node/index.js", - "types": "./target_types/index.d.ts", "kibana": { "devOnly": true } diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 381e99ac677f5..9b207ad9e9966 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -export * from '@kbn/utils'; export { withProcRunner, ProcRunner } from './proc_runner'; export * from './tooling_log'; export * from './serializers'; diff --git a/packages/kbn-dev-utils/src/tooling_log/__snapshots__/tooling_log_text_writer.test.ts.snap b/packages/kbn-dev-utils/src/tooling_log/__snapshots__/tooling_log_text_writer.test.ts.snap index 7ff982acafbe4..5fa074d4c7739 100644 --- a/packages/kbn-dev-utils/src/tooling_log/__snapshots__/tooling_log_text_writer.test.ts.snap +++ b/packages/kbn-dev-utils/src/tooling_log/__snapshots__/tooling_log_text_writer.test.ts.snap @@ -170,6 +170,14 @@ exports[`level:warning/type:warning snapshots: output 1`] = ` " `; +exports[`never ignores write messages from the kibana elasticsearch.deprecation logger context 1`] = ` +" │[elasticsearch.deprecation] + │{ foo: { bar: { '1': [Array] } }, bar: { bar: { '1': [Array] } } } + │ + │Infinity +" +`; + exports[`throws error if created with invalid level 1`] = `"Invalid log level \\"foo\\" (expected one of silent,error,warning,success,info,debug,verbose)"`; exports[`throws error if writeTo config is not defined or doesn't have a write method 1`] = `"ToolingLogTextWriter requires the \`writeTo\` option be set to a stream (like process.stdout)"`; diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.test.ts b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.test.ts index b4668f29b6e21..fbccfdcdf6ac0 100644 --- a/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.test.ts +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.test.ts @@ -88,3 +88,55 @@ it('formats %s patterns and indents multi-line messages correctly', () => { const output = write.mock.calls.reduce((acc, chunk) => `${acc}${chunk}`, ''); expect(output).toMatchSnapshot(); }); + +it('does not write messages from sources in ignoreSources', () => { + const write = jest.fn(); + const writer = new ToolingLogTextWriter({ + ignoreSources: ['myIgnoredSource'], + level: 'debug', + writeTo: { + write, + }, + }); + + writer.write({ + source: 'myIgnoredSource', + type: 'success', + indent: 10, + args: [ + '%s\n%O\n\n%d', + 'foo bar', + { foo: { bar: { 1: [1, 2, 3] } }, bar: { bar: { 1: [1, 2, 3] } } }, + Infinity, + ], + }); + + const output = write.mock.calls.reduce((acc, chunk) => `${acc}${chunk}`, ''); + expect(output).toEqual(''); +}); + +it('never ignores write messages from the kibana elasticsearch.deprecation logger context', () => { + const write = jest.fn(); + const writer = new ToolingLogTextWriter({ + ignoreSources: ['myIgnoredSource'], + level: 'debug', + writeTo: { + write, + }, + }); + + writer.write({ + source: 'myIgnoredSource', + type: 'write', + indent: 10, + args: [ + '%s\n%O\n\n%d', + '[elasticsearch.deprecation]', + { foo: { bar: { 1: [1, 2, 3] } }, bar: { bar: { 1: [1, 2, 3] } } }, + Infinity, + ], + }); + + const output = write.mock.calls.reduce((acc, chunk) => `${acc}${chunk}`, ''); + expect(output).toMatchSnapshot(); +}); diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.ts b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.ts index 660dae3fa1f55..4fe33241cf77e 100644 --- a/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.ts +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.ts @@ -92,7 +92,15 @@ export class ToolingLogTextWriter implements Writer { } if (this.ignoreSources && msg.source && this.ignoreSources.includes(msg.source)) { - return false; + if (msg.type === 'write') { + const txt = format(msg.args[0], ...msg.args.slice(1)); + // Ensure that Elasticsearch deprecation log messages from Kibana aren't ignored + if (!/elasticsearch\.deprecation/.test(txt)) { + return false; + } + } else { + return false; + } } const prefix = has(MSG_PREFIXES, msg.type) ? MSG_PREFIXES[msg.type] : ''; diff --git a/packages/kbn-docs-utils/BUILD.bazel b/packages/kbn-docs-utils/BUILD.bazel index 6bb37b3500152..edfd3ee96c181 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( [ @@ -37,7 +38,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/kbn-config:npm_module_types", - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-utils", "@npm//ts-morph", "@npm//@types/dedent", @@ -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-docs-utils/src/api_docs/build_api_docs_cli.ts b/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts index 2e4ce08540714..3c9137b260a3e 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts @@ -9,7 +9,8 @@ import Fs from 'fs'; import Path from 'path'; -import { REPO_ROOT, run, CiStatsReporter, createFlagError } from '@kbn/dev-utils'; +import { run, CiStatsReporter, createFlagError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Project } from 'ts-morph'; import { writePluginDocs } from './mdx/write_plugin_mdx_docs'; @@ -241,7 +242,7 @@ export function runBuildApiDocsCli() { boolean: ['references'], help: ` --plugin Optionally, run for only a specific plugin - --stats Optionally print API stats. Must be one or more of: any, comments or exports. + --stats Optionally print API stats. Must be one or more of: any, comments or exports. --references Collect references for API items `, }, diff --git a/packages/kbn-docs-utils/src/api_docs/find_plugins.ts b/packages/kbn-docs-utils/src/api_docs/find_plugins.ts index 78cba3f3a9476..774452a6f1f9f 100644 --- a/packages/kbn-docs-utils/src/api_docs/find_plugins.ts +++ b/packages/kbn-docs-utils/src/api_docs/find_plugins.ts @@ -12,7 +12,8 @@ import globby from 'globby'; import loadJsonFile from 'load-json-file'; import { getPluginSearchPaths } from '@kbn/config'; -import { simpleKibanaPlatformPluginDiscovery, REPO_ROOT } from '@kbn/dev-utils'; +import { simpleKibanaPlatformPluginDiscovery } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { ApiScope, PluginOrPackage } from './types'; export function findPlugins(): PluginOrPackage[] { diff --git a/packages/kbn-es-archiver/BUILD.bazel b/packages/kbn-es-archiver/BUILD.bazel index 2dc311ed74406..da8aaf913ab67 100644 --- a/packages/kbn-es-archiver/BUILD.bazel +++ b/packages/kbn-es-archiver/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-es-archiver" PKG_REQUIRE_NAME = "@kbn/es-archiver" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__es-archiver" SOURCE_FILES = glob( [ @@ -43,7 +44,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-test", "//packages/kbn-utils", "@npm//@elastic/elasticsearch", @@ -90,7 +91,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"], ) @@ -109,3 +110,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-es-archiver/package.json b/packages/kbn-es-archiver/package.json index 0cce08eaf0352..bff3990a0c1bc 100644 --- a/packages/kbn-es-archiver/package.json +++ b/packages/kbn-es-archiver/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-es-archiver/src/actions/load.ts b/packages/kbn-es-archiver/src/actions/load.ts index 0a7235c566b52..c5bea5e29a687 100644 --- a/packages/kbn-es-archiver/src/actions/load.ts +++ b/packages/kbn-es-archiver/src/actions/load.ts @@ -9,7 +9,8 @@ import { resolve, relative } from 'path'; import { createReadStream } from 'fs'; import { Readable } from 'stream'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { KbnClient } from '@kbn/test'; import type { Client } from '@elastic/elasticsearch'; import { createPromiseFromStreams, concatStreamProviders } from '@kbn/utils'; diff --git a/packages/kbn-es-archiver/src/actions/rebuild_all.ts b/packages/kbn-es-archiver/src/actions/rebuild_all.ts index 360fdb438f2db..27fcae0c7cec5 100644 --- a/packages/kbn-es-archiver/src/actions/rebuild_all.ts +++ b/packages/kbn-es-archiver/src/actions/rebuild_all.ts @@ -10,8 +10,8 @@ import { resolve, relative } from 'path'; import { Stats, createReadStream, createWriteStream } from 'fs'; import { stat, rename } from 'fs/promises'; import { Readable, Writable } from 'stream'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; -import { createPromiseFromStreams } from '@kbn/utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { createPromiseFromStreams, REPO_ROOT } from '@kbn/utils'; import { prioritizeMappings, readDirectory, diff --git a/packages/kbn-es-archiver/src/actions/save.ts b/packages/kbn-es-archiver/src/actions/save.ts index 9cb5be05ac060..e5e3f06b8436d 100644 --- a/packages/kbn-es-archiver/src/actions/save.ts +++ b/packages/kbn-es-archiver/src/actions/save.ts @@ -10,8 +10,8 @@ import { resolve, relative } from 'path'; import { createWriteStream, mkdirSync } from 'fs'; import { Readable, Writable } from 'stream'; import type { Client } from '@elastic/elasticsearch'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; -import { createListStream, createPromiseFromStreams } from '@kbn/utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { createListStream, createPromiseFromStreams, REPO_ROOT } from '@kbn/utils'; import { createStats, diff --git a/packages/kbn-es-archiver/src/actions/unload.ts b/packages/kbn-es-archiver/src/actions/unload.ts index 1c5f4cd5d7d03..22830b7289174 100644 --- a/packages/kbn-es-archiver/src/actions/unload.ts +++ b/packages/kbn-es-archiver/src/actions/unload.ts @@ -10,9 +10,9 @@ import { resolve, relative } from 'path'; import { createReadStream } from 'fs'; import { Readable, Writable } from 'stream'; import type { Client } from '@elastic/elasticsearch'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; -import { createPromiseFromStreams } from '@kbn/utils'; +import { createPromiseFromStreams, REPO_ROOT } from '@kbn/utils'; import { isGzip, diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index 354197a98fa46..e13e20f25a703 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -10,7 +10,8 @@ import Fs from 'fs'; import Path from 'path'; import type { Client } from '@elastic/elasticsearch'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { KbnClient } from '@kbn/test'; import { diff --git a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts index ae21649690a99..2590074a25411 100644 --- a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts @@ -6,13 +6,14 @@ * Side Public License, v 1. */ +import { ToolingLog } from '@kbn/dev-utils'; + import { createListStream, createPromiseFromStreams, createConcatStream, createMapStream, - ToolingLog, -} from '@kbn/dev-utils'; +} from '@kbn/utils'; import { createGenerateDocRecordsStream } from './generate_doc_records_stream'; import { Progress } from '../progress'; diff --git a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts index bcf28a4976a1c..9c0ff4a8f91ec 100644 --- a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts @@ -6,12 +6,9 @@ * Side Public License, v 1. */ -import { - createListStream, - createPromiseFromStreams, - ToolingLog, - createRecursiveSerializer, -} from '@kbn/dev-utils'; +import { ToolingLog, createRecursiveSerializer } from '@kbn/dev-utils'; + +import { createListStream, createPromiseFromStreams } from '@kbn/utils'; import { Progress } from '../progress'; import { createIndexDocRecordsStream } from './index_doc_records_stream'; diff --git a/packages/kbn-es-query/BUILD.bazel b/packages/kbn-es-query/BUILD.bazel index 70d8d659c99fe..86f3d3ccc13a8 100644 --- a/packages/kbn-es-query/BUILD.bazel +++ b/packages/kbn-es-query/BUILD.bazel @@ -1,10 +1,11 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@npm//@bazel/typescript:index.bzl", "ts_config") load("@npm//peggy:index.bzl", "peggy") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +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-es-query" PKG_REQUIRE_NAME = "@kbn/es-query" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__es-query" SOURCE_FILES = glob( [ @@ -104,7 +105,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES + [":grammar"], - deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -123,3 +124,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-es-query/package.json b/packages/kbn-es-query/package.json index 335ef61b8b360..b317ce4ca4c95 100644 --- a/packages/kbn-es-query/package.json +++ b/packages/kbn-es-query/package.json @@ -2,7 +2,6 @@ "name": "@kbn/es-query", "browser": "./target_web/index.js", "main": "./target_node/index.js", - "types": "./target_types/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "private": true diff --git a/packages/kbn-es-query/src/kuery/index.ts b/packages/kbn-es-query/src/kuery/index.ts index 868904125dc44..13039956916cb 100644 --- a/packages/kbn-es-query/src/kuery/index.ts +++ b/packages/kbn-es-query/src/kuery/index.ts @@ -23,4 +23,5 @@ export const toElasticsearchQuery = (...params: Parameters { it('should return artifact metadata for the correct architecture', async () => { const artifact = await Artifact.getSnapshot('oss', MOCK_VERSION, log); - expect(artifact.getFilename()).toEqual(MOCK_FILENAME + `-${ARCHITECTURE}.oss`); + expect(artifact.spec.filename).toEqual(MOCK_FILENAME + `-${ARCHITECTURE}.oss`); }); }); @@ -182,7 +182,7 @@ describe('Artifact', () => { describe('with latest unverified snapshot', () => { beforeEach(() => { - process.env.KBN_ES_SNAPSHOT_USE_UNVERIFIED = 1; + process.env.KBN_ES_SNAPSHOT_USE_UNVERIFIED = '1'; mockFetch(MOCKS.valid); }); diff --git a/packages/kbn-es/src/artifact.js b/packages/kbn-es/src/artifact.ts similarity index 65% rename from packages/kbn-es/src/artifact.js rename to packages/kbn-es/src/artifact.ts index 0fa2c7a1727d0..9c5935c96e8cd 100644 --- a/packages/kbn-es/src/artifact.js +++ b/packages/kbn-es/src/artifact.ts @@ -6,25 +6,69 @@ * Side Public License, v 1. */ -const fetch = require('node-fetch'); -const AbortController = require('abort-controller'); -const fs = require('fs'); -const { promisify } = require('util'); -const { pipeline, Transform } = require('stream'); -const chalk = require('chalk'); -const { createHash } = require('crypto'); -const path = require('path'); +import fs from 'fs'; +import { promisify } from 'util'; +import path from 'path'; +import { createHash } from 'crypto'; +import { pipeline, Transform } from 'stream'; +import { setTimeout } from 'timers/promises'; + +import fetch, { Headers } from 'node-fetch'; +import AbortController from 'abort-controller'; +import chalk from 'chalk'; +import { ToolingLog } from '@kbn/dev-utils'; + +import { cache } from './utils/cache'; +import { resolveCustomSnapshotUrl } from './custom_snapshots'; +import { createCliError, isCliError } from './errors'; const asyncPipeline = promisify(pipeline); const DAILY_SNAPSHOTS_BASE_URL = 'https://storage.googleapis.com/kibana-ci-es-snapshots-daily'; const PERMANENT_SNAPSHOTS_BASE_URL = 'https://storage.googleapis.com/kibana-ci-es-snapshots-permanent'; -const { cache } = require('./utils'); -const { resolveCustomSnapshotUrl } = require('./custom_snapshots'); -const { createCliError, isCliError } = require('./errors'); +type ChecksumType = 'sha512'; +export type ArtifactLicense = 'oss' | 'basic' | 'trial'; + +interface ArtifactManifest { + id: string; + bucket: string; + branch: string; + sha: string; + sha_short: string; + version: string; + generated: string; + archives: Array<{ + filename: string; + checksum: string; + url: string; + version: string; + platform: string; + architecture: string; + license: string; + }>; +} + +export interface ArtifactSpec { + url: string; + checksumUrl: string; + checksumType: ChecksumType; + filename: string; +} + +interface ArtifactDownloaded { + cached: false; + checksum: string; + etag?: string; + contentLength: number; + first500Bytes: Buffer; + headers: Headers; +} +interface ArtifactCached { + cached: true; +} -function getChecksumType(checksumUrl) { +function getChecksumType(checksumUrl: string): ChecksumType { if (checksumUrl.endsWith('.sha512')) { return 'sha512'; } @@ -32,15 +76,18 @@ function getChecksumType(checksumUrl) { throw new Error(`unable to determine checksum type: ${checksumUrl}`); } -function headersToString(headers, indent = '') { +function headersToString(headers: Headers, indent = '') { return [...headers.entries()].reduce( (acc, [key, value]) => `${acc}\n${indent}${key}: ${value}`, '' ); } -async function retry(log, fn) { - async function doAttempt(attempt) { +async function retry(log: ToolingLog, fn: () => Promise): Promise { + let attempt = 0; + while (true) { + attempt += 1; + try { return await fn(); } catch (error) { @@ -49,13 +96,10 @@ async function retry(log, fn) { } log.warning('...failure, retrying in 5 seconds:', error.message); - await new Promise((resolve) => setTimeout(resolve, 5000)); + await setTimeout(5000); log.info('...retrying'); - return await doAttempt(attempt + 1); } } - - return await doAttempt(1); } // Setting this flag provides an easy way to run the latest un-promoted snapshot without having to look it up @@ -63,7 +107,7 @@ function shouldUseUnverifiedSnapshot() { return !!process.env.KBN_ES_SNAPSHOT_USE_UNVERIFIED; } -async function fetchSnapshotManifest(url, log) { +async function fetchSnapshotManifest(url: string, log: ToolingLog) { log.info('Downloading snapshot manifest from %s', chalk.bold(url)); const abc = new AbortController(); @@ -73,7 +117,11 @@ async function fetchSnapshotManifest(url, log) { return { abc, resp, json }; } -async function getArtifactSpecForSnapshot(urlVersion, license, log) { +async function getArtifactSpecForSnapshot( + urlVersion: string, + license: string, + log: ToolingLog +): Promise { const desiredVersion = urlVersion.replace('-SNAPSHOT', ''); const desiredLicense = license === 'oss' ? 'oss' : 'default'; @@ -103,17 +151,16 @@ async function getArtifactSpecForSnapshot(urlVersion, license, log) { throw new Error(`Unable to read snapshot manifest: ${resp.statusText}\n ${json}`); } - const manifest = JSON.parse(json); - + const manifest: ArtifactManifest = JSON.parse(json); const platform = process.platform === 'win32' ? 'windows' : process.platform; const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64'; const archive = manifest.archives.find( - (archive) => - archive.version === desiredVersion && - archive.platform === platform && - archive.license === desiredLicense && - archive.architecture === arch + (a) => + a.version === desiredVersion && + a.platform === platform && + a.license === desiredLicense && + a.architecture === arch ); if (!archive) { @@ -130,93 +177,65 @@ async function getArtifactSpecForSnapshot(urlVersion, license, log) { }; } -exports.Artifact = class Artifact { +export class Artifact { /** * Fetch an Artifact from the Artifact API for a license level and version - * @param {('oss'|'basic'|'trial')} license - * @param {string} version - * @param {ToolingLog} log */ - static async getSnapshot(license, version, log) { + static async getSnapshot(license: ArtifactLicense, version: string, log: ToolingLog) { const urlVersion = `${encodeURIComponent(version)}-SNAPSHOT`; const customSnapshotArtifactSpec = resolveCustomSnapshotUrl(urlVersion, license); if (customSnapshotArtifactSpec) { - return new Artifact(customSnapshotArtifactSpec, log); + return new Artifact(log, customSnapshotArtifactSpec); } const artifactSpec = await getArtifactSpecForSnapshot(urlVersion, license, log); - return new Artifact(artifactSpec, log); + return new Artifact(log, artifactSpec); } /** * Fetch an Artifact from the Elasticsearch past releases url - * @param {string} url - * @param {ToolingLog} log */ - static async getArchive(url, log) { + static async getArchive(url: string, log: ToolingLog) { const shaUrl = `${url}.sha512`; - const artifactSpec = { - url: url, + return new Artifact(log, { + url, filename: path.basename(url), checksumUrl: shaUrl, checksumType: getChecksumType(shaUrl), - }; - - return new Artifact(artifactSpec, log); - } - - constructor(spec, log) { - this._spec = spec; - this._log = log; - } - - getUrl() { - return this._spec.url; - } - - getChecksumUrl() { - return this._spec.checksumUrl; + }); } - getChecksumType() { - return this._spec.checksumType; - } - - getFilename() { - return this._spec.filename; - } + constructor(private readonly log: ToolingLog, public readonly spec: ArtifactSpec) {} /** * Download the artifact to disk, skips the download if the cache is * up-to-date, verifies checksum when downloaded - * @param {string} dest - * @return {Promise} */ - async download(dest, { useCached = false }) { - await retry(this._log, async () => { + async download(dest: string, { useCached = false }: { useCached?: boolean } = {}) { + await retry(this.log, async () => { const cacheMeta = cache.readMeta(dest); const tmpPath = `${dest}.tmp`; if (useCached) { if (cacheMeta.exists) { - this._log.info( + this.log.info( 'use-cached passed, forcing to use existing snapshot', chalk.bold(cacheMeta.ts) ); return; } else { - this._log.info('use-cached passed but no cached snapshot found. Continuing to download'); + this.log.info('use-cached passed but no cached snapshot found. Continuing to download'); } } - const artifactResp = await this._download(tmpPath, cacheMeta.etag, cacheMeta.ts); + const artifactResp = await this.fetchArtifact(tmpPath, cacheMeta.etag, cacheMeta.ts); if (artifactResp.cached) { return; } - await this._verifyChecksum(artifactResp); + await this.verifyChecksum(artifactResp); // cache the etag for future downloads cache.writeMeta(dest, { etag: artifactResp.etag }); @@ -228,18 +247,18 @@ exports.Artifact = class Artifact { /** * Fetch the artifact with an etag - * @param {string} tmpPath - * @param {string} etag - * @param {string} ts - * @return {{ cached: true }|{ checksum: string, etag: string, first500Bytes: Buffer }} */ - async _download(tmpPath, etag, ts) { - const url = this.getUrl(); + private async fetchArtifact( + tmpPath: string, + etag: string, + ts: string + ): Promise { + const url = this.spec.url; if (etag) { - this._log.info('verifying cache of %s', chalk.bold(url)); + this.log.info('verifying cache of %s', chalk.bold(url)); } else { - this._log.info('downloading artifact from %s', chalk.bold(url)); + this.log.info('downloading artifact from %s', chalk.bold(url)); } const abc = new AbortController(); @@ -251,7 +270,7 @@ exports.Artifact = class Artifact { }); if (resp.status === 304) { - this._log.info('etags match, reusing cache from %s', chalk.bold(ts)); + this.log.info('etags match, reusing cache from %s', chalk.bold(ts)); abc.abort(); return { @@ -270,10 +289,10 @@ exports.Artifact = class Artifact { } if (etag) { - this._log.info('cache invalid, redownloading'); + this.log.info('cache invalid, redownloading'); } - const hash = createHash(this.getChecksumType()); + const hash = createHash(this.spec.checksumType); let first500Bytes = Buffer.alloc(0); let contentLength = 0; @@ -300,8 +319,9 @@ exports.Artifact = class Artifact { ); return { + cached: false, checksum: hash.digest('hex'), - etag: resp.headers.get('etag'), + etag: resp.headers.get('etag') ?? undefined, contentLength, first500Bytes, headers: resp.headers, @@ -310,14 +330,12 @@ exports.Artifact = class Artifact { /** * Verify the checksum of the downloaded artifact with the checksum at checksumUrl - * @param {{ checksum: string, contentLength: number, first500Bytes: Buffer }} artifactResp - * @return {Promise} */ - async _verifyChecksum(artifactResp) { - this._log.info('downloading artifact checksum from %s', chalk.bold(this.getChecksumUrl())); + private async verifyChecksum(artifactResp: ArtifactDownloaded) { + this.log.info('downloading artifact checksum from %s', chalk.bold(this.spec.checksumUrl)); const abc = new AbortController(); - const resp = await fetch(this.getChecksumUrl(), { + const resp = await fetch(this.spec.checksumUrl, { signal: abc.signal, }); @@ -338,7 +356,7 @@ exports.Artifact = class Artifact { const lenString = `${len} / ${artifactResp.contentLength}`; throw createCliError( - `artifact downloaded from ${this.getUrl()} does not match expected checksum\n` + + `artifact downloaded from ${this.spec.url} does not match expected checksum\n` + ` expected: ${expectedChecksum}\n` + ` received: ${artifactResp.checksum}\n` + ` headers: ${headersToString(artifactResp.headers, ' ')}\n` + @@ -346,6 +364,6 @@ exports.Artifact = class Artifact { ); } - this._log.info('checksum verified'); + this.log.info('checksum verified'); } -}; +} diff --git a/packages/kbn-es/src/custom_snapshots.js b/packages/kbn-es/src/custom_snapshots.ts similarity index 82% rename from packages/kbn-es/src/custom_snapshots.js rename to packages/kbn-es/src/custom_snapshots.ts index 9dd8097244947..f3e6d3ecaf857 100644 --- a/packages/kbn-es/src/custom_snapshots.js +++ b/packages/kbn-es/src/custom_snapshots.ts @@ -6,13 +6,15 @@ * Side Public License, v 1. */ -const { basename } = require('path'); +import Path from 'path'; -function isVersionFlag(a) { +import type { ArtifactSpec } from './artifact'; + +function isVersionFlag(a: string) { return a.startsWith('--version'); } -function getCustomSnapshotUrl() { +export function getCustomSnapshotUrl() { // force use of manually created snapshots until ReindexPutMappings fix if ( !process.env.ES_SNAPSHOT_MANIFEST && @@ -28,7 +30,10 @@ function getCustomSnapshotUrl() { } } -function resolveCustomSnapshotUrl(urlVersion, license) { +export function resolveCustomSnapshotUrl( + urlVersion: string, + license: string +): ArtifactSpec | undefined { const customSnapshotUrl = getCustomSnapshotUrl(); if (!customSnapshotUrl) { @@ -48,8 +53,6 @@ function resolveCustomSnapshotUrl(urlVersion, license) { url: overrideUrl, checksumUrl: overrideUrl + '.sha512', checksumType: 'sha512', - filename: basename(overrideUrl), + filename: Path.basename(overrideUrl), }; } - -module.exports = { getCustomSnapshotUrl, resolveCustomSnapshotUrl }; diff --git a/packages/kbn-es/src/errors.ts b/packages/kbn-es/src/errors.ts new file mode 100644 index 0000000000000..a0c526dc48a9c --- /dev/null +++ b/packages/kbn-es/src/errors.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 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. + */ + +interface CliError extends Error { + isCliError: boolean; +} + +export function createCliError(message: string) { + return Object.assign(new Error(message), { + isCliError: true, + }); +} + +function isObj(x: unknown): x is Record { + return typeof x === 'object' && x !== null; +} + +export function isCliError(error: unknown): error is CliError { + return isObj(error) && error.isCliError === true; +} diff --git a/src/plugins/discover/public/utils/get_single_doc_url.ts b/packages/kbn-es/src/index.ts similarity index 65% rename from src/plugins/discover/public/utils/get_single_doc_url.ts rename to packages/kbn-es/src/index.ts index 913463e6d44a4..68fd931794c0c 100644 --- a/src/plugins/discover/public/utils/get_single_doc_url.ts +++ b/packages/kbn-es/src/index.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ -export const getSingleDocUrl = (indexPatternId: string, rowIndex: string, rowId: string) => { - return `/app/discover#/doc/${indexPatternId}/${rowIndex}?id=${encodeURIComponent(rowId)}`; -}; +// @ts-expect-error not typed yet +export { run } from './cli'; +// @ts-expect-error not typed yet +export { Cluster } from './cluster'; diff --git a/packages/kbn-es/src/install/index.ts b/packages/kbn-es/src/install/index.ts new file mode 100644 index 0000000000000..e827dee2247f9 --- /dev/null +++ b/packages/kbn-es/src/install/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { installArchive } from './install_archive'; +export { installSnapshot, downloadSnapshot } from './install_snapshot'; +export { installSource } from './install_source'; diff --git a/packages/kbn-es/src/install/archive.js b/packages/kbn-es/src/install/install_archive.ts similarity index 64% rename from packages/kbn-es/src/install/archive.js rename to packages/kbn-es/src/install/install_archive.ts index 76db5a4427e6d..ee04d9e4b62b5 100644 --- a/packages/kbn-es/src/install/archive.js +++ b/packages/kbn-es/src/install/install_archive.ts @@ -6,29 +6,40 @@ * Side Public License, v 1. */ -const fs = require('fs'); -const path = require('path'); -const chalk = require('chalk'); -const execa = require('execa'); -const del = require('del'); -const url = require('url'); -const { extract } = require('@kbn/dev-utils'); -const { log: defaultLog } = require('../utils'); -const { BASE_PATH, ES_CONFIG, ES_KEYSTORE_BIN } = require('../paths'); -const { Artifact } = require('../artifact'); -const { parseSettings, SettingsFilter } = require('../settings'); +import fs from 'fs'; +import path from 'path'; + +import chalk from 'chalk'; +import execa from 'execa'; +import del from 'del'; +import { extract, ToolingLog } from '@kbn/dev-utils'; + +import { BASE_PATH, ES_CONFIG, ES_KEYSTORE_BIN } from '../paths'; +import { Artifact } from '../artifact'; +import { parseSettings, SettingsFilter } from '../settings'; +import { log as defaultLog } from '../utils/log'; + +interface InstallArchiveOptions { + license?: string; + password?: string; + basePath?: string; + installPath?: string; + log?: ToolingLog; + esArgs?: string[]; +} + +const isHttpUrl = (str: string) => { + try { + return ['http:', 'https:'].includes(new URL(str).protocol); + } catch { + return false; + } +}; /** * Extracts an ES archive and optionally installs plugins - * - * @param {String} archive - path to tar - * @param {Object} options - * @property {('oss'|'basic'|'trial')} options.license - * @property {String} options.basePath - * @property {String} options.installPath - * @property {ToolingLog} options.log */ -exports.installArchive = async function installArchive(archive, options = {}) { +export async function installArchive(archive: string, options: InstallArchiveOptions = {}) { const { license = 'basic', password = 'changeme', @@ -39,9 +50,9 @@ exports.installArchive = async function installArchive(archive, options = {}) { } = options; let dest = archive; - if (['http:', 'https:'].includes(url.parse(archive).protocol)) { + if (isHttpUrl(archive)) { const artifact = await Artifact.getArchive(archive, log); - dest = path.resolve(basePath, 'cache', artifact.getFilename()); + dest = path.resolve(basePath, 'cache', artifact.spec.filename); await artifact.download(dest); } @@ -75,28 +86,23 @@ exports.installArchive = async function installArchive(archive, options = {}) { } return { installPath }; -}; +} /** * Appends single line to elasticsearch.yml config file - * - * @param {String} installPath - * @param {String} key - * @param {String} value */ -async function appendToConfig(installPath, key, value) { +async function appendToConfig(installPath: string, key: string, value: string) { fs.appendFileSync(path.resolve(installPath, ES_CONFIG), `${key}: ${value}\n`, 'utf8'); } /** * Creates and configures Keystore - * - * @param {String} installPath - * @param {ToolingLog} log - * @param {Array<[string, string]>} secureSettings List of custom Elasticsearch secure settings to - * add into the keystore. */ -async function configureKeystore(installPath, log = defaultLog, secureSettings) { +async function configureKeystore( + installPath: string, + log: ToolingLog = defaultLog, + secureSettings: Array<[string, string]> +) { const env = { JAVA_HOME: '' }; await execa(ES_KEYSTORE_BIN, ['create'], { cwd: installPath, env }); diff --git a/packages/kbn-es/src/install/snapshot.js b/packages/kbn-es/src/install/install_snapshot.ts similarity index 55% rename from packages/kbn-es/src/install/snapshot.js rename to packages/kbn-es/src/install/install_snapshot.ts index cf1ce50f7e413..84d713745eb82 100644 --- a/packages/kbn-es/src/install/snapshot.js +++ b/packages/kbn-es/src/install/install_snapshot.ts @@ -6,56 +6,58 @@ * Side Public License, v 1. */ -const chalk = require('chalk'); -const path = require('path'); -const { BASE_PATH } = require('../paths'); -const { installArchive } = require('./archive'); -const { log: defaultLog } = require('../utils'); -const { Artifact } = require('../artifact'); +import path from 'path'; + +import chalk from 'chalk'; +import { ToolingLog } from '@kbn/dev-utils'; + +import { BASE_PATH } from '../paths'; +import { installArchive } from './install_archive'; +import { log as defaultLog } from '../utils/log'; +import { Artifact, ArtifactLicense } from '../artifact'; + +interface DownloadSnapshotOptions { + version: string; + license?: ArtifactLicense; + basePath?: string; + installPath?: string; + log?: ToolingLog; + useCached?: boolean; +} /** * Download an ES snapshot - * - * @param {Object} options - * @property {('oss'|'basic'|'trial')} options.license - * @property {String} options.version - * @property {String} options.basePath - * @property {String} options.installPath - * @property {ToolingLog} options.log */ -exports.downloadSnapshot = async function installSnapshot({ +export async function downloadSnapshot({ license = 'basic', version, basePath = BASE_PATH, installPath = path.resolve(basePath, version), log = defaultLog, useCached = false, -}) { +}: DownloadSnapshotOptions) { log.info('version: %s', chalk.bold(version)); log.info('install path: %s', chalk.bold(installPath)); log.info('license: %s', chalk.bold(license)); const artifact = await Artifact.getSnapshot(license, version, log); - const dest = path.resolve(basePath, 'cache', artifact.getFilename()); + const dest = path.resolve(basePath, 'cache', artifact.spec.filename); await artifact.download(dest, { useCached }); return { downloadPath: dest, }; -}; +} + +interface InstallSnapshotOptions extends DownloadSnapshotOptions { + password?: string; + esArgs?: string[]; +} /** * Installs ES from snapshot - * - * @param {Object} options - * @property {('oss'|'basic'|'trial')} options.license - * @property {String} options.password - * @property {String} options.version - * @property {String} options.basePath - * @property {String} options.installPath - * @property {ToolingLog} options.log */ -exports.installSnapshot = async function installSnapshot({ +export async function installSnapshot({ license = 'basic', password = 'password', version, @@ -64,8 +66,8 @@ exports.installSnapshot = async function installSnapshot({ log = defaultLog, esArgs, useCached = false, -}) { - const { downloadPath } = await exports.downloadSnapshot({ +}: InstallSnapshotOptions) { + const { downloadPath } = await downloadSnapshot({ license, version, basePath, @@ -82,4 +84,4 @@ exports.installSnapshot = async function installSnapshot({ log, esArgs, }); -}; +} diff --git a/packages/kbn-es/src/install/source.js b/packages/kbn-es/src/install/install_source.ts similarity index 73% rename from packages/kbn-es/src/install/source.js rename to packages/kbn-es/src/install/install_source.ts index 81a1019509906..d8c272677058e 100644 --- a/packages/kbn-es/src/install/source.js +++ b/packages/kbn-es/src/install/install_source.ts @@ -6,28 +6,35 @@ * Side Public License, v 1. */ -const path = require('path'); -const fs = require('fs'); -const os = require('os'); -const chalk = require('chalk'); -const crypto = require('crypto'); -const simpleGit = require('simple-git/promise'); -const { installArchive } = require('./archive'); -const { log: defaultLog, cache, buildSnapshot, archiveForPlatform } = require('../utils'); -const { BASE_PATH } = require('../paths'); +import path from 'path'; +import fs from 'fs'; +import os from 'os'; +import crypto from 'crypto'; + +import chalk from 'chalk'; +import simpleGit from 'simple-git/promise'; +import { ToolingLog } from '@kbn/dev-utils'; + +import { installArchive } from './install_archive'; +import { log as defaultLog } from '../utils/log'; +import { cache } from '../utils/cache'; +import { buildSnapshot, archiveForPlatform } from '../utils/build_snapshot'; +import { BASE_PATH } from '../paths'; + +interface InstallSourceOptions { + sourcePath: string; + license?: string; + password?: string; + basePath?: string; + installPath?: string; + log?: ToolingLog; + esArgs?: string[]; +} /** * Installs ES from source - * - * @param {Object} options - * @property {('oss'|'basic'|'trial')} options.license - * @property {String} options.password - * @property {String} options.sourcePath - * @property {String} options.basePath - * @property {String} options.installPath - * @property {ToolingLog} options.log */ -exports.installSource = async function installSource({ +export async function installSource({ license = 'basic', password = 'changeme', sourcePath, @@ -35,7 +42,7 @@ exports.installSource = async function installSource({ installPath = path.resolve(basePath, 'source'), log = defaultLog, esArgs, -}) { +}: InstallSourceOptions) { log.info('source path: %s', chalk.bold(sourcePath)); log.info('install path: %s', chalk.bold(installPath)); log.info('license: %s', chalk.bold(license)); @@ -62,14 +69,9 @@ exports.installSource = async function installSource({ log, esArgs, }); -}; +} -/** - * - * @param {String} cwd - * @param {ToolingLog} log - */ -async function sourceInfo(cwd, license, log = defaultLog) { +async function sourceInfo(cwd: string, license: string, log: ToolingLog = defaultLog) { if (!fs.existsSync(cwd)) { throw new Error(`${cwd} does not exist`); } diff --git a/packages/kbn-es/src/paths.js b/packages/kbn-es/src/paths.js deleted file mode 100644 index 5c8d3b654ecf9..0000000000000 --- a/packages/kbn-es/src/paths.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const os = require('os'); -const path = require('path'); - -function maybeUseBat(bin) { - return os.platform().startsWith('win') ? `${bin}.bat` : bin; -} - -const tempDir = os.tmpdir(); - -exports.BASE_PATH = path.resolve(tempDir, 'kbn-es'); - -exports.GRADLE_BIN = maybeUseBat('./gradlew'); -exports.ES_BIN = maybeUseBat('bin/elasticsearch'); -exports.ES_CONFIG = 'config/elasticsearch.yml'; - -exports.ES_KEYSTORE_BIN = maybeUseBat('./bin/elasticsearch-keystore'); diff --git a/packages/kbn-es/src/paths.ts b/packages/kbn-es/src/paths.ts new file mode 100644 index 0000000000000..c1b859af4e1f5 --- /dev/null +++ b/packages/kbn-es/src/paths.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 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 Os from 'os'; +import Path from 'path'; + +function maybeUseBat(bin: string) { + return Os.platform().startsWith('win') ? `${bin}.bat` : bin; +} + +const tempDir = Os.tmpdir(); + +export const BASE_PATH = Path.resolve(tempDir, 'kbn-es'); + +export const GRADLE_BIN = maybeUseBat('./gradlew'); +export const ES_BIN = maybeUseBat('bin/elasticsearch'); +export const ES_CONFIG = 'config/elasticsearch.yml'; + +export const ES_KEYSTORE_BIN = maybeUseBat('./bin/elasticsearch-keystore'); diff --git a/packages/kbn-es/src/utils/build_snapshot.js b/packages/kbn-es/src/utils/build_snapshot.ts similarity index 53% rename from packages/kbn-es/src/utils/build_snapshot.js rename to packages/kbn-es/src/utils/build_snapshot.ts index ec26ba69e658b..542e63dcc0748 100644 --- a/packages/kbn-es/src/utils/build_snapshot.js +++ b/packages/kbn-es/src/utils/build_snapshot.ts @@ -6,25 +6,25 @@ * Side Public License, v 1. */ -const execa = require('execa'); -const path = require('path'); -const os = require('os'); -const readline = require('readline'); -const { createCliError } = require('../errors'); -const { findMostRecentlyChanged } = require('../utils'); -const { GRADLE_BIN } = require('../paths'); +import path from 'path'; +import os from 'os'; -const onceEvent = (emitter, event) => new Promise((resolve) => emitter.once(event, resolve)); +import { ToolingLog, withProcRunner } from '@kbn/dev-utils'; + +import { createCliError } from '../errors'; +import { findMostRecentlyChanged } from './find_most_recently_changed'; +import { GRADLE_BIN } from '../paths'; + +interface BuildSnapshotOptions { + license: string; + sourcePath: string; + log: ToolingLog; + platform?: string; +} /** * Creates archive from source * - * @param {Object} options - * @property {('oss'|'basic'|'trial')} options.license - * @property {String} options.sourcePath - * @property {ToolingLog} options.log - * @returns {Object} containing archive and optional plugins - * * Gradle tasks: * $ ./gradlew tasks --all | grep 'distribution.*assemble\s' * :distribution:archives:darwin-tar:assemble @@ -34,39 +34,27 @@ const onceEvent = (emitter, event) => new Promise((resolve) => emitter.once(even * :distribution:archives:oss-linux-tar:assemble * :distribution:archives:oss-windows-zip:assemble */ -exports.buildSnapshot = async ({ license, sourcePath, log, platform = os.platform() }) => { +export async function buildSnapshot({ + license, + sourcePath, + log, + platform = os.platform(), +}: BuildSnapshotOptions) { const { task, ext } = exports.archiveForPlatform(platform, license); const buildArgs = [`:distribution:archives:${task}:assemble`]; log.info('%s %s', GRADLE_BIN, buildArgs.join(' ')); log.debug('cwd:', sourcePath); - const build = execa(GRADLE_BIN, buildArgs, { - cwd: sourcePath, - stdio: ['ignore', 'pipe', 'pipe'], + await withProcRunner(log, async (procs) => { + await procs.run('gradle', { + cmd: GRADLE_BIN, + args: buildArgs, + cwd: sourcePath, + wait: true, + }); }); - const stdout = readline.createInterface({ input: build.stdout }); - const stderr = readline.createInterface({ input: build.stderr }); - - stdout.on('line', (line) => log.debug(line)); - stderr.on('line', (line) => log.error(line)); - - const [exitCode] = await Promise.all([ - Promise.race([ - onceEvent(build, 'exit'), - onceEvent(build, 'error').then((error) => { - throw createCliError(`Error spawning gradle: ${error.message}`); - }), - ]), - onceEvent(stdout, 'close'), - onceEvent(stderr, 'close'), - ]); - - if (exitCode > 0) { - throw createCliError('unable to build ES'); - } - const archivePattern = `distribution/archives/${task}/build/distributions/elasticsearch-*.${ext}`; const esArchivePath = findMostRecentlyChanged(path.resolve(sourcePath, archivePattern)); @@ -75,9 +63,9 @@ exports.buildSnapshot = async ({ license, sourcePath, log, platform = os.platfor } return esArchivePath; -}; +} -exports.archiveForPlatform = (platform, license) => { +export function archiveForPlatform(platform: NodeJS.Platform, license: string) { const taskPrefix = license === 'oss' ? 'oss-' : ''; switch (platform) { @@ -88,6 +76,6 @@ exports.archiveForPlatform = (platform, license) => { case 'linux': return { format: 'tar', ext: 'tar.gz', task: `${taskPrefix}linux-tar`, platform: 'linux' }; default: - throw new Error(`unknown platform: ${platform}`); + throw new Error(`unsupported platform: ${platform}`); } -}; +} diff --git a/packages/kbn-es/src/utils/cache.js b/packages/kbn-es/src/utils/cache.js deleted file mode 100644 index 248faf23bbc46..0000000000000 --- a/packages/kbn-es/src/utils/cache.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const fs = require('fs'); -const path = require('path'); - -exports.readMeta = function readMeta(file) { - try { - const meta = fs.readFileSync(`${file}.meta`, { - encoding: 'utf8', - }); - - return { - exists: fs.existsSync(file), - ...JSON.parse(meta), - }; - } catch (e) { - if (e.code !== 'ENOENT') { - throw e; - } - - return { - exists: false, - }; - } -}; - -exports.writeMeta = function readMeta(file, details = {}) { - const meta = { - ts: new Date(), - ...details, - }; - - fs.mkdirSync(path.dirname(file), { recursive: true }); - fs.writeFileSync(`${file}.meta`, JSON.stringify(meta, null, 2)); -}; diff --git a/packages/kbn-es/src/utils/cache.ts b/packages/kbn-es/src/utils/cache.ts new file mode 100644 index 0000000000000..819119b6ce010 --- /dev/null +++ b/packages/kbn-es/src/utils/cache.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Fs from 'fs'; +import Path from 'path'; + +export const cache = { + readMeta(path: string) { + try { + const meta = Fs.readFileSync(`${path}.meta`, { + encoding: 'utf8', + }); + + return { + ...JSON.parse(meta), + }; + } catch (e) { + if (e.code !== 'ENOENT') { + throw e; + } + + return {}; + } + }, + + writeMeta(path: string, details = {}) { + const meta = { + ts: new Date(), + ...details, + }; + + Fs.mkdirSync(Path.dirname(path), { recursive: true }); + Fs.writeFileSync(`${path}.meta`, JSON.stringify(meta, null, 2)); + }, +}; diff --git a/packages/kbn-es/src/utils/find_most_recently_changed.test.js b/packages/kbn-es/src/utils/find_most_recently_changed.test.ts similarity index 93% rename from packages/kbn-es/src/utils/find_most_recently_changed.test.js rename to packages/kbn-es/src/utils/find_most_recently_changed.test.ts index 8198495e7197f..721e5baba7513 100644 --- a/packages/kbn-es/src/utils/find_most_recently_changed.test.js +++ b/packages/kbn-es/src/utils/find_most_recently_changed.test.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { findMostRecentlyChanged } from './find_most_recently_changed'; + jest.mock('fs', () => ({ statSync: jest.fn().mockImplementation((path) => { if (path.includes('oldest')) { @@ -31,8 +33,6 @@ jest.mock('fs', () => ({ }), })); -const { findMostRecentlyChanged } = require('./find_most_recently_changed'); - test('returns newest file', () => { const file = findMostRecentlyChanged('/data/*.yml'); expect(file).toEqual('/data/newest.yml'); diff --git a/packages/kbn-es/src/utils/find_most_recently_changed.js b/packages/kbn-es/src/utils/find_most_recently_changed.ts similarity index 65% rename from packages/kbn-es/src/utils/find_most_recently_changed.js rename to packages/kbn-es/src/utils/find_most_recently_changed.ts index 16d300f080b8d..29e1edcc5fcc9 100644 --- a/packages/kbn-es/src/utils/find_most_recently_changed.js +++ b/packages/kbn-es/src/utils/find_most_recently_changed.ts @@ -6,25 +6,22 @@ * Side Public License, v 1. */ -const path = require('path'); -const fs = require('fs'); -const glob = require('glob'); +import path from 'path'; +import fs from 'fs'; +import glob from 'glob'; /** * Find the most recently modified file that matches the pattern pattern - * - * @param {String} pattern absolute path with glob expressions - * @return {String} Absolute path */ -exports.findMostRecentlyChanged = function findMostRecentlyChanged(pattern) { +export function findMostRecentlyChanged(pattern: string) { if (!path.isAbsolute(pattern)) { throw new TypeError(`Pattern must be absolute, got ${pattern}`); } - const ctime = (path) => fs.statSync(path).ctime.getTime(); + const ctime = (p: string) => fs.statSync(p).ctime.getTime(); return glob .sync(pattern) .sort((a, b) => ctime(a) - ctime(b)) .pop(); -}; +} diff --git a/packages/kbn-es/src/utils/index.js b/packages/kbn-es/src/utils/index.js deleted file mode 100644 index ed83495e5310a..0000000000000 --- a/packages/kbn-es/src/utils/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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. - */ - -exports.cache = require('./cache'); -exports.log = require('./log').log; -exports.parseEsLog = require('./parse_es_log').parseEsLog; -exports.findMostRecentlyChanged = require('./find_most_recently_changed').findMostRecentlyChanged; -exports.extractConfigFiles = require('./extract_config_files').extractConfigFiles; -exports.NativeRealm = require('./native_realm').NativeRealm; -exports.buildSnapshot = require('./build_snapshot').buildSnapshot; -exports.archiveForPlatform = require('./build_snapshot').archiveForPlatform; diff --git a/packages/kbn-es/src/utils/index.ts b/packages/kbn-es/src/utils/index.ts new file mode 100644 index 0000000000000..ce0a222dafd3b --- /dev/null +++ b/packages/kbn-es/src/utils/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { cache } from './cache'; +export { log } from './log'; +// @ts-expect-error not typed yet +export { parseEsLog } from './parse_es_log'; +export { findMostRecentlyChanged } from './find_most_recently_changed'; +// @ts-expect-error not typed yet +export { extractConfigFiles } from './extract_config_files'; +// @ts-expect-error not typed yet +export { NativeRealm } from './native_realm'; +export { buildSnapshot } from './build_snapshot'; +export { archiveForPlatform } from './build_snapshot'; diff --git a/packages/kbn-es/src/utils/log.js b/packages/kbn-es/src/utils/log.ts similarity index 80% rename from packages/kbn-es/src/utils/log.js rename to packages/kbn-es/src/utils/log.ts index b33ae509c6c45..a0299f885cf6a 100644 --- a/packages/kbn-es/src/utils/log.js +++ b/packages/kbn-es/src/utils/log.ts @@ -6,11 +6,9 @@ * Side Public License, v 1. */ -const { ToolingLog } = require('@kbn/dev-utils'); +import { ToolingLog } from '@kbn/dev-utils'; -const log = new ToolingLog({ +export const log = new ToolingLog({ level: 'verbose', writeTo: process.stdout, }); - -exports.log = log; diff --git a/packages/kbn-eslint-import-resolver-kibana/BUILD.bazel b/packages/kbn-eslint-import-resolver-kibana/BUILD.bazel index a4d96f76053e1..759f4ac706471 100644 --- a/packages/kbn-eslint-import-resolver-kibana/BUILD.bazel +++ b/packages/kbn-eslint-import-resolver-kibana/BUILD.bazel @@ -1,4 +1,5 @@ -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "pkg_npm") PKG_BASE_NAME = "kbn-eslint-import-resolver-kibana" PKG_REQUIRE_NAME = "@kbn/eslint-import-resolver-kibana" diff --git a/packages/kbn-eslint-plugin-eslint/helpers/exports.js b/packages/kbn-eslint-plugin-eslint/helpers/exports.js index b7af8e83d7661..971364633356c 100644 --- a/packages/kbn-eslint-plugin-eslint/helpers/exports.js +++ b/packages/kbn-eslint-plugin-eslint/helpers/exports.js @@ -9,7 +9,7 @@ const Fs = require('fs'); const Path = require('path'); const ts = require('typescript'); -const { REPO_ROOT } = require('@kbn/dev-utils'); +const { REPO_ROOT } = require('@kbn/utils'); const { ExportSet } = require('./export_set'); /** @typedef {import("@typescript-eslint/types").TSESTree.ExportAllDeclaration} ExportAllDeclaration */ diff --git a/packages/kbn-optimizer/BUILD.bazel b/packages/kbn-optimizer/BUILD.bazel index a389086c9ee3c..3bd41249e2d51 100644 --- a/packages/kbn-optimizer/BUILD.bazel +++ b/packages/kbn-optimizer/BUILD.bazel @@ -38,10 +38,12 @@ RUNTIME_DEPS = [ "//packages/kbn-ui-shared-deps-npm", "//packages/kbn-ui-shared-deps-src", "//packages/kbn-utils", + "@npm//@babel/core", "@npm//chalk", "@npm//clean-webpack-plugin", "@npm//compression-webpack-plugin", "@npm//cpy", + "@npm//dedent", "@npm//del", "@npm//execa", "@npm//jest-diff", @@ -64,7 +66,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/kbn-config:npm_module_types", "//packages/kbn-config-schema:npm_module_types", - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-std", "//packages/kbn-ui-shared-deps-npm", "//packages/kbn-ui-shared-deps-src", @@ -79,7 +81,9 @@ TYPES_DEPS = [ "@npm//pirates", "@npm//rxjs", "@npm//zlib", + "@npm//@types/babel__core", "@npm//@types/compression-webpack-plugin", + "@npm//@types/dedent", "@npm//@types/jest", "@npm//@types/json-stable-stringify", "@npm//@types/js-yaml", 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-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts b/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts index f00905f3f4920..c07a9764af76f 100644 --- a/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts +++ b/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts @@ -8,7 +8,8 @@ import Path from 'path'; -import { run, REPO_ROOT } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { OptimizerConfig } from '../optimizer'; import { parseStats, inAnyEntryChunk } from './parse_stats'; diff --git a/packages/kbn-optimizer/src/node/node_auto_tranpilation.ts b/packages/kbn-optimizer/src/node/node_auto_tranpilation.ts index 6f5dabf410ffa..2710ba8a54210 100644 --- a/packages/kbn-optimizer/src/node/node_auto_tranpilation.ts +++ b/packages/kbn-optimizer/src/node/node_auto_tranpilation.ts @@ -39,7 +39,7 @@ import Crypto from 'crypto'; import * as babel from '@babel/core'; import { addHook } from 'pirates'; -import { REPO_ROOT, UPSTREAM_BRANCH } from '@kbn/dev-utils'; +import { REPO_ROOT, UPSTREAM_BRANCH } from '@kbn/utils'; import sourceMapSupport from 'source-map-support'; import { Cache } from './cache'; diff --git a/packages/kbn-optimizer/src/optimizer/get_changes.test.ts b/packages/kbn-optimizer/src/optimizer/get_changes.test.ts index d3cc5cceefddf..d1754248dba17 100644 --- a/packages/kbn-optimizer/src/optimizer/get_changes.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_changes.test.ts @@ -9,7 +9,8 @@ jest.mock('execa'); import { getChanges } from './get_changes'; -import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; const execa: jest.Mock = jest.requireMock('execa'); diff --git a/packages/kbn-optimizer/src/optimizer/get_changes.ts b/packages/kbn-optimizer/src/optimizer/get_changes.ts index c5f8abe99c322..b59f938eb8c37 100644 --- a/packages/kbn-optimizer/src/optimizer/get_changes.ts +++ b/packages/kbn-optimizer/src/optimizer/get_changes.ts @@ -10,7 +10,7 @@ import Path from 'path'; import execa from 'execa'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; export type Changes = Map; diff --git a/packages/kbn-plugin-generator/BUILD.bazel b/packages/kbn-plugin-generator/BUILD.bazel index c935d1763dae8..488f09bdd5d52 100644 --- a/packages/kbn-plugin-generator/BUILD.bazel +++ b/packages/kbn-plugin-generator/BUILD.bazel @@ -51,7 +51,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/kbn-utils", - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "@npm//del", "@npm//execa", "@npm//globby", diff --git a/packages/kbn-plugin-helpers/BUILD.bazel b/packages/kbn-plugin-helpers/BUILD.bazel index d7744aecac26e..47f205f1530b7 100644 --- a/packages/kbn-plugin-helpers/BUILD.bazel +++ b/packages/kbn-plugin-helpers/BUILD.bazel @@ -42,7 +42,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-optimizer", "//packages/kbn-utils", "@npm//del", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index c1d0f69e4ea07..fc92d18698132 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -6639,7 +6639,15 @@ class ToolingLogTextWriter { } if (this.ignoreSources && msg.source && this.ignoreSources.includes(msg.source)) { - return false; + if (msg.type === 'write') { + const txt = (0, _util.format)(msg.args[0], ...msg.args.slice(1)); // Ensure that Elasticsearch deprecation log messages from Kibana aren't ignored + + if (!/elasticsearch\.deprecation/.test(txt)) { + return false; + } + } else { + return false; + } } const prefix = has(MSG_PREFIXES, msg.type) ? MSG_PREFIXES[msg.type] : ''; diff --git a/packages/kbn-rule-data-utils/BUILD.bazel b/packages/kbn-rule-data-utils/BUILD.bazel index 730e907aafc65..d23cf25f181ca 100644 --- a/packages/kbn-rule-data-utils/BUILD.bazel +++ b/packages/kbn-rule-data-utils/BUILD.bazel @@ -34,7 +34,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-es-query", + "//packages/kbn-es-query:npm_module_types", "@npm//@elastic/elasticsearch", "@npm//tslib", "@npm//utility-types", diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index 349719c019c22..fde8deade36b5 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -24,6 +24,7 @@ const VERSION = `${KIBANA_NAMESPACE}.version` as const; // Fields pertaining to the alert const ALERT_ACTION_GROUP = `${ALERT_NAMESPACE}.action_group` as const; +const ALERT_BUILDING_BLOCK_TYPE = `${ALERT_NAMESPACE}.building_block_type` as const; const ALERT_DURATION = `${ALERT_NAMESPACE}.duration.us` as const; const ALERT_END = `${ALERT_NAMESPACE}.end` as const; const ALERT_EVALUATION_THRESHOLD = `${ALERT_NAMESPACE}.evaluation.threshold` as const; @@ -91,6 +92,7 @@ const fields = { TAGS, TIMESTAMP, ALERT_ACTION_GROUP, + ALERT_BUILDING_BLOCK_TYPE, ALERT_DURATION, ALERT_END, ALERT_EVALUATION_THRESHOLD, @@ -141,6 +143,7 @@ const fields = { export { ALERT_ACTION_GROUP, + ALERT_BUILDING_BLOCK_TYPE, ALERT_DURATION, ALERT_END, ALERT_EVALUATION_THRESHOLD, diff --git a/packages/kbn-securitysolution-autocomplete/BUILD.bazel b/packages/kbn-securitysolution-autocomplete/BUILD.bazel index 57ac8c62273e0..50df292b8796e 100644 --- a/packages/kbn-securitysolution-autocomplete/BUILD.bazel +++ b/packages/kbn-securitysolution-autocomplete/BUILD.bazel @@ -45,7 +45,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-es-query", + "//packages/kbn-es-query:npm_module_types", "//packages/kbn-i18n", "//packages/kbn-securitysolution-list-hooks", "//packages/kbn-securitysolution-list-utils", diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.mock.ts index e491b50b0f9c8..176a6357b30e7 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.mock.ts @@ -10,9 +10,11 @@ import { EndpointEntriesArray } from '.'; import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEndpointEntryNestedMock } from '../entry_nested/index.mock'; +import { getEndpointEntryMatchWildcard } from '../entry_match_wildcard/index.mock'; export const getEndpointEntriesArrayMock = (): EndpointEntriesArray => [ getEndpointEntryMatchMock(), getEndpointEntryMatchAnyMock(), getEndpointEntryNestedMock(), + getEndpointEntryMatchWildcard(), ]; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.test.ts index 09f1740567bc1..ca852e15c5c2a 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.test.ts @@ -20,6 +20,7 @@ import { getEndpointEntryNestedMock } from '../entry_nested/index.mock'; import { getEndpointEntriesArrayMock } from './index.mock'; import { getEntryListMock } from '../../entries_list/index.mock'; import { getEntryExistsMock } from '../../entries_exist/index.mock'; +import { getEndpointEntryMatchWildcard } from '../entry_match_wildcard/index.mock'; describe('Endpoint', () => { describe('entriesArray', () => { @@ -99,6 +100,15 @@ describe('Endpoint', () => { expect(message.schema).toEqual(payload); }); + test('it should validate an array with wildcard entry', () => { + const payload = [getEndpointEntryMatchWildcard()]; + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + test('it should validate an array with all types of entries', () => { const payload = getEndpointEntriesArrayMock(); const decoded = endpointEntriesArray.decode(payload); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.ts index 451131dafc459..58b0d80f9c1fa 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.ts @@ -11,9 +11,15 @@ import { Either } from 'fp-ts/lib/Either'; import { endpointEntryMatch } from '../entry_match'; import { endpointEntryMatchAny } from '../entry_match_any'; import { endpointEntryNested } from '../entry_nested'; +import { endpointEntryMatchWildcard } from '../entry_match_wildcard'; export const endpointEntriesArray = t.array( - t.union([endpointEntryMatch, endpointEntryMatchAny, endpointEntryNested]) + t.union([ + endpointEntryMatch, + endpointEntryMatchAny, + endpointEntryMatchWildcard, + endpointEntryNested, + ]) ); export type EndpointEntriesArray = t.TypeOf; diff --git a/packages/kbn-es/src/install/index.js b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entry_match_wildcard/index.mock.ts similarity index 53% rename from packages/kbn-es/src/install/index.js rename to packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entry_match_wildcard/index.mock.ts index 07582f73c663a..e001552277e0c 100644 --- a/packages/kbn-es/src/install/index.js +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entry_match_wildcard/index.mock.ts @@ -6,7 +6,12 @@ * Side Public License, v 1. */ -exports.installArchive = require('./archive').installArchive; -exports.installSnapshot = require('./snapshot').installSnapshot; -exports.downloadSnapshot = require('./snapshot').downloadSnapshot; -exports.installSource = require('./source').installSource; +import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../../constants/index.mock'; +import { EndpointEntryMatchWildcard } from './index'; + +export const getEndpointEntryMatchWildcard = (): EndpointEntryMatchWildcard => ({ + field: FIELD, + operator: OPERATOR, + type: WILDCARD, + value: ENTRY_VALUE, +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_item_schema/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_item_schema/index.mock.ts new file mode 100644 index 0000000000000..03ec225351e6d --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_item_schema/index.mock.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ENTRIES } from '../../constants/index.mock'; +import { ImportExceptionListItemSchema, ImportExceptionListItemSchemaDecoded } from '.'; + +export const getImportExceptionsListItemSchemaMock = ( + itemId = 'item_id_1', + listId = 'detection_list_id' +): ImportExceptionListItemSchema => ({ + description: 'some description', + entries: ENTRIES, + item_id: itemId, + list_id: listId, + name: 'Query with a rule id', + type: 'simple', +}); + +export const getImportExceptionsListItemSchemaDecodedMock = ( + itemId = 'item_id_1', + listId = 'detection_list_id' +): ImportExceptionListItemSchemaDecoded => ({ + ...getImportExceptionsListItemSchemaMock(itemId, listId), + comments: [], + meta: undefined, + namespace_type: 'single', + os_types: [], + tags: [], +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_item_schema/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_item_schema/index.test.ts new file mode 100644 index 0000000000000..d202f65b57ab5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_item_schema/index.test.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +import { importExceptionListItemSchema, ImportExceptionListItemSchema } from '.'; +import { + getImportExceptionsListItemSchemaDecodedMock, + getImportExceptionsListItemSchemaMock, +} from './index.mock'; + +describe('import_list_item_schema', () => { + test('it should validate a typical item request', () => { + const payload = getImportExceptionsListItemSchemaMock(); + const decoded = importExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getImportExceptionsListItemSchemaDecodedMock()); + }); + + test('it should NOT accept an undefined for "item_id"', () => { + const payload: Partial> = + getImportExceptionsListItemSchemaMock(); + delete payload.item_id; + const decoded = importExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "item_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "list_id"', () => { + const payload: Partial> = + getImportExceptionsListItemSchemaMock(); + delete payload.list_id; + const decoded = importExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "list_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "description"', () => { + const payload: Partial> = + getImportExceptionsListItemSchemaMock(); + delete payload.description; + const decoded = importExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "description"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "name"', () => { + const payload: Partial> = + getImportExceptionsListItemSchemaMock(); + delete payload.name; + const decoded = importExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "name"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "type"', () => { + const payload: Partial> = + getImportExceptionsListItemSchemaMock(); + delete payload.type; + const decoded = importExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "entries"', () => { + const payload: Partial> = + getImportExceptionsListItemSchemaMock(); + delete payload.entries; + const decoded = importExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "entries"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should accept any partial fields', () => { + const payload: ImportExceptionListItemSchema = { + ...getImportExceptionsListItemSchemaMock(), + id: '123', + namespace_type: 'single', + comments: [], + os_types: [], + tags: ['123'], + created_at: '2018-08-24T17:49:30.145142000', + created_by: 'elastic', + updated_at: '2018-08-24T17:49:30.145142000', + updated_by: 'elastic', + tie_breaker_id: '123', + _version: '3', + meta: undefined, + }; + + const decoded = importExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ImportExceptionListItemSchema & { + extraKey?: string; + } = getImportExceptionsListItemSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = importExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_item_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_item_schema/index.ts new file mode 100644 index 0000000000000..3da30a21a0115 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_item_schema/index.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { OsTypeArray, osTypeArrayOrUndefined } from '../../common/os_type'; +import { Tags } from '../../common/tags'; +import { NamespaceType } from '../../common/default_namespace'; +import { name } from '../../common/name'; +import { description } from '../../common/description'; +import { namespace_type } from '../../common/namespace_type'; +import { tags } from '../../common/tags'; +import { meta } from '../../common/meta'; +import { list_id } from '../../common/list_id'; +import { item_id } from '../../common/item_id'; +import { id } from '../../common/id'; +import { created_at } from '../../common/created_at'; +import { created_by } from '../../common/created_by'; +import { updated_at } from '../../common/updated_at'; +import { updated_by } from '../../common/updated_by'; +import { _version } from '../../common/underscore_version'; +import { tie_breaker_id } from '../../common/tie_breaker_id'; +import { nonEmptyEntriesArray } from '../../common/non_empty_entries_array'; +import { exceptionListItemType } from '../../common/exception_list_item_type'; +import { ItemId } from '../../common/item_id'; +import { EntriesArray } from '../../common/entries'; +import { CreateCommentsArray } from '../../common/create_comment'; +import { DefaultCreateCommentsArray } from '../../common/default_create_comments_array'; + +/** + * Differences from this and the createExceptionsListItemSchema are + * - item_id is required + * - id is optional (but ignored in the import code - item_id is exclusively used for imports) + * - immutable is optional but if it is any value other than false it will be rejected + * - created_at is optional (but ignored in the import code) + * - updated_at is optional (but ignored in the import code) + * - created_by is optional (but ignored in the import code) + * - updated_by is optional (but ignored in the import code) + */ +export const importExceptionListItemSchema = t.intersection([ + t.exact( + t.type({ + description, + entries: nonEmptyEntriesArray, + item_id, + list_id, + name, + type: exceptionListItemType, + }) + ), + t.exact( + t.partial({ + id, // defaults to undefined if not set during decode + comments: DefaultCreateCommentsArray, // defaults to empty array if not set during decode + created_at, // defaults undefined if not set during decode + updated_at, // defaults undefined if not set during decode + created_by, // defaults undefined if not set during decode + updated_by, // defaults undefined if not set during decode + _version, // defaults to undefined if not set during decode + tie_breaker_id, + meta, // defaults to undefined if not set during decode + namespace_type, // defaults to 'single' if not set during decode + os_types: osTypeArrayOrUndefined, // defaults to empty array if not set during decode + tags, // defaults to empty array if not set during decode + }) + ), +]); + +export type ImportExceptionListItemSchema = t.OutputOf; + +// This type is used after a decode since some things are defaults after a decode. +export type ImportExceptionListItemSchemaDecoded = Omit< + ImportExceptionListItemSchema, + 'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments' +> & { + comments: CreateCommentsArray; + tags: Tags; + item_id: ItemId; + entries: EntriesArray; + namespace_type: NamespaceType; + os_types: OsTypeArray; +}; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_list_schema/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_list_schema/index.mock.ts new file mode 100644 index 0000000000000..dc6aa8644c1f5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_list_schema/index.mock.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ImportExceptionListSchemaDecoded, ImportExceptionsListSchema } from '.'; + +export const getImportExceptionsListSchemaMock = ( + listId = 'detection_list_id' +): ImportExceptionsListSchema => ({ + description: 'some description', + list_id: listId, + name: 'Query with a rule id', + type: 'detection', +}); + +export const getImportExceptionsListSchemaDecodedMock = ( + listId = 'detection_list_id' +): ImportExceptionListSchemaDecoded => ({ + ...getImportExceptionsListSchemaMock(listId), + immutable: false, + meta: undefined, + namespace_type: 'single', + os_types: [], + tags: [], + version: 1, +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_list_schema/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_list_schema/index.test.ts new file mode 100644 index 0000000000000..92a24cd4352f5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_list_schema/index.test.ts @@ -0,0 +1,132 @@ +/* + * Copyright 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +import { importExceptionsListSchema, ImportExceptionsListSchema } from '.'; +import { + getImportExceptionsListSchemaMock, + getImportExceptionsListSchemaDecodedMock, +} from './index.mock'; + +describe('import_list_item_schema', () => { + test('it should validate a typical lists request', () => { + const payload = getImportExceptionsListSchemaMock(); + const decoded = importExceptionsListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getImportExceptionsListSchemaDecodedMock()); + }); + + test('it should NOT accept an undefined for "list_id"', () => { + const payload: Partial> = + getImportExceptionsListSchemaMock(); + delete payload.list_id; + const decoded = importExceptionsListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "list_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "description"', () => { + const payload: Partial> = + getImportExceptionsListSchemaMock(); + delete payload.description; + const decoded = importExceptionsListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "description"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "name"', () => { + const payload: Partial> = + getImportExceptionsListSchemaMock(); + delete payload.name; + const decoded = importExceptionsListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "name"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "type"', () => { + const payload: Partial> = + getImportExceptionsListSchemaMock(); + delete payload.type; + const decoded = importExceptionsListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept value of "true" for "immutable"', () => { + const payload: ImportExceptionsListSchema = { + ...getImportExceptionsListSchemaMock(), + immutable: true, + }; + + const decoded = importExceptionsListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "true" supplied to "immutable"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should accept any partial fields', () => { + const payload: ImportExceptionsListSchema = { + ...getImportExceptionsListSchemaMock(), + namespace_type: 'single', + immutable: false, + os_types: [], + tags: ['123'], + created_at: '2018-08-24T17:49:30.145142000', + created_by: 'elastic', + updated_at: '2018-08-24T17:49:30.145142000', + updated_by: 'elastic', + version: 3, + tie_breaker_id: '123', + _version: '3', + meta: undefined, + }; + + const decoded = importExceptionsListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ImportExceptionsListSchema & { + extraKey?: string; + } = getImportExceptionsListSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = importExceptionsListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_list_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_list_schema/index.ts new file mode 100644 index 0000000000000..610bbae97f579 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_exception_list_schema/index.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { + DefaultVersionNumber, + DefaultVersionNumberDecoded, + OnlyFalseAllowed, +} from '@kbn/securitysolution-io-ts-types'; + +import { exceptionListType } from '../../common/exception_list'; +import { OsTypeArray, osTypeArrayOrUndefined } from '../../common/os_type'; +import { Tags } from '../../common/tags'; +import { ListId } from '../../common/list_id'; +import { NamespaceType } from '../../common/default_namespace'; +import { name } from '../../common/name'; +import { description } from '../../common/description'; +import { namespace_type } from '../../common/namespace_type'; +import { tags } from '../../common/tags'; +import { meta } from '../../common/meta'; +import { list_id } from '../../common/list_id'; +import { id } from '../../common/id'; +import { created_at } from '../../common/created_at'; +import { created_by } from '../../common/created_by'; +import { updated_at } from '../../common/updated_at'; +import { updated_by } from '../../common/updated_by'; +import { _version } from '../../common/underscore_version'; +import { tie_breaker_id } from '../../common/tie_breaker_id'; + +/** + * Differences from this and the createExceptionsSchema are + * - list_id is required + * - id is optional (but ignored in the import code - list_id is exclusively used for imports) + * - immutable is optional but if it is any value other than false it will be rejected + * - created_at is optional (but ignored in the import code) + * - updated_at is optional (but ignored in the import code) + * - created_by is optional (but ignored in the import code) + * - updated_by is optional (but ignored in the import code) + */ +export const importExceptionsListSchema = t.intersection([ + t.exact( + t.type({ + description, + name, + type: exceptionListType, + list_id, + }) + ), + t.exact( + t.partial({ + id, // defaults to undefined if not set during decode + immutable: OnlyFalseAllowed, + meta, // defaults to undefined if not set during decode + namespace_type, // defaults to 'single' if not set during decode + os_types: osTypeArrayOrUndefined, // defaults to empty array if not set during decode + tags, // defaults to empty array if not set during decode + created_at, // defaults "undefined" if not set during decode + updated_at, // defaults "undefined" if not set during decode + created_by, // defaults "undefined" if not set during decode + updated_by, // defaults "undefined" if not set during decode + _version, // defaults to undefined if not set during decode + tie_breaker_id, + version: DefaultVersionNumber, // defaults to numerical 1 if not set during decode + }) + ), +]); + +export type ImportExceptionsListSchema = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type ImportExceptionListSchemaDecoded = Omit< + ImportExceptionsListSchema, + 'tags' | 'list_id' | 'namespace_type' | 'os_types' | 'immutable' +> & { + immutable: false; + tags: Tags; + list_id: ListId; + namespace_type: NamespaceType; + os_types: OsTypeArray; + version: DefaultVersionNumberDecoded; +}; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/index.ts index 3d3c41aed5a72..da8bd7ed8306e 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/index.ts @@ -23,6 +23,8 @@ export * from './find_exception_list_item_schema'; export * from './find_list_item_schema'; export * from './find_list_schema'; export * from './import_list_item_query_schema'; +export * from './import_exception_list_schema'; +export * from './import_exception_item_schema'; export * from './import_list_item_schema'; export * from './patch_list_item_schema'; export * from './patch_list_schema'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/response/import_exceptions_schema/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/response/import_exceptions_schema/index.mock.ts new file mode 100644 index 0000000000000..d4c17c7f9422e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/response/import_exceptions_schema/index.mock.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 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 { ImportExceptionsResponseSchema } from '.'; + +export const getImportExceptionsResponseSchemaMock = ( + success = 0, + lists = 0, + items = 0 +): ImportExceptionsResponseSchema => ({ + errors: [], + success: true, + success_count: success, + success_exception_lists: true, + success_count_exception_lists: lists, + success_exception_list_items: true, + success_count_exception_list_items: items, +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/response/import_exceptions_schema/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/response/import_exceptions_schema/index.test.ts new file mode 100644 index 0000000000000..dc6780d4b1ce2 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/response/import_exceptions_schema/index.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +import { importExceptionsResponseSchema, ImportExceptionsResponseSchema } from '.'; +import { getImportExceptionsResponseSchemaMock } from './index.mock'; + +describe('importExceptionsResponseSchema', () => { + test('it should validate a typical exceptions import response', () => { + const payload = getImportExceptionsResponseSchemaMock(); + const decoded = importExceptionsResponseSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for "errors"', () => { + const payload: Partial> = + getImportExceptionsResponseSchemaMock(); + delete payload.errors; + const decoded = importExceptionsResponseSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "errors"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "success"', () => { + const payload: Partial> = + getImportExceptionsResponseSchemaMock(); + delete payload.success; + const decoded = importExceptionsResponseSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "success"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "success_count"', () => { + const payload: Partial> = + getImportExceptionsResponseSchemaMock(); + delete payload.success_count; + const decoded = importExceptionsResponseSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "success_count"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "success_exception_lists"', () => { + const payload: Partial> = + getImportExceptionsResponseSchemaMock(); + delete payload.success_exception_lists; + const decoded = importExceptionsResponseSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "success_exception_lists"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "success_count_exception_lists"', () => { + const payload: Partial> = + getImportExceptionsResponseSchemaMock(); + delete payload.success_count_exception_lists; + const decoded = importExceptionsResponseSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "success_count_exception_lists"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "success_exception_list_items"', () => { + const payload: Partial> = + getImportExceptionsResponseSchemaMock(); + delete payload.success_exception_list_items; + const decoded = importExceptionsResponseSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "success_exception_list_items"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "success_count_exception_list_items"', () => { + const payload: Partial> = + getImportExceptionsResponseSchemaMock(); + delete payload.success_count_exception_list_items; + const decoded = importExceptionsResponseSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "success_count_exception_list_items"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ImportExceptionsResponseSchema & { + extraKey?: string; + } = getImportExceptionsResponseSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = importExceptionsResponseSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/response/import_exceptions_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/response/import_exceptions_schema/index.ts new file mode 100644 index 0000000000000..f50356d2789f8 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/response/import_exceptions_schema/index.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; + +import { id } from '../../common/id'; +import { list_id } from '../../common/list_id'; +import { item_id } from '../../common/item_id'; + +export const bulkErrorErrorSchema = t.exact( + t.type({ + status_code: t.number, + message: t.string, + }) +); + +export const bulkErrorSchema = t.intersection([ + t.exact( + t.type({ + error: bulkErrorErrorSchema, + }) + ), + t.partial({ + id, + list_id, + item_id, + }), +]); + +export type BulkErrorSchema = t.TypeOf; + +export const importExceptionsResponseSchema = t.exact( + t.type({ + errors: t.array(bulkErrorSchema), + success: t.boolean, + success_count: PositiveInteger, + success_exception_lists: t.boolean, + success_count_exception_lists: PositiveInteger, + success_exception_list_items: t.boolean, + success_count_exception_list_items: PositiveInteger, + }) +); + +export type ImportExceptionsResponseSchema = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/response/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/response/index.ts index dc29bdf16ab48..c37b092eb3477 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/response/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/response/index.ts @@ -14,6 +14,7 @@ export * from './found_exception_list_item_schema'; export * from './found_exception_list_schema'; export * from './found_list_item_schema'; export * from './found_list_schema'; +export * from './import_exceptions_schema'; export * from './list_item_schema'; export * from './list_schema'; export * from './exception_list_summary_schema'; diff --git a/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.test.ts new file mode 100644 index 0000000000000..03ec9df51a318 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { left } from 'fp-ts/lib/Either'; +import { ImportQuerySchema, importQuerySchema } from '.'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('importQuerySchema', () => { + test('it should validate proper schema', () => { + const payload = { + overwrite: true, + }; + const decoded = importQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a non boolean value for "overwrite"', () => { + const payload: Omit & { overwrite: string } = { + overwrite: 'wrong', + }; + const decoded = importQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "wrong" supplied to "overwrite"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT allow an extra key to be sent in', () => { + const payload: ImportQuerySchema & { + extraKey?: string; + } = { + extraKey: 'extra', + overwrite: true, + }; + + const decoded = importQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.ts b/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.ts new file mode 100644 index 0000000000000..95cbf96b2ef8d --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/import_query_schema/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { DefaultStringBooleanFalse } from '../default_string_boolean_false'; + +export const importQuerySchema = t.exact( + t.partial({ + overwrite: DefaultStringBooleanFalse, + }) +); + +export type ImportQuerySchema = t.TypeOf; +export type ImportQuerySchemaDecoded = Omit & { + overwrite: boolean; +}; diff --git a/packages/kbn-securitysolution-io-ts-types/src/index.ts b/packages/kbn-securitysolution-io-ts-types/src/index.ts index b85bff63fe2a7..0bb99e4c766e7 100644 --- a/packages/kbn-securitysolution-io-ts-types/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/index.ts @@ -17,6 +17,7 @@ export * from './default_version_number'; export * from './empty_string_array'; export * from './enumeration'; export * from './iso_date_string'; +export * from './import_query_schema'; export * from './non_empty_array'; export * from './non_empty_or_nullable_string_array'; export * from './non_empty_string_array'; diff --git a/packages/kbn-securitysolution-list-utils/BUILD.bazel b/packages/kbn-securitysolution-list-utils/BUILD.bazel index eb33eb1a03b66..30568ca725041 100644 --- a/packages/kbn-securitysolution-list-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-list-utils/BUILD.bazel @@ -38,11 +38,12 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-es-query", - "//packages/kbn-i18n", + "//packages/kbn-es-query:npm_module_types", + "//packages/kbn-i18n:npm_module_types", "//packages/kbn-securitysolution-io-ts-list-types", "//packages/kbn-securitysolution-list-constants", "//packages/kbn-securitysolution-utils", + "@npm//@elastic/elasticsearch", "@npm//@types/jest", "@npm//@types/lodash", "@npm//@types/node", diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel index f2a7bf25fb407..5dbe22b56c63f 100644 --- a/packages/kbn-storybook/BUILD.bazel +++ b/packages/kbn-storybook/BUILD.bazel @@ -32,6 +32,7 @@ RUNTIME_DEPS = [ "//packages/kbn-dev-utils", "//packages/kbn-ui-shared-deps-npm", "//packages/kbn-ui-shared-deps-src", + "//packages/kbn-utils", "@npm//@storybook/addons", "@npm//@storybook/api", "@npm//@storybook/components", @@ -47,9 +48,10 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-ui-shared-deps-npm", "//packages/kbn-ui-shared-deps-src", + "//packages/kbn-utils", "@npm//@storybook/addons", "@npm//@storybook/api", "@npm//@storybook/components", diff --git a/packages/kbn-storybook/src/lib/constants.ts b/packages/kbn-storybook/src/lib/constants.ts index 722f789fde786..69b05c94ea1b0 100644 --- a/packages/kbn-storybook/src/lib/constants.ts +++ b/packages/kbn-storybook/src/lib/constants.ts @@ -7,7 +7,7 @@ */ import { resolve } from 'path'; -import { REPO_ROOT as KIBANA_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT as KIBANA_ROOT } from '@kbn/utils'; export const REPO_ROOT = KIBANA_ROOT; export const ASSET_DIR = resolve(KIBANA_ROOT, 'built_assets/storybook'); 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-telemetry-tools/BUILD.bazel b/packages/kbn-telemetry-tools/BUILD.bazel index 1183de2586424..d2ea3a704f154 100644 --- a/packages/kbn-telemetry-tools/BUILD.bazel +++ b/packages/kbn-telemetry-tools/BUILD.bazel @@ -38,8 +38,9 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-utility-types", + "@npm//tslib", "@npm//@types/glob", "@npm//@types/jest", "@npm//@types/listr", diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index c42c33483703e..eae0fe2cdf5dc 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -44,11 +44,13 @@ RUNTIME_DEPS = [ "@npm//axios", "@npm//@babel/traverse", "@npm//chance", + "@npm//dedent", "@npm//del", "@npm//enzyme", "@npm//execa", "@npm//exit-hook", "@npm//form-data", + "@npm//getopts", "@npm//globby", "@npm//he", "@npm//history", @@ -59,6 +61,7 @@ RUNTIME_DEPS = [ "@npm//@jest/reporters", "@npm//joi", "@npm//mustache", + "@npm//normalize-path", "@npm//parse-link-header", "@npm//prettier", "@npm//react-dom", @@ -72,12 +75,17 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-i18n-react:npm_module_types", + "//packages/kbn-std", "//packages/kbn-utils", "@npm//@elastic/elasticsearch", + "@npm//axios", + "@npm//elastic-apm-node", "@npm//del", + "@npm//exit-hook", "@npm//form-data", + "@npm//getopts", "@npm//jest", "@npm//jest-cli", "@npm//jest-snapshot", @@ -85,6 +93,7 @@ TYPES_DEPS = [ "@npm//rxjs", "@npm//xmlbuilder", "@npm//@types/chance", + "@npm//@types/dedent", "@npm//@types/enzyme", "@npm//@types/he", "@npm//@types/history", @@ -92,6 +101,7 @@ TYPES_DEPS = [ "@npm//@types/joi", "@npm//@types/lodash", "@npm//@types/mustache", + "@npm//@types/normalize-path", "@npm//@types/node", "@npm//@types/parse-link-header", "@npm//@types/prettier", 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/es/es_test_config.ts b/packages/kbn-test/src/es/es_test_config.ts index db5d705710a75..70000c8068e9f 100644 --- a/packages/kbn-test/src/es/es_test_config.ts +++ b/packages/kbn-test/src/es/es_test_config.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { kibanaPackageJson as pkg } from '@kbn/dev-utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import Url from 'url'; import { adminTestUser } from '../kbn'; diff --git a/packages/kbn-test/src/failed_tests_reporter/buildkite_metadata.ts b/packages/kbn-test/src/failed_tests_reporter/buildkite_metadata.ts new file mode 100644 index 0000000000000..d63f0166390cb --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/buildkite_metadata.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface BuildkiteMetadata { + buildId?: string; + jobId?: string; + url?: string; + jobName?: string; + jobUrl?: string; +} + +export function getBuildkiteMetadata(): BuildkiteMetadata { + // Buildkite steps that use `parallelism` need a numerical suffix added to identify them + // We should also increment the number by one, since it's 0-based + const jobNumberSuffix = process.env.BUILDKITE_PARALLEL_JOB + ? ` #${parseInt(process.env.BUILDKITE_PARALLEL_JOB, 10) + 1}` + : ''; + + const buildUrl = process.env.BUILDKITE_BUILD_URL; + const jobUrl = process.env.BUILDKITE_JOB_ID + ? `${buildUrl}#${process.env.BUILDKITE_JOB_ID}` + : undefined; + + return { + buildId: process.env.BUJILDKITE_BUILD_ID, + jobId: process.env.BUILDKITE_JOB_ID, + url: buildUrl, + jobUrl, + jobName: process.env.BUILDKITE_LABEL + ? `${process.env.BUILDKITE_LABEL}${jobNumberSuffix}` + : undefined, + }; +} diff --git a/packages/kbn-test/src/failed_tests_reporter/github_api.ts b/packages/kbn-test/src/failed_tests_reporter/github_api.ts index adaae11b7aa16..bb7570225a013 100644 --- a/packages/kbn-test/src/failed_tests_reporter/github_api.ts +++ b/packages/kbn-test/src/failed_tests_reporter/github_api.ts @@ -42,6 +42,7 @@ export class GithubApi { private readonly token: string | undefined; private readonly dryRun: boolean; private readonly x: AxiosInstance; + private requestCount: number = 0; /** * Create a GithubApi helper object, if token is undefined requests won't be @@ -68,6 +69,10 @@ export class GithubApi { }); } + getRequestCount() { + return this.requestCount; + } + private failedTestIssuesPageCache: { pages: GithubIssue[][]; nextRequest: RequestOptions | undefined; @@ -191,53 +196,50 @@ export class GithubApi { }> { const executeRequest = !this.dryRun || options.safeForDryRun; const maxAttempts = options.maxAttempts || 5; - const attempt = options.attempt || 1; - - this.log.verbose('Github API', executeRequest ? 'Request' : 'Dry Run', options); - - if (!executeRequest) { - return { - status: 200, - statusText: 'OK', - headers: {}, - data: dryRunResponse, - }; - } - try { - return await this.x.request(options); - } catch (error) { - const unableToReachGithub = isAxiosRequestError(error); - const githubApiFailed = isAxiosResponseError(error) && error.response.status >= 500; - const errorResponseLog = - isAxiosResponseError(error) && - `[${error.config.method} ${error.config.url}] ${error.response.status} ${error.response.statusText} Error`; + let attempt = 0; + while (true) { + attempt += 1; + this.log.verbose('Github API', executeRequest ? 'Request' : 'Dry Run', options); + + if (!executeRequest) { + return { + status: 200, + statusText: 'OK', + headers: {}, + data: dryRunResponse, + }; + } - if ((unableToReachGithub || githubApiFailed) && attempt < maxAttempts) { - const waitMs = 1000 * attempt; + try { + this.requestCount += 1; + return await this.x.request(options); + } catch (error) { + const unableToReachGithub = isAxiosRequestError(error); + const githubApiFailed = isAxiosResponseError(error) && error.response.status >= 500; + const errorResponseLog = + isAxiosResponseError(error) && + `[${error.config.method} ${error.config.url}] ${error.response.status} ${error.response.statusText} Error`; + + if ((unableToReachGithub || githubApiFailed) && attempt < maxAttempts) { + const waitMs = 1000 * attempt; + + if (errorResponseLog) { + this.log.error(`${errorResponseLog}: waiting ${waitMs}ms to retry`); + } else { + this.log.error(`Unable to reach github, waiting ${waitMs}ms to retry`); + } + + await new Promise((resolve) => setTimeout(resolve, waitMs)); + continue; + } if (errorResponseLog) { - this.log.error(`${errorResponseLog}: waiting ${waitMs}ms to retry`); - } else { - this.log.error(`Unable to reach github, waiting ${waitMs}ms to retry`); + throw new Error(`${errorResponseLog}: ${JSON.stringify(error.response.data)}`); } - await new Promise((resolve) => setTimeout(resolve, waitMs)); - return await this.request( - { - ...options, - maxAttempts, - attempt: attempt + 1, - }, - dryRunResponse - ); + throw error; } - - if (errorResponseLog) { - throw new Error(`${errorResponseLog}: ${JSON.stringify(error.response.data)}`); - } - - throw error; } } } diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failures_to_file.ts b/packages/kbn-test/src/failed_tests_reporter/report_failures_to_file.ts index e481da019945c..33dab240ec8b4 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failures_to_file.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failures_to_file.ts @@ -14,6 +14,7 @@ import { ToolingLog } from '@kbn/dev-utils'; import { REPO_ROOT } from '@kbn/utils'; import { escape } from 'he'; +import { BuildkiteMetadata } from './buildkite_metadata'; import { TestFailure } from './get_failures'; const findScreenshots = (dirPath: string, allScreenshots: string[] = []) => { @@ -37,7 +38,11 @@ const findScreenshots = (dirPath: string, allScreenshots: string[] = []) => { return allScreenshots; }; -export function reportFailuresToFile(log: ToolingLog, failures: TestFailure[]) { +export function reportFailuresToFile( + log: ToolingLog, + failures: TestFailure[], + bkMeta: BuildkiteMetadata +) { if (!failures?.length) { return; } @@ -76,28 +81,15 @@ export function reportFailuresToFile(log: ToolingLog, failures: TestFailure[]) { .flat() .join('\n'); - // Buildkite steps that use `parallelism` need a numerical suffix added to identify them - // We should also increment the number by one, since it's 0-based - const jobNumberSuffix = process.env.BUILDKITE_PARALLEL_JOB - ? ` #${parseInt(process.env.BUILDKITE_PARALLEL_JOB, 10) + 1}` - : ''; - - const buildUrl = process.env.BUILDKITE_BUILD_URL || ''; - const jobUrl = process.env.BUILDKITE_JOB_ID - ? `${buildUrl}#${process.env.BUILDKITE_JOB_ID}` - : ''; - const failureJSON = JSON.stringify( { ...failure, hash, - buildId: process.env.BUJILDKITE_BUILD_ID || '', - jobId: process.env.BUILDKITE_JOB_ID || '', - url: buildUrl, - jobUrl, - jobName: process.env.BUILDKITE_LABEL - ? `${process.env.BUILDKITE_LABEL}${jobNumberSuffix}` - : '', + buildId: bkMeta.buildId, + jobId: bkMeta.jobId, + url: bkMeta.url, + jobUrl: bkMeta.jobUrl, + jobName: bkMeta.jobName, }, null, 2 @@ -149,11 +141,11 @@ export function reportFailuresToFile(log: ToolingLog, failures: TestFailure[]) {

${ - jobUrl + bkMeta.jobUrl ? `

Buildkite Job
- ${escape(jobUrl)} + ${escape(bkMeta.jobUrl)}

` : '' diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index 193bc668ce003..6ab135a6afa7e 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -9,7 +9,7 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; -import { run, createFailError, createFlagError } from '@kbn/dev-utils'; +import { run, createFailError, createFlagError, CiStatsReporter } from '@kbn/dev-utils'; import globby from 'globby'; import normalize from 'normalize-path'; @@ -22,6 +22,7 @@ import { addMessagesToReport } from './add_messages_to_report'; import { getReportMessageIter } from './report_metadata'; import { reportFailuresToEs } from './report_failures_to_es'; import { reportFailuresToFile } from './report_failures_to_file'; +import { getBuildkiteMetadata } from './buildkite_metadata'; const DEFAULT_PATTERNS = [Path.resolve(REPO_ROOT, 'target/junit/**/*.xml')]; @@ -71,108 +72,129 @@ export function runFailedTestsReporterCli() { dryRun: !updateGithub, }); - const buildUrl = flags['build-url'] || (updateGithub ? '' : 'http://buildUrl'); - if (typeof buildUrl !== 'string' || !buildUrl) { - throw createFlagError('Missing --build-url or process.env.BUILD_URL'); - } + const bkMeta = getBuildkiteMetadata(); - const patterns = (flags._.length ? flags._ : DEFAULT_PATTERNS).map((p) => - normalize(Path.resolve(p)) - ); - log.info('Searching for reports at', patterns); - const reportPaths = await globby(patterns, { - absolute: true, - }); + try { + const buildUrl = flags['build-url'] || (updateGithub ? '' : 'http://buildUrl'); + if (typeof buildUrl !== 'string' || !buildUrl) { + throw createFlagError('Missing --build-url or process.env.BUILD_URL'); + } - if (!reportPaths.length) { - throw createFailError(`Unable to find any junit reports with patterns [${patterns}]`); - } + const patterns = (flags._.length ? flags._ : DEFAULT_PATTERNS).map((p) => + normalize(Path.resolve(p)) + ); + log.info('Searching for reports at', patterns); + const reportPaths = await globby(patterns, { + absolute: true, + }); - log.info('found', reportPaths.length, 'junit reports', reportPaths); - const newlyCreatedIssues: Array<{ - failure: TestFailure; - newIssue: GithubIssueMini; - }> = []; + if (!reportPaths.length) { + throw createFailError(`Unable to find any junit reports with patterns [${patterns}]`); + } - for (const reportPath of reportPaths) { - const report = await readTestReport(reportPath); - const messages = Array.from(getReportMessageIter(report)); - const failures = await getFailures(report); + log.info('found', reportPaths.length, 'junit reports', reportPaths); + const newlyCreatedIssues: Array<{ + failure: TestFailure; + newIssue: GithubIssueMini; + }> = []; - if (indexInEs) { - await reportFailuresToEs(log, failures); - } + for (const reportPath of reportPaths) { + const report = await readTestReport(reportPath); + const messages = Array.from(getReportMessageIter(report)); + const failures = await getFailures(report); - for (const failure of failures) { - const pushMessage = (msg: string) => { - messages.push({ - classname: failure.classname, - name: failure.name, - message: msg, - }); - }; - - if (failure.likelyIrrelevant) { - pushMessage( - 'Failure is likely irrelevant' + - (updateGithub ? ', so an issue was not created or updated' : '') - ); - continue; + if (indexInEs) { + await reportFailuresToEs(log, failures); } - let existingIssue: GithubIssueMini | undefined = await githubApi.findFailedTestIssue( - (i) => - getIssueMetadata(i.body, 'test.class') === failure.classname && - getIssueMetadata(i.body, 'test.name') === failure.name - ); + for (const failure of failures) { + const pushMessage = (msg: string) => { + messages.push({ + classname: failure.classname, + name: failure.name, + message: msg, + }); + }; + + if (failure.likelyIrrelevant) { + pushMessage( + 'Failure is likely irrelevant' + + (updateGithub ? ', so an issue was not created or updated' : '') + ); + continue; + } - if (!existingIssue) { - const newlyCreated = newlyCreatedIssues.find( - ({ failure: f }) => f.classname === failure.classname && f.name === failure.name - ); + let existingIssue: GithubIssueMini | undefined = updateGithub + ? await githubApi.findFailedTestIssue( + (i) => + getIssueMetadata(i.body, 'test.class') === failure.classname && + getIssueMetadata(i.body, 'test.name') === failure.name + ) + : undefined; + + if (!existingIssue) { + const newlyCreated = newlyCreatedIssues.find( + ({ failure: f }) => f.classname === failure.classname && f.name === failure.name + ); + + if (newlyCreated) { + existingIssue = newlyCreated.newIssue; + } + } - if (newlyCreated) { - existingIssue = newlyCreated.newIssue; + if (existingIssue) { + const newFailureCount = await updateFailureIssue( + buildUrl, + existingIssue, + githubApi, + branch + ); + const url = existingIssue.html_url; + failure.githubIssue = url; + failure.failureCount = updateGithub ? newFailureCount : newFailureCount - 1; + pushMessage( + `Test has failed ${newFailureCount - 1} times on tracked branches: ${url}` + ); + if (updateGithub) { + pushMessage(`Updated existing issue: ${url} (fail count: ${newFailureCount})`); + } + continue; } - } - if (existingIssue) { - const newFailureCount = await updateFailureIssue( - buildUrl, - existingIssue, - githubApi, - branch - ); - const url = existingIssue.html_url; - failure.githubIssue = url; - failure.failureCount = updateGithub ? newFailureCount : newFailureCount - 1; - pushMessage(`Test has failed ${newFailureCount - 1} times on tracked branches: ${url}`); + const newIssue = await createFailureIssue(buildUrl, failure, githubApi, branch); + pushMessage('Test has not failed recently on tracked branches'); if (updateGithub) { - pushMessage(`Updated existing issue: ${url} (fail count: ${newFailureCount})`); + pushMessage(`Created new issue: ${newIssue.html_url}`); + failure.githubIssue = newIssue.html_url; } - continue; - } - - const newIssue = await createFailureIssue(buildUrl, failure, githubApi, branch); - pushMessage('Test has not failed recently on tracked branches'); - if (updateGithub) { - pushMessage(`Created new issue: ${newIssue.html_url}`); - failure.githubIssue = newIssue.html_url; + newlyCreatedIssues.push({ failure, newIssue }); + failure.failureCount = updateGithub ? 1 : 0; } - newlyCreatedIssues.push({ failure, newIssue }); - failure.failureCount = updateGithub ? 1 : 0; - } - // mutates report to include messages and writes updated report to disk - await addMessagesToReport({ - report, - messages, - log, - reportPath, - dryRun: !flags['report-update'], - }); + // mutates report to include messages and writes updated report to disk + await addMessagesToReport({ + report, + messages, + log, + reportPath, + dryRun: !flags['report-update'], + }); - reportFailuresToFile(log, failures); + reportFailuresToFile(log, failures, bkMeta); + } + } finally { + await CiStatsReporter.fromEnv(log).metrics([ + { + group: 'github api request count', + id: `failed test reporter`, + value: githubApi.getRequestCount(), + meta: Object.fromEntries( + Object.entries(bkMeta).map( + ([k, v]) => [`buildkite${k[0].toUpperCase()}${k.slice(1)}`, v] as const + ) + ), + }, + ]); } }, { diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js index 3446c5be5d4a7..4f798839d7231 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js @@ -8,7 +8,7 @@ import Path from 'path'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; /** * Traverse the suites configured and ensure that each suite has no more than one ciGroup assigned diff --git a/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts b/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts index e87f316a100a7..53ce4c74c1388 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts @@ -14,7 +14,7 @@ jest.mock('@kbn/utils', () => { return { REPO_ROOT: '/dev/null/root' }; }); -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Lifecycle } from './lifecycle'; import { SuiteTracker } from './suite_tracker'; import { Suite } from '../fake_mocha_types'; diff --git a/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js b/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js index 03947f7e267ba..63d2b56350ba1 100644 --- a/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js +++ b/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js @@ -9,7 +9,7 @@ const Fs = require('fs'); const Path = require('path'); -const { REPO_ROOT: REPO_ROOT_FOLLOWING_SYMLINKS } = require('@kbn/dev-utils'); +const { REPO_ROOT: REPO_ROOT_FOLLOWING_SYMLINKS } = require('@kbn/utils'); const BASE_REPO_ROOT = Path.resolve( Fs.realpathSync(Path.resolve(REPO_ROOT_FOLLOWING_SYMLINKS, 'package.json')), '..' diff --git a/packages/kbn-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts index 6dde114d3a98e..6a6c7edb98c79 100644 --- a/packages/kbn-test/src/functional_tests/tasks.ts +++ b/packages/kbn-test/src/functional_tests/tasks.ts @@ -9,7 +9,8 @@ import { relative } from 'path'; import * as Rx from 'rxjs'; import { startWith, switchMap, take } from 'rxjs/operators'; -import { withProcRunner, ToolingLog, REPO_ROOT, getTimeReporter } from '@kbn/dev-utils'; +import { withProcRunner, ToolingLog, getTimeReporter } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import dedent from 'dedent'; import { 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-test/src/kbn_client/kbn_client_import_export.ts b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts index 4adae7d1cd031..6da34228bbe7f 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts @@ -12,7 +12,8 @@ import { existsSync } from 'fs'; import Path from 'path'; import FormData from 'form-data'; -import { ToolingLog, isAxiosResponseError, createFailError, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog, isAxiosResponseError, createFailError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { KbnClientRequester, uriencode, ReqOptions } from './kbn_client_requester'; import { KbnClientSavedObjects } from './kbn_client_saved_objects'; 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..f15fd99a02a87 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -13,97 +13,13 @@ import { RequiredKeys, ValuesType } from 'utility-types'; // import { unconst } from '../unconst'; import { NormalizePath } from './utils'; -type PathsOfRoute = - | TRoute['path'] - | (TRoute extends { children: Route[] } - ? AppendPath | PathsOf - : never); - -export type PathsOf = TRoutes extends [] - ? never - : TRoutes extends [Route] - ? PathsOfRoute - : TRoutes extends [Route, Route] - ? PathsOfRoute | PathsOfRoute - : TRoutes extends [Route, Route, Route] - ? PathsOfRoute | PathsOfRoute | PathsOfRoute - : TRoutes extends [Route, Route, Route, Route] - ? - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - : TRoutes extends [Route, Route, Route, Route, Route] - ? - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - : TRoutes extends [Route, Route, Route, Route, Route, Route] - ? - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - : TRoutes extends [Route, Route, Route, Route, Route, Route, Route] - ? - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route] - ? - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route] - ? - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route, Route] - ? - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route, Route, Route] - ? - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - | PathsOfRoute - : string; +// type PathsOfRoute = +// | TRoute['path'] +// | (TRoute extends { children: Route[] } +// ? AppendPath | PathsOf +// : never); + +export type PathsOf = keyof MapRoutes & string; export interface RouteMatch { route: TRoute; @@ -115,7 +31,7 @@ export interface RouteMatch { params: t.Type; } ? t.TypeOf - : AnyObj; + : {}; }; } @@ -160,11 +76,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 +106,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 +192,7 @@ type MapRoute = MaybeUnion< >; } > - : AnyObj + : {} >; type MapRoutes = TRoutes extends [Route] @@ -343,12 +257,20 @@ type MapRoutes = TRoutes extends [Route] MapRoute & MapRoute & MapRoute - : AnyObj; + : {}; // const element = null as any; // const routes = unconst([ // { +// path: '/link-to/transaction/{transactionId}', +// element, +// }, +// { +// path: '/link-to/trace/{traceId}', +// element, +// }, +// { // path: '/', // element, // children: [ @@ -395,6 +317,10 @@ type MapRoutes = TRoutes extends [Route] // element, // }, // { +// path: '/settings/agent-keys', +// element, +// }, +// { // path: '/settings', // element, // }, @@ -432,11 +358,19 @@ type MapRoutes = TRoutes extends [Route] // element, // }, // { +// path: '/services/:serviceName/transactions/view', +// element, +// }, +// { +// path: '/services/:serviceName/dependencies', +// element, +// }, +// { // path: '/services/:serviceName/errors', // element, // children: [ // { -// path: '/:groupId', +// path: '/services/:serviceName/errors/:groupId', // element, // params: t.type({ // path: t.type({ @@ -445,7 +379,7 @@ type MapRoutes = TRoutes extends [Route] // }), // }, // { -// path: '/services/:serviceName', +// path: '/services/:serviceName/errors', // element, // params: t.partial({ // query: t.partial({ @@ -459,15 +393,33 @@ type MapRoutes = TRoutes extends [Route] // ], // }, // { -// path: '/services/:serviceName/foo', +// path: '/services/:serviceName/metrics', +// element, +// }, +// { +// path: '/services/:serviceName/nodes', +// element, +// children: [ +// { +// path: '/services/{serviceName}/nodes/{serviceNodeName}/metrics', +// element, +// }, +// { +// path: '/services/:serviceName/nodes', +// element, +// }, +// ], +// }, +// { +// path: '/services/:serviceName/service-map', // element, // }, // { -// path: '/services/:serviceName/bar', +// path: '/services/:serviceName/logs', // element, // }, // { -// path: '/services/:serviceName/baz', +// path: '/services/:serviceName/profiling', // element, // }, // { @@ -499,6 +451,24 @@ type MapRoutes = TRoutes extends [Route] // element, // }, // { +// path: '/backends', +// element, +// children: [ +// { +// path: '/backends/{backendName}/overview', +// element, +// }, +// { +// path: '/backends/overview', +// element, +// }, +// { +// path: '/backends', +// element, +// }, +// ], +// }, +// { // path: '/', // element, // }, @@ -511,10 +481,11 @@ type MapRoutes = TRoutes extends [Route] // type Routes = typeof routes; // type Mapped = keyof MapRoutes; +// type Paths = PathsOf; // type Bar = ValuesType>['route']['path']; // type Foo = OutputOf; -// type Baz = OutputOf; +// // type Baz = OutputOf; // const { path }: Foo = {} as any; @@ -522,4 +493,4 @@ type MapRoutes = TRoutes extends [Route] // return {} as any; // } -// const params = _useApmParams('/*'); +// // const params = _useApmParams('/services/:serviceName/nodes/*'); diff --git a/src/cli/serve/integration_tests/invalid_config.test.ts b/src/cli/serve/integration_tests/invalid_config.test.ts index 2de902582a548..ca051f37a816e 100644 --- a/src/cli/serve/integration_tests/invalid_config.test.ts +++ b/src/cli/serve/integration_tests/invalid_config.test.ts @@ -8,7 +8,7 @@ import { spawnSync } from 'child_process'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; const INVALID_CONFIG_PATH = require.resolve('./__fixtures__/invalid_config.yml'); diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 24c085ef64de3..fed3aa3093166 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`, @@ -485,6 +486,7 @@ export class DocLinksService { hdfsRepo: `${PLUGIN_DOCS}repository-hdfs.html`, s3Repo: `${PLUGIN_DOCS}repository-s3.html`, snapshotRestoreRepos: `${PLUGIN_DOCS}repository.html`, + mapperSize: `${PLUGIN_DOCS}mapper-size-usage.html`, }, snapshotRestore: { guide: `${ELASTICSEARCH_DOCS}snapshot-restore.html`, @@ -671,6 +673,7 @@ export interface DocLinksStart { readonly usersAccess: string; }; readonly workplaceSearch: { + readonly apiKeys: string; readonly box: string; readonly confluenceCloud: string; readonly confluenceServer: string; @@ -872,7 +875,14 @@ export interface DocLinksStart { }>; readonly watcher: Record; readonly ccs: Record; - readonly plugins: Record; + readonly plugins: { + azureRepo: string; + gcsRepo: string; + hdfsRepo: string; + s3Repo: string; + snapshotRestoreRepos: string; + mapperSize: string; + }; readonly snapshotRestore: Record; readonly ingest: Record; readonly fleet: Readonly<{ diff --git a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap index e93ef34c38025..1c394112a404c 100644 --- a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap +++ b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap @@ -98,6 +98,7 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiDataGridToolbar.fullScreenButtonActive": "Exit full screen", "euiDatePopoverButton.invalidTitle": [Function], "euiDatePopoverButton.outdatedTitle": [Function], + "euiErrorBoundary.error": "Error", "euiFieldPassword.maskPassword": "Mask password", "euiFieldPassword.showPassword": "Show password as plain text. Note: this will visually expose your password on the screen.", "euiFilePicker.clearSelectedFiles": "Clear selected files", @@ -218,7 +219,7 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiStyleSelector.labelExpanded": "Expanded density", "euiStyleSelector.labelNormal": "Normal density", "euiSuperDatePicker.showDatesButtonLabel": "Show dates", - "euiSuperSelect.screenReaderAnnouncement": [Function], + "euiSuperSelect.screenReaderAnnouncement": "You are in a form selector and must select a single option. Use the up and down keys to navigate or escape to close.", "euiSuperSelectControl.selectAnOption": [Function], "euiSuperUpdateButton.cannotUpdateTooltip": "Cannot update", "euiSuperUpdateButton.clickToApplyTooltip": "Click to apply", diff --git a/src/core/public/i18n/i18n_eui_mapping.tsx b/src/core/public/i18n/i18n_eui_mapping.tsx index 7c4d39fa2b11a..e3357d138e794 100644 --- a/src/core/public/i18n/i18n_eui_mapping.tsx +++ b/src/core/public/i18n/i18n_eui_mapping.tsx @@ -663,6 +663,10 @@ export const getEuiContextMapping = (): EuiTokensObject => { defaultMessage: '+ {messagesLength} more', values: { messagesLength }, }), + 'euiErrorBoundary.error': i18n.translate('core.euiErrorBoundary.error', { + defaultMessage: 'Error', + description: 'Error boundary for uncaught exceptions when rendering part of the application', + }), 'euiNotificationEventMessages.accordionAriaLabelButtonText': ({ messagesLength, eventName, @@ -1046,12 +1050,13 @@ export const getEuiContextMapping = (): EuiTokensObject => { description: 'Displayed in a button that shows date picker', } ), - 'euiSuperSelect.screenReaderAnnouncement': ({ optionsCount }: EuiValues) => - i18n.translate('core.euiSuperSelect.screenReaderAnnouncement', { + 'euiSuperSelect.screenReaderAnnouncement': i18n.translate( + 'core.euiSuperSelect.screenReaderAnnouncement', + { defaultMessage: - 'You are in a form selector of {optionsCount} items and must select a single option. Use the up and down keys to navigate or escape to close.', - values: { optionsCount }, - }), + 'You are in a form selector and must select a single option. Use the up and down keys to navigate or escape to close.', + } + ), 'euiSuperSelectControl.selectAnOption': ({ selectedValue }: EuiValues) => i18n.translate('core.euiSuperSelectControl.selectAnOption', { defaultMessage: 'Select an option: {selectedValue}, is selected', diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 30225acb3dd8d..63e0898b5fb90 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; @@ -772,7 +773,14 @@ export interface DocLinksStart { }>; readonly watcher: Record; readonly ccs: Record; - readonly plugins: Record; + readonly plugins: { + azureRepo: string; + gcsRepo: string; + hdfsRepo: string; + s3Repo: string; + snapshotRestoreRepos: string; + mapperSize: string; + }; readonly snapshotRestore: Record; readonly ingest: Record; readonly fleet: Readonly<{ diff --git a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts index 2e80fbb9d20c0..c1f6ffb5add77 100644 --- a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts +++ b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts @@ -7,7 +7,7 @@ */ import supertest from 'supertest'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { HttpService, InternalHttpServicePreboot, InternalHttpServiceSetup } from '../../http'; import { contextServiceMock } from '../../context/context_service.mock'; import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock'; diff --git a/src/core/server/core_context.mock.ts b/src/core/server/core_context.mock.ts index ddb87d31383c8..4d7b4e1ba5548 100644 --- a/src/core/server/core_context.mock.ts +++ b/src/core/server/core_context.mock.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { CoreContext } from './core_context'; import { Env, IConfigService } from './config'; diff --git a/src/core/server/elasticsearch/client/configure_client.test.ts b/src/core/server/elasticsearch/client/configure_client.test.ts index 7988e81045d17..f252993415afa 100644 --- a/src/core/server/elasticsearch/client/configure_client.test.ts +++ b/src/core/server/elasticsearch/client/configure_client.test.ts @@ -6,21 +6,16 @@ * Side Public License, v 1. */ -import { Buffer } from 'buffer'; -import { Readable } from 'stream'; - -import { errors } from '@elastic/elasticsearch'; -import type { - TransportRequestOptions, - TransportRequestParams, - DiagnosticResult, - RequestBody, -} from '@elastic/elasticsearch'; +jest.mock('./log_query_and_deprecation.ts', () => ({ + __esModule: true, + instrumentEsQueryAndDeprecationLogger: jest.fn(), +})); import { parseClientOptionsMock, ClientMock } from './configure_client.test.mocks'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import type { ElasticsearchClientConfig } from './client_config'; import { configureClient } from './configure_client'; +import { instrumentEsQueryAndDeprecationLogger } from './log_query_and_deprecation'; const createFakeConfig = ( parts: Partial = {} @@ -36,40 +31,9 @@ const createFakeClient = () => { const client = new actualEs.Client({ nodes: ['http://localhost'], // Enforcing `nodes` because it's mandatory }); - jest.spyOn(client.diagnostic, 'on'); return client; }; -const createApiResponse = ({ - body, - statusCode = 200, - headers = {}, - warnings = [], - params, - requestOptions = {}, -}: { - body: T; - statusCode?: number; - headers?: Record; - warnings?: string[]; - params?: TransportRequestParams; - requestOptions?: TransportRequestOptions; -}): DiagnosticResult => { - return { - body, - statusCode, - headers, - warnings, - meta: { - body, - request: { - params: params!, - options: requestOptions, - } as any, - } as any, - }; -}; - describe('configureClient', () => { let logger: ReturnType; let config: ElasticsearchClientConfig; @@ -84,6 +48,7 @@ describe('configureClient', () => { afterEach(() => { parseClientOptionsMock.mockReset(); ClientMock.mockReset(); + jest.clearAllMocks(); }); it('calls `parseClientOptions` with the correct parameters', () => { @@ -113,366 +78,14 @@ describe('configureClient', () => { expect(client).toBe(ClientMock.mock.results[0].value); }); - it('listens to client on `response` events', () => { + it('calls instrumentEsQueryAndDeprecationLogger', () => { const client = configureClient(config, { logger, type: 'test', scoped: false }); - expect(client.diagnostic.on).toHaveBeenCalledTimes(1); - expect(client.diagnostic.on).toHaveBeenCalledWith('response', expect.any(Function)); - }); - - describe('Client logging', () => { - function createResponseWithBody(body?: RequestBody) { - return createApiResponse({ - body: {}, - statusCode: 200, - params: { - method: 'GET', - path: '/foo', - querystring: { hello: 'dolly' }, - body, - }, - }); - } - - describe('logs each query', () => { - it('creates a query logger context based on the `type` parameter', () => { - configureClient(createFakeConfig(), { logger, type: 'test123' }); - expect(logger.get).toHaveBeenCalledWith('query', 'test123'); - }); - - it('when request body is an object', () => { - const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); - - const response = createResponseWithBody({ - seq_no_primary_term: true, - query: { - term: { user: 'kimchy' }, - }, - }); - - client.diagnostic.emit('response', null, response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "200 - GET /foo?hello=dolly - {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}", - undefined, - ], - ] - `); - }); - - it('when request body is a string', () => { - const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); - - const response = createResponseWithBody( - JSON.stringify({ - seq_no_primary_term: true, - query: { - term: { user: 'kimchy' }, - }, - }) - ); - - client.diagnostic.emit('response', null, response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "200 - GET /foo?hello=dolly - {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}", - undefined, - ], - ] - `); - }); - - it('when request body is a buffer', () => { - const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); - - const response = createResponseWithBody( - Buffer.from( - JSON.stringify({ - seq_no_primary_term: true, - query: { - term: { user: 'kimchy' }, - }, - }) - ) - ); - - client.diagnostic.emit('response', null, response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "200 - GET /foo?hello=dolly - [buffer]", - undefined, - ], - ] - `); - }); - - it('when request body is a readable stream', () => { - const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); - - const response = createResponseWithBody( - Readable.from( - JSON.stringify({ - seq_no_primary_term: true, - query: { - term: { user: 'kimchy' }, - }, - }) - ) - ); - - client.diagnostic.emit('response', null, response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "200 - GET /foo?hello=dolly - [stream]", - undefined, - ], - ] - `); - }); - - it('when request body is not defined', () => { - const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); - - const response = createResponseWithBody(); - - client.diagnostic.emit('response', null, response); - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "200 - GET /foo?hello=dolly", - undefined, - ], - ] - `); - }); - - it('properly encode queries', () => { - const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); - - const response = createApiResponse({ - body: {}, - statusCode: 200, - params: { - method: 'GET', - path: '/foo', - querystring: { city: 'Münich' }, - }, - }); - - client.diagnostic.emit('response', null, response); - - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "200 - GET /foo?city=M%C3%BCnich", - undefined, - ], - ] - `); - }); - - it('logs queries even in case of errors', () => { - const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); - - const response = createApiResponse({ - statusCode: 500, - body: { - error: { - type: 'internal server error', - }, - }, - params: { - method: 'GET', - path: '/foo', - querystring: { hello: 'dolly' }, - body: { - seq_no_primary_term: true, - query: { - term: { user: 'kimchy' }, - }, - }, - }, - }); - client.diagnostic.emit('response', new errors.ResponseError(response), response); - - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "500 - GET /foo?hello=dolly - {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}} [internal server error]: internal server error", - undefined, - ], - ] - `); - }); - - it('logs debug when the client emits an @elastic/elasticsearch error', () => { - const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); - - const response = createApiResponse({ body: {} }); - client.diagnostic.emit('response', new errors.TimeoutError('message', response), response); - - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "[TimeoutError]: message", - undefined, - ], - ] - `); - }); - - it('logs debug when the client emits an ResponseError returned by elasticsearch', () => { - const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); - - const response = createApiResponse({ - statusCode: 400, - headers: {}, - params: { - method: 'GET', - path: '/_path', - querystring: { hello: 'dolly' }, - }, - body: { - error: { - type: 'illegal_argument_exception', - reason: 'request [/_path] contains unrecognized parameter: [name]', - }, - }, - }); - client.diagnostic.emit('response', new errors.ResponseError(response), response); - - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "400 - GET /_path?hello=dolly [illegal_argument_exception]: request [/_path] contains unrecognized parameter: [name]", - undefined, - ], - ] - `); - }); - - it('logs default error info when the error response body is empty', () => { - const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); - - let response: DiagnosticResult = createApiResponse({ - statusCode: 400, - headers: {}, - params: { - method: 'GET', - path: '/_path', - }, - body: { - error: {}, - }, - }); - client.diagnostic.emit('response', new errors.ResponseError(response), response); - - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "400 - GET /_path [undefined]: {\\"error\\":{}}", - undefined, - ], - ] - `); - - logger.debug.mockClear(); - - response = createApiResponse({ - statusCode: 400, - headers: {}, - params: { - method: 'GET', - path: '/_path', - }, - body: undefined, - }); - client.diagnostic.emit('response', new errors.ResponseError(response), response); - - expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "400 - GET /_path [undefined]: Response Error", - undefined, - ], - ] - `); - }); - - it('adds meta information to logs', () => { - const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); - - let response = createApiResponse({ - statusCode: 400, - headers: {}, - params: { - method: 'GET', - path: '/_path', - }, - requestOptions: { - opaqueId: 'opaque-id', - }, - body: { - error: {}, - }, - }); - client.diagnostic.emit('response', null, response); - - expect(loggingSystemMock.collect(logger).debug[0][1]).toMatchInlineSnapshot(` - Object { - "http": Object { - "request": Object { - "id": "opaque-id", - }, - }, - } - `); - - logger.debug.mockClear(); - - response = createApiResponse({ - statusCode: 400, - headers: {}, - params: { - method: 'GET', - path: '/_path', - }, - requestOptions: { - opaqueId: 'opaque-id', - }, - body: {} as any, - }); - client.diagnostic.emit('response', new errors.ResponseError(response), response); - - expect(loggingSystemMock.collect(logger).debug[0][1]).toMatchInlineSnapshot(` - Object { - "http": Object { - "request": Object { - "id": "opaque-id", - }, - }, - } - `); - }); + expect(instrumentEsQueryAndDeprecationLogger).toHaveBeenCalledTimes(1); + expect(instrumentEsQueryAndDeprecationLogger).toHaveBeenCalledWith({ + logger, + client, + type: 'test', }); }); }); diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts index fc8a06660cc5e..e48a36fa4fe58 100644 --- a/src/core/server/elasticsearch/client/configure_client.ts +++ b/src/core/server/elasticsearch/client/configure_client.ts @@ -6,21 +6,17 @@ * Side Public License, v 1. */ -import { Buffer } from 'buffer'; -import { stringify } from 'querystring'; -import { Client, errors, Transport, HttpConnection } from '@elastic/elasticsearch'; +import { Client, Transport, HttpConnection } from '@elastic/elasticsearch'; import type { KibanaClient } from '@elastic/elasticsearch/lib/api/kibana'; import type { TransportRequestParams, TransportRequestOptions, TransportResult, - DiagnosticResult, - RequestBody, } from '@elastic/elasticsearch'; import { Logger } from '../../logging'; import { parseClientOptions, ElasticsearchClientConfig } from './client_config'; -import type { ElasticsearchErrorDetails } from './types'; +import { instrumentEsQueryAndDeprecationLogger } from './log_query_and_deprecation'; const noop = () => undefined; @@ -61,91 +57,8 @@ export const configureClient = ( Transport: KibanaTransport, Connection: HttpConnection, }); - addLogging(client, logger.get('query', type)); - return client as KibanaClient; -}; - -const convertQueryString = (qs: string | Record | undefined): string => { - if (qs === undefined || typeof qs === 'string') { - return qs ?? ''; - } - return stringify(qs); -}; - -function ensureString(body: RequestBody): string { - if (typeof body === 'string') return body; - if (Buffer.isBuffer(body)) return '[buffer]'; - if ('readable' in body && body.readable && typeof body._read === 'function') return '[stream]'; - return JSON.stringify(body); -} - -/** - * Returns a debug message from an Elasticsearch error in the following format: - * [error type] error reason - */ -export function getErrorMessage(error: errors.ElasticsearchClientError): string { - if (error instanceof errors.ResponseError) { - const errorBody = error.meta.body as ElasticsearchErrorDetails; - return `[${errorBody?.error?.type}]: ${errorBody?.error?.reason ?? error.message}`; - } - return `[${error.name}]: ${error.message}`; -} + instrumentEsQueryAndDeprecationLogger({ logger, client, type }); -/** - * returns a string in format: - * - * status code - * method URL - * request body - * - * so it could be copy-pasted into the Dev console - */ -function getResponseMessage(event: DiagnosticResult): string { - const errorMeta = getRequestDebugMeta(event); - const body = errorMeta.body ? `\n${errorMeta.body}` : ''; - return `${errorMeta.statusCode}\n${errorMeta.method} ${errorMeta.url}${body}`; -} - -/** - * Returns stringified debug information from an Elasticsearch request event - * useful for logging in case of an unexpected failure. - */ -export function getRequestDebugMeta(event: DiagnosticResult): { - url: string; - body: string; - statusCode: number | null; - method: string; -} { - const params = event.meta.request.params; - // definition is wrong, `params.querystring` can be either a string or an object - const querystring = convertQueryString(params.querystring); - return { - url: `${params.path}${querystring ? `?${querystring}` : ''}`, - body: params.body ? `${ensureString(params.body)}` : '', - method: params.method, - statusCode: event.statusCode!, - }; -} - -const addLogging = (client: Client, logger: Logger) => { - client.diagnostic.on('response', (error, event) => { - if (event) { - const opaqueId = event.meta.request.options.opaqueId; - const meta = opaqueId - ? { - http: { request: { id: event.meta.request.options.opaqueId } }, - } - : undefined; // do not clutter logs if opaqueId is not present - if (error) { - if (error instanceof errors.ResponseError) { - logger.debug(`${getResponseMessage(event)} ${getErrorMessage(error)}`, meta); - } else { - logger.debug(getErrorMessage(error), meta); - } - } else { - logger.debug(getResponseMessage(event), meta); - } - } - }); + return client as KibanaClient; }; diff --git a/src/core/server/elasticsearch/client/index.ts b/src/core/server/elasticsearch/client/index.ts index 2cf5a0229a489..123c498f1ee21 100644 --- a/src/core/server/elasticsearch/client/index.ts +++ b/src/core/server/elasticsearch/client/index.ts @@ -21,5 +21,6 @@ export type { IScopedClusterClient } from './scoped_cluster_client'; export type { ElasticsearchClientConfig } from './client_config'; export { ClusterClient } from './cluster_client'; export type { IClusterClient, ICustomClusterClient } from './cluster_client'; -export { configureClient, getRequestDebugMeta, getErrorMessage } from './configure_client'; +export { configureClient } from './configure_client'; +export { getRequestDebugMeta, getErrorMessage } from './log_query_and_deprecation'; export { retryCallCluster, migrationRetryCallCluster } from './retry_call_cluster'; diff --git a/src/core/server/elasticsearch/client/log_query_and_deprecation.test.ts b/src/core/server/elasticsearch/client/log_query_and_deprecation.test.ts new file mode 100644 index 0000000000000..30d5d8b87ed1c --- /dev/null +++ b/src/core/server/elasticsearch/client/log_query_and_deprecation.test.ts @@ -0,0 +1,624 @@ +/* + * Copyright 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 { Buffer } from 'buffer'; +import { Readable } from 'stream'; + +import { + Client, + ConnectionRequestParams, + errors, + TransportRequestOptions, + TransportRequestParams, +} from '@elastic/elasticsearch'; +import type { DiagnosticResult, RequestBody } from '@elastic/elasticsearch'; + +import { parseClientOptionsMock, ClientMock } from './configure_client.test.mocks'; +import { loggingSystemMock } from '../../logging/logging_system.mock'; +import { instrumentEsQueryAndDeprecationLogger } from './log_query_and_deprecation'; + +const createApiResponse = ({ + body, + statusCode = 200, + headers = {}, + warnings = null, + params, + requestOptions = {}, +}: { + body: T; + statusCode?: number; + headers?: Record; + warnings?: string[] | null; + params?: TransportRequestParams | ConnectionRequestParams; + requestOptions?: TransportRequestOptions; +}): DiagnosticResult => { + return { + body, + statusCode, + headers, + warnings, + meta: { + body, + request: { + params: params!, + options: requestOptions, + } as any, + } as any, + }; +}; + +const createFakeClient = () => { + const actualEs = jest.requireActual('@elastic/elasticsearch'); + const client = new actualEs.Client({ + nodes: ['http://localhost'], // Enforcing `nodes` because it's mandatory + }); + jest.spyOn(client.diagnostic, 'on'); + return client as Client; +}; + +describe('instrumentQueryAndDeprecationLogger', () => { + let logger: ReturnType; + const client = createFakeClient(); + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + parseClientOptionsMock.mockReturnValue({}); + ClientMock.mockImplementation(() => createFakeClient()); + }); + + afterEach(() => { + parseClientOptionsMock.mockReset(); + ClientMock.mockReset(); + jest.clearAllMocks(); + }); + + function createResponseWithBody(body?: RequestBody) { + return createApiResponse({ + body: {}, + statusCode: 200, + params: { + method: 'GET', + path: '/foo', + querystring: { hello: 'dolly' }, + body, + }, + }); + } + + it('creates a query logger context based on the `type` parameter', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test123' }); + expect(logger.get).toHaveBeenCalledWith('query', 'test123'); + }); + + describe('logs each query', () => { + it('when request body is an object', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createResponseWithBody({ + seq_no_primary_term: true, + query: { + term: { user: 'kimchy' }, + }, + }); + + client.diagnostic.emit('response', null, response); + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "200 + GET /foo?hello=dolly + {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}", + undefined, + ], + ] + `); + }); + + it('when request body is a string', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createResponseWithBody( + JSON.stringify({ + seq_no_primary_term: true, + query: { + term: { user: 'kimchy' }, + }, + }) + ); + + client.diagnostic.emit('response', null, response); + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "200 + GET /foo?hello=dolly + {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}", + undefined, + ], + ] + `); + }); + + it('when request body is a buffer', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createResponseWithBody( + Buffer.from( + JSON.stringify({ + seq_no_primary_term: true, + query: { + term: { user: 'kimchy' }, + }, + }) + ) + ); + + client.diagnostic.emit('response', null, response); + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "200 + GET /foo?hello=dolly + [buffer]", + undefined, + ], + ] + `); + }); + + it('when request body is a readable stream', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createResponseWithBody( + Readable.from( + JSON.stringify({ + seq_no_primary_term: true, + query: { + term: { user: 'kimchy' }, + }, + }) + ) + ); + + client.diagnostic.emit('response', null, response); + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "200 + GET /foo?hello=dolly + [stream]", + undefined, + ], + ] + `); + }); + + it('when request body is not defined', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createResponseWithBody(); + + client.diagnostic.emit('response', null, response); + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "200 + GET /foo?hello=dolly", + undefined, + ], + ] + `); + }); + + it('properly encode queries', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createApiResponse({ + body: {}, + statusCode: 200, + params: { + method: 'GET', + path: '/foo', + querystring: { city: 'Münich' }, + }, + }); + + client.diagnostic.emit('response', null, response); + + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "200 + GET /foo?city=M%C3%BCnich", + undefined, + ], + ] + `); + }); + + it('logs queries even in case of errors', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createApiResponse({ + statusCode: 500, + body: { + error: { + type: 'internal server error', + }, + }, + params: { + method: 'GET', + path: '/foo', + querystring: { hello: 'dolly' }, + body: { + seq_no_primary_term: true, + query: { + term: { user: 'kimchy' }, + }, + }, + }, + }); + client.diagnostic.emit('response', new errors.ResponseError(response), response); + + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "500 + GET /foo?hello=dolly + {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}} [internal server error]: internal server error", + undefined, + ], + ] + `); + }); + + it('logs debug when the client emits an @elastic/elasticsearch error', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createApiResponse({ body: {} }); + client.diagnostic.emit('response', new errors.TimeoutError('message', response), response); + + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "[TimeoutError]: message", + undefined, + ], + ] + `); + }); + + it('logs debug when the client emits an ResponseError returned by elasticsearch', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createApiResponse({ + statusCode: 400, + headers: {}, + params: { + method: 'GET', + path: '/_path', + querystring: { hello: 'dolly' }, + }, + body: { + error: { + type: 'illegal_argument_exception', + reason: 'request [/_path] contains unrecognized parameter: [name]', + }, + }, + }); + client.diagnostic.emit('response', new errors.ResponseError(response), response); + + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "400 + GET /_path?hello=dolly [illegal_argument_exception]: request [/_path] contains unrecognized parameter: [name]", + undefined, + ], + ] + `); + }); + + it('logs default error info when the error response body is empty', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + let response: DiagnosticResult = createApiResponse({ + statusCode: 400, + headers: {}, + params: { + method: 'GET', + path: '/_path', + }, + body: { + error: {}, + }, + }); + client.diagnostic.emit('response', new errors.ResponseError(response), response); + + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "400 + GET /_path [undefined]: {\\"error\\":{}}", + undefined, + ], + ] + `); + + logger.debug.mockClear(); + + response = createApiResponse({ + statusCode: 400, + headers: {}, + params: { + method: 'GET', + path: '/_path', + }, + body: undefined, + }); + client.diagnostic.emit('response', new errors.ResponseError(response), response); + + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "400 + GET /_path [undefined]: Response Error", + undefined, + ], + ] + `); + }); + + it('adds meta information to logs', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + let response = createApiResponse({ + statusCode: 400, + headers: {}, + params: { + method: 'GET', + path: '/_path', + }, + requestOptions: { + opaqueId: 'opaque-id', + }, + body: { + error: {}, + }, + }); + client.diagnostic.emit('response', null, response); + + expect(loggingSystemMock.collect(logger).debug[0][1]).toMatchInlineSnapshot(` + Object { + "http": Object { + "request": Object { + "id": "opaque-id", + }, + }, + } + `); + + logger.debug.mockClear(); + + response = createApiResponse({ + statusCode: 400, + headers: {}, + params: { + method: 'GET', + path: '/_path', + }, + requestOptions: { + opaqueId: 'opaque-id', + }, + body: {} as any, + }); + client.diagnostic.emit('response', new errors.ResponseError(response), response); + + expect(loggingSystemMock.collect(logger).debug[0][1]).toMatchInlineSnapshot(` + Object { + "http": Object { + "request": Object { + "id": "opaque-id", + }, + }, + } + `); + }); + }); + + describe('deprecation warnings from response headers', () => { + it('does not log when no deprecation warning header is returned', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createApiResponse({ + statusCode: 200, + warnings: null, + params: { + method: 'GET', + path: '/_path', + querystring: { hello: 'dolly' }, + }, + body: { + hits: [ + { + _source: 'may the source be with you', + }, + ], + }, + }); + client.diagnostic.emit('response', new errors.ResponseError(response), response); + + // One debug log entry from 'elasticsearch.query' context + expect(loggingSystemMock.collect(logger).debug.length).toEqual(1); + expect(loggingSystemMock.collect(logger).info).toEqual([]); + }); + + it('does not log when warning header comes from a warn-agent that is not elasticsearch', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createApiResponse({ + statusCode: 200, + warnings: [ + '299 nginx/2.3.1 "GET /_path is deprecated"', + '299 nginx/2.3.1 "GET hello query param is deprecated"', + ], + params: { + method: 'GET', + path: '/_path', + querystring: { hello: 'dolly' }, + }, + body: { + hits: [ + { + _source: 'may the source be with you', + }, + ], + }, + }); + client.diagnostic.emit('response', new errors.ResponseError(response), response); + + // One debug log entry from 'elasticsearch.query' context + expect(loggingSystemMock.collect(logger).debug.length).toEqual(1); + expect(loggingSystemMock.collect(logger).info).toEqual([]); + }); + + it('logs error when the client receives an Elasticsearch error response for a deprecated request originating from a user', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createApiResponse({ + statusCode: 400, + warnings: ['299 Elasticsearch-8.1.0 "GET /_path is deprecated"'], + params: { + method: 'GET', + path: '/_path', + querystring: { hello: 'dolly' }, + }, + body: { + error: { + type: 'illegal_argument_exception', + reason: 'request [/_path] contains unrecognized parameter: [name]', + }, + }, + }); + client.diagnostic.emit('response', new errors.ResponseError(response), response); + + expect(loggingSystemMock.collect(logger).info).toEqual([]); + // Test debug[1] since theree is one log entry from 'elasticsearch.query' context + expect(loggingSystemMock.collect(logger).debug[1][0]).toMatch( + 'Elasticsearch deprecation: 299 Elasticsearch-8.1.0 "GET /_path is deprecated"' + ); + expect(loggingSystemMock.collect(logger).debug[1][0]).toMatch('Origin:user'); + expect(loggingSystemMock.collect(logger).debug[1][0]).toMatch(/Stack trace:\n.*at/); + expect(loggingSystemMock.collect(logger).debug[1][0]).toMatch( + /Query:\n.*400\n.*GET \/_path\?hello\=dolly \[illegal_argument_exception\]: request \[\/_path\] contains unrecognized parameter: \[name\]/ + ); + }); + + it('logs warning when the client receives an Elasticsearch error response for a deprecated request originating from kibana', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createApiResponse({ + statusCode: 400, + warnings: ['299 Elasticsearch-8.1.0 "GET /_path is deprecated"'], + params: { + method: 'GET', + path: '/_path', + querystring: { hello: 'dolly' }, + // Set the request header to indicate to Elasticsearch that this is a request over which users have no control + headers: { 'x-elastic-product-origin': 'kibana' }, + }, + body: { + error: { + type: 'illegal_argument_exception', + reason: 'request [/_path] contains unrecognized parameter: [name]', + }, + }, + }); + client.diagnostic.emit('response', new errors.ResponseError(response), response); + + // One debug log entry from 'elasticsearch.query' context + expect(loggingSystemMock.collect(logger).debug.length).toEqual(1); + expect(loggingSystemMock.collect(logger).info[0][0]).toMatch( + 'Elasticsearch deprecation: 299 Elasticsearch-8.1.0 "GET /_path is deprecated"' + ); + expect(loggingSystemMock.collect(logger).info[0][0]).toMatch('Origin:kibana'); + expect(loggingSystemMock.collect(logger).info[0][0]).toMatch(/Stack trace:\n.*at/); + expect(loggingSystemMock.collect(logger).info[0][0]).toMatch( + /Query:\n.*400\n.*GET \/_path\?hello\=dolly \[illegal_argument_exception\]: request \[\/_path\] contains unrecognized parameter: \[name\]/ + ); + }); + + it('logs error when the client receives an Elasticsearch success response for a deprecated request originating from a user', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createApiResponse({ + statusCode: 200, + warnings: ['299 Elasticsearch-8.1.0 "GET /_path is deprecated"'], + params: { + method: 'GET', + path: '/_path', + querystring: { hello: 'dolly' }, + }, + body: { + hits: [ + { + _source: 'may the source be with you', + }, + ], + }, + }); + client.diagnostic.emit('response', null, response); + + expect(loggingSystemMock.collect(logger).info).toEqual([]); + // Test debug[1] since theree is one log entry from 'elasticsearch.query' context + expect(loggingSystemMock.collect(logger).debug[1][0]).toMatch( + 'Elasticsearch deprecation: 299 Elasticsearch-8.1.0 "GET /_path is deprecated"' + ); + expect(loggingSystemMock.collect(logger).debug[1][0]).toMatch('Origin:user'); + expect(loggingSystemMock.collect(logger).debug[1][0]).toMatch(/Stack trace:\n.*at/); + expect(loggingSystemMock.collect(logger).debug[1][0]).toMatch( + /Query:\n.*200\n.*GET \/_path\?hello\=dolly/ + ); + }); + + it('logs warning when the client receives an Elasticsearch success response for a deprecated request originating from kibana', () => { + instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); + + const response = createApiResponse({ + statusCode: 200, + warnings: ['299 Elasticsearch-8.1.0 "GET /_path is deprecated"'], + params: { + method: 'GET', + path: '/_path', + querystring: { hello: 'dolly' }, + // Set the request header to indicate to Elasticsearch that this is a request over which users have no control + headers: { 'x-elastic-product-origin': 'kibana' }, + }, + body: { + hits: [ + { + _source: 'may the source be with you', + }, + ], + }, + }); + client.diagnostic.emit('response', null, response); + + // One debug log entry from 'elasticsearch.query' context + expect(loggingSystemMock.collect(logger).debug.length).toEqual(1); + expect(loggingSystemMock.collect(logger).info[0][0]).toMatch( + 'Elasticsearch deprecation: 299 Elasticsearch-8.1.0 "GET /_path is deprecated"' + ); + expect(loggingSystemMock.collect(logger).info[0][0]).toMatch('Origin:kibana'); + expect(loggingSystemMock.collect(logger).info[0][0]).toMatch(/Stack trace:\n.*at/); + expect(loggingSystemMock.collect(logger).info[0][0]).toMatch( + /Query:\n.*200\n.*GET \/_path\?hello\=dolly/ + ); + }); + }); +}); diff --git a/src/core/server/elasticsearch/client/log_query_and_deprecation.ts b/src/core/server/elasticsearch/client/log_query_and_deprecation.ts new file mode 100644 index 0000000000000..fc5a0fa6e1111 --- /dev/null +++ b/src/core/server/elasticsearch/client/log_query_and_deprecation.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Buffer } from 'buffer'; +import { stringify } from 'querystring'; +import { errors, DiagnosticResult, RequestBody, Client } from '@elastic/elasticsearch'; +import type { ElasticsearchErrorDetails } from './types'; +import { Logger } from '../../logging'; + +const convertQueryString = (qs: string | Record | undefined): string => { + if (qs === undefined || typeof qs === 'string') { + return qs ?? ''; + } + return stringify(qs); +}; + +function ensureString(body: RequestBody): string { + if (typeof body === 'string') return body; + if (Buffer.isBuffer(body)) return '[buffer]'; + if ('readable' in body && body.readable && typeof body._read === 'function') return '[stream]'; + return JSON.stringify(body); +} + +/** + * Returns a debug message from an Elasticsearch error in the following format: + * [error type] error reason + */ +export function getErrorMessage(error: errors.ElasticsearchClientError): string { + if (error instanceof errors.ResponseError) { + const errorBody = error.meta.body as ElasticsearchErrorDetails; + return `[${errorBody?.error?.type}]: ${errorBody?.error?.reason ?? error.message}`; + } + return `[${error.name}]: ${error.message}`; +} + +/** + * returns a string in format: + * + * status code + * method URL + * request body + * + * so it could be copy-pasted into the Dev console + */ +function getResponseMessage(event: DiagnosticResult): string { + const errorMeta = getRequestDebugMeta(event); + const body = errorMeta.body ? `\n${errorMeta.body}` : ''; + return `${errorMeta.statusCode}\n${errorMeta.method} ${errorMeta.url}${body}`; +} + +/** + * Returns stringified debug information from an Elasticsearch request event + * useful for logging in case of an unexpected failure. + */ +export function getRequestDebugMeta(event: DiagnosticResult): { + url: string; + body: string; + statusCode: number | null; + method: string; +} { + const params = event.meta.request.params; + // definition is wrong, `params.querystring` can be either a string or an object + const querystring = convertQueryString(params.querystring); + return { + url: `${params.path}${querystring ? `?${querystring}` : ''}`, + body: params.body ? `${ensureString(params.body)}` : '', + method: params.method, + statusCode: event.statusCode!, + }; +} + +/** HTTP Warning headers have the following syntax: + * (where warn-code is a three digit number) + * This function tests if a warning comes from an Elasticsearch warn-agent + * */ +const isEsWarning = (warning: string) => /\d\d\d Elasticsearch-/.test(warning); + +export const instrumentEsQueryAndDeprecationLogger = ({ + logger, + client, + type, +}: { + logger: Logger; + client: Client; + type: string; +}) => { + const queryLogger = logger.get('query', type); + const deprecationLogger = logger.get('deprecation'); + client.diagnostic.on('response', (error, event) => { + if (event) { + const opaqueId = event.meta.request.options.opaqueId; + const meta = opaqueId + ? { + http: { request: { id: event.meta.request.options.opaqueId } }, + } + : undefined; // do not clutter logs if opaqueId is not present + let queryMsg = ''; + if (error) { + if (error instanceof errors.ResponseError) { + queryMsg = `${getResponseMessage(event)} ${getErrorMessage(error)}`; + } else { + queryMsg = getErrorMessage(error); + } + } else { + queryMsg = getResponseMessage(event); + } + + queryLogger.debug(queryMsg, meta); + + if (event.warnings && event.warnings.filter(isEsWarning).length > 0) { + // Plugins can explicitly mark requests as originating from a user by + // removing the `'x-elastic-product-origin': 'kibana'` header that's + // added by default. User requests will be shown to users in the + // upgrade assistant UI as an action item that has to be addressed + // before they upgrade. + // Kibana requests will be hidden from the upgrade assistant UI and are + // only logged to help developers maintain their plugins + const requestOrigin = + (event.meta.request.params.headers != null && + (event.meta.request.params.headers[ + 'x-elastic-product-origin' + ] as unknown as string)) === 'kibana' + ? 'kibana' + : 'user'; + + // Strip the first 5 stack trace lines as these are irrelavent to finding the call site + const stackTrace = new Error().stack?.split('\n').slice(5).join('\n'); + + const deprecationMsg = `Elasticsearch deprecation: ${event.warnings}\nOrigin:${requestOrigin}\nStack trace:\n${stackTrace}\nQuery:\n${queryMsg}`; + if (requestOrigin === 'kibana') { + deprecationLogger.info(deprecationMsg); + } else { + deprecationLogger.debug(deprecationMsg); + } + } + } + }); +}; diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 3b75d19b80a10..ce5672ad30519 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -21,7 +21,7 @@ import { MockClusterClient, isScriptingEnabledMock } from './elasticsearch_servi import type { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { BehaviorSubject } from 'rxjs'; import { first } from 'rxjs/operators'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Env } from '../config'; import { configServiceMock, getEnvOptions } from '../config/mocks'; import { CoreContext } from '../core_context'; diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts index ad05d37c81e99..8e2cd58733faf 100644 --- a/src/core/server/http/cookie_session_storage.test.ts +++ b/src/core/server/http/cookie_session_storage.test.ts @@ -8,7 +8,7 @@ import { parse as parseCookie } from 'tough-cookie'; import supertest from 'supertest'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { ByteSizeValue } from '@kbn/config-schema'; import { BehaviorSubject } from 'rxjs'; diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index 4955d19668580..3a387cdfd5e35 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -10,7 +10,7 @@ import { mockHttpServer } from './http_service.test.mocks'; import { noop } from 'lodash'; import { BehaviorSubject } from 'rxjs'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { getEnvOptions } from '../config/mocks'; import { HttpService } from '.'; import { HttpConfigType, config } from './http_config'; diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts index 4e1a88e967f8f..8a8c545b365b3 100644 --- a/src/core/server/http/test_utils.ts +++ b/src/core/server/http/test_utils.ts @@ -8,7 +8,7 @@ import { BehaviorSubject } from 'rxjs'; import moment from 'moment'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { ByteSizeValue } from '@kbn/config-schema'; import { Env } from '../config'; import { HttpService } from './http_service'; diff --git a/src/core/server/metrics/logging/get_ops_metrics_log.test.ts b/src/core/server/metrics/logging/get_ops_metrics_log.test.ts index cba188c94c74e..3fd3c4a7a24d6 100644 --- a/src/core/server/metrics/logging/get_ops_metrics_log.test.ts +++ b/src/core/server/metrics/logging/get_ops_metrics_log.test.ts @@ -42,6 +42,7 @@ const testMetrics = { memory: { heap: { used_in_bytes: 100 } }, uptime_in_millis: 1500, event_loop_delay: 50, + event_loop_delay_histogram: { percentiles: { '50': 50, '75': 75, '95': 95, '99': 99 } }, }, os: { load: { @@ -56,7 +57,7 @@ describe('getEcsOpsMetricsLog', () => { it('provides correctly formatted message', () => { const result = getEcsOpsMetricsLog(createMockOpsMetrics(testMetrics)); expect(result.message).toMatchInlineSnapshot( - `"memory: 100.0B uptime: 0:00:01 load: [10.00,20.00,30.00] delay: 50.000"` + `"memory: 100.0B uptime: 0:00:01 load: [10.00,20.00,30.00] mean delay: 50.000 delay histogram: { 50: 50.000; 95: 95.000; 99: 99.000 }"` ); }); @@ -70,6 +71,7 @@ describe('getEcsOpsMetricsLog', () => { const missingMetrics = { ...baseMetrics, process: {}, + processes: [], os: {}, } as unknown as OpsMetrics; const logMeta = getEcsOpsMetricsLog(missingMetrics); @@ -77,39 +79,41 @@ describe('getEcsOpsMetricsLog', () => { }); it('provides an ECS-compatible response', () => { - const logMeta = getEcsOpsMetricsLog(createBaseOpsMetrics()); - expect(logMeta).toMatchInlineSnapshot(` + const logMeta = getEcsOpsMetricsLog(createMockOpsMetrics(testMetrics)); + expect(logMeta.meta).toMatchInlineSnapshot(` Object { - "message": "memory: 1.0B load: [1.00,1.00,1.00] delay: 1.000", - "meta": Object { - "event": Object { - "category": Array [ - "process", - "host", - ], - "kind": "metric", - "type": Array [ - "info", - ], - }, - "host": Object { - "os": Object { - "load": Object { - "15m": 1, - "1m": 1, - "5m": 1, - }, + "event": Object { + "category": Array [ + "process", + "host", + ], + "kind": "metric", + "type": Array [ + "info", + ], + }, + "host": Object { + "os": Object { + "load": Object { + "15m": 30, + "1m": 10, + "5m": 20, }, }, - "process": Object { - "eventLoopDelay": 1, - "memory": Object { - "heap": Object { - "usedInBytes": 1, - }, + }, + "process": Object { + "eventLoopDelay": 50, + "eventLoopDelayHistogram": Object { + "50": 50, + "95": 95, + "99": 99, + }, + "memory": Object { + "heap": Object { + "usedInBytes": 100, }, - "uptime": 0, }, + "uptime": 1, }, } `); diff --git a/src/core/server/metrics/logging/get_ops_metrics_log.ts b/src/core/server/metrics/logging/get_ops_metrics_log.ts index 7e13f35889ec7..6211407ae86f0 100644 --- a/src/core/server/metrics/logging/get_ops_metrics_log.ts +++ b/src/core/server/metrics/logging/get_ops_metrics_log.ts @@ -30,10 +30,29 @@ export function getEcsOpsMetricsLog(metrics: OpsMetrics) { // HH:mm:ss message format for backward compatibility const uptimeValMsg = uptimeVal ? `uptime: ${numeral(uptimeVal).format('00:00:00')} ` : ''; - // Event loop delay is in ms + // Event loop delay metrics are in ms const eventLoopDelayVal = process?.event_loop_delay; const eventLoopDelayValMsg = eventLoopDelayVal - ? `delay: ${numeral(process?.event_loop_delay).format('0.000')}` + ? `mean delay: ${numeral(process?.event_loop_delay).format('0.000')}` + : ''; + + const eventLoopDelayPercentiles = process?.event_loop_delay_histogram?.percentiles; + + // Extract 50th, 95th and 99th percentiles for log meta + const eventLoopDelayHistVals = eventLoopDelayPercentiles + ? { + 50: eventLoopDelayPercentiles[50], + 95: eventLoopDelayPercentiles[95], + 99: eventLoopDelayPercentiles[99], + } + : undefined; + // Format message from 50th, 95th and 99th percentiles + const eventLoopDelayHistMsg = eventLoopDelayPercentiles + ? ` delay histogram: { 50: ${numeral(eventLoopDelayPercentiles['50']).format( + '0.000' + )}; 95: ${numeral(eventLoopDelayPercentiles['95']).format('0.000')}; 99: ${numeral( + eventLoopDelayPercentiles['99'] + ).format('0.000')} }` : ''; const loadEntries = { @@ -65,6 +84,7 @@ export function getEcsOpsMetricsLog(metrics: OpsMetrics) { }, }, eventLoopDelay: eventLoopDelayVal, + eventLoopDelayHistogram: eventLoopDelayHistVals, }, host: { os: { @@ -75,7 +95,13 @@ export function getEcsOpsMetricsLog(metrics: OpsMetrics) { }; return { - message: `${processMemoryUsedInBytesMsg}${uptimeValMsg}${loadValsMsg}${eventLoopDelayValMsg}`, + message: [ + processMemoryUsedInBytesMsg, + uptimeValMsg, + loadValsMsg, + eventLoopDelayValMsg, + eventLoopDelayHistMsg, + ].join(''), meta, }; } diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts index d7de41fd7ccf7..27043b8fa2c8a 100644 --- a/src/core/server/metrics/metrics_service.test.ts +++ b/src/core/server/metrics/metrics_service.test.ts @@ -203,6 +203,7 @@ describe('MetricsService', () => { }, "process": Object { "eventLoopDelay": undefined, + "eventLoopDelayHistogram": undefined, "memory": Object { "heap": Object { "usedInBytes": undefined, diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index 958e051d0476d..a6ffdff4422be 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -7,7 +7,7 @@ */ // must be before mocks imports to avoid conflicting with `REPO_ROOT` accessor. -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { mockPackage, scanPluginSearchPathsMock } from './plugins_discovery.test.mocks'; import mockFs from 'mock-fs'; import { loggingSystemMock } from '../../logging/logging_system.mock'; diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts index 4170d9422f277..ebbb3fa473b6d 100644 --- a/src/core/server/plugins/integration_tests/plugins_service.test.ts +++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts @@ -7,7 +7,7 @@ */ // must be before mocks imports to avoid conflicting with `REPO_ROOT` accessor. -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { mockPackage, mockDiscover } from './plugins_service.test.mocks'; import { join } from 'path'; diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 513e893992005..92cbda2a69cfe 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -8,7 +8,7 @@ import { join } from 'path'; import { BehaviorSubject } from 'rxjs'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { schema } from '@kbn/config-schema'; import { Env } from '../config'; diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 867d4d978314b..7bcf392ed510b 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -8,7 +8,7 @@ import { duration } from 'moment'; import { first } from 'rxjs/operators'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { fromRoot } from '@kbn/utils'; import { createPluginInitializerContext, diff --git a/src/core/server/plugins/plugins_config.test.ts b/src/core/server/plugins/plugins_config.test.ts index d65b057fb65c0..b9225054e63ef 100644 --- a/src/core/server/plugins/plugins_config.test.ts +++ b/src/core/server/plugins/plugins_config.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { getEnvOptions } from '../config/mocks'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { Env } from '../config'; diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 0c077d732c67b..5a05817d2111f 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -11,7 +11,8 @@ import { mockDiscover, mockPackage } from './plugins_service.test.mocks'; import { resolve, join } from 'path'; import { BehaviorSubject, from } from 'rxjs'; import { schema } from '@kbn/config-schema'; -import { createAbsolutePathSerializer, REPO_ROOT } from '@kbn/dev-utils'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { ConfigPath, ConfigService, Env } from '../config'; import { rawConfigServiceMock, getEnvOptions } from '../config/mocks'; diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 4cd8e4c551bea..3d8a47005b362 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -14,7 +14,7 @@ import { import { BehaviorSubject } from 'rxjs'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Env } from '../config'; import { configServiceMock, getEnvOptions } from '../config/mocks'; import { CoreContext } from '../core_context'; diff --git a/src/core/server/preboot/preboot_service.test.ts b/src/core/server/preboot/preboot_service.test.ts index dd4b1cb7d1df0..77242f0c5765f 100644 --- a/src/core/server/preboot/preboot_service.test.ts +++ b/src/core/server/preboot/preboot_service.test.ts @@ -7,7 +7,7 @@ */ import { nextTick } from '@kbn/test/jest'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { LoggerFactory } from '@kbn/logging'; import { Env } from '@kbn/config'; import { getEnvOptions } from '../config/mocks'; diff --git a/src/core/server/root/index.test.ts b/src/core/server/root/index.test.ts index 7eba051a128f0..6ea3e05b9c2c2 100644 --- a/src/core/server/root/index.test.ts +++ b/src/core/server/root/index.test.ts @@ -10,7 +10,7 @@ import { rawConfigService, configService, logger, mockServer } from './index.tes import { BehaviorSubject } from 'rxjs'; import { filter, first } from 'rxjs/operators'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { getEnvOptions } from '../config/mocks'; import { Root } from '.'; import { Env } from '../config'; diff --git a/src/core/server/saved_objects/migrations/integration_tests/7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7.7.2_xpack_100k.test.ts index c22c6154c2605..139cd298d28ed 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/7.7.2_xpack_100k.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/7.7.2_xpack_100k.test.ts @@ -8,7 +8,7 @@ import path from 'path'; import { unlink } from 'fs/promises'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Env } from '@kbn/config'; import { getEnvOptions } from '../../../config/mocks'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; diff --git a/src/core/server/saved_objects/migrations/integration_tests/migration_from_older_v1.test.ts b/src/core/server/saved_objects/migrations/integration_tests/migration_from_older_v1.test.ts index 0ed9262017263..c341463b78910 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/migration_from_older_v1.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/migration_from_older_v1.test.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Fs from 'fs'; import Util from 'util'; import Semver from 'semver'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Env } from '@kbn/config'; import { getEnvOptions } from '../../../config/mocks'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; diff --git a/src/core/server/saved_objects/migrations/integration_tests/migration_from_same_v1.test.ts b/src/core/server/saved_objects/migrations/integration_tests/migration_from_same_v1.test.ts index 15d985daccba6..34d1317755c14 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/migration_from_same_v1.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/migration_from_same_v1.test.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Fs from 'fs'; import Util from 'util'; import Semver from 'semver'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Env } from '@kbn/config'; import { getEnvOptions } from '../../../config/mocks'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index a4f6c019c9624..a8bda95af46f9 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -19,7 +19,7 @@ import { import { BehaviorSubject } from 'rxjs'; import { RawPackageInfo } from '@kbn/config'; import { ByteSizeValue } from '@kbn/config-schema'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { SavedObjectsService } from './saved_objects_service'; import { mockCoreContext } from '../core_context.mock'; diff --git a/src/core/server/saved_objects/service/lib/repository.test.ts b/src/core/server/saved_objects/service/lib/repository.test.ts index ab692b146e7f6..1668df7a82253 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository.test.ts @@ -3558,6 +3558,20 @@ describe('SavedObjectsRepository', () => { }); }); + it('search for the right fields when typeToNamespacesMap is set', async () => { + const relevantOpts = { + ...commonOptions, + fields: ['title'], + type: '', + namespaces: [], + typeToNamespacesMap: new Map([[type, [namespace]]]), + }; + + await findSuccess(relevantOpts, namespace); + const esOptions = client.search.mock.calls[0][0]; + expect(esOptions?._source ?? []).toContain('index-pattern.title'); + }); + it(`accepts hasReferenceOperator`, async () => { const relevantOpts: SavedObjectsFindOptions = { ...commonOptions, diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 0d17525016043..53bc6f158bf93 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -930,7 +930,7 @@ export class SavedObjectsRepository { index: pit ? undefined : this.getIndicesForTypes(allowedTypes), // If `searchAfter` is provided, we drop `from` as it will not be used for pagination. from: searchAfter ? undefined : perPage * (page - 1), - _source: includedFields(type, fields), + _source: includedFields(allowedTypes, fields), preference, rest_total_hits_as_int: true, size: perPage, @@ -938,7 +938,7 @@ export class SavedObjectsRepository { size: perPage, seq_no_primary_term: true, from: perPage * (page - 1), - _source: includedFields(type, fields), + _source: includedFields(allowedTypes, fields), ...(aggsObject ? { aggs: aggsObject } : {}), ...getSearchDsl(this._mappings, this._registry, { search, diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 112693aae0279..48547883d5f67 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -26,7 +26,7 @@ import { } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { rawConfigServiceMock, getEnvOptions } from './config/mocks'; import { Env } from './config'; import { Server } from './server'; diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts index ef635e90dac70..3f85beb2acec6 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -7,7 +7,7 @@ */ import { Env } from '@kbn/config'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { getEnvOptions } from '../../config/mocks'; import { startServers, stopServers } from './lib'; import { docExistsSuite } from './doc_exists'; diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 58720be637e2f..c326c7a35df63 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { createTestEsCluster, CreateTestEsClusterOptions, diff --git a/src/core/types/elasticsearch/search.ts b/src/core/types/elasticsearch/search.ts index c28bf3c258f77..ac93a45da3258 100644 --- a/src/core/types/elasticsearch/search.ts +++ b/src/core/types/elasticsearch/search.ts @@ -9,6 +9,11 @@ import { ValuesType, UnionToIntersection } from 'utility-types'; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +interface AggregationsAggregationContainer extends Record { + aggs?: any; + aggregations?: any; +} + type InvalidAggregationRequest = unknown; // ensures aggregations work with requests where aggregation options are a union type, @@ -31,7 +36,7 @@ type KeysOfSources = T extends [any] ? KeyOfSource & KeyOfSource & KeyOfSource & KeyOfSource : Record; -type CompositeKeysOf = +type CompositeKeysOf = TAggregationContainer extends { composite: { sources: [...infer TSource] }; } @@ -40,7 +45,7 @@ type CompositeKeysOf = +type TopMetricKeysOf = TAggregationContainer extends { top_metrics: { metrics: { field: infer TField } } } ? TField : TAggregationContainer extends { top_metrics: { metrics: Array<{ field: infer TField }> } } @@ -92,17 +97,9 @@ type HitsOf< > >; -type AggregationTypeName = Exclude< - keyof estypes.AggregationsAggregationContainer, - 'aggs' | 'aggregations' ->; +type AggregationMap = Partial>; -type AggregationMap = Partial>; - -type TopLevelAggregationRequest = Pick< - estypes.AggregationsAggregationContainer, - 'aggs' | 'aggregations' ->; +type TopLevelAggregationRequest = Pick; type MaybeKeyed< TAggregationContainer, @@ -113,448 +110,460 @@ type MaybeKeyed< : { buckets: TBucket[] }; export type AggregateOf< - TAggregationContainer extends estypes.AggregationsAggregationContainer, + TAggregationContainer extends AggregationsAggregationContainer, TDocument -> = (Record & { - adjacency_matrix: { - buckets: Array< - { - key: string; - doc_count: number; - } & SubAggregateOf - >; - }; - auto_date_histogram: { - interval: string; - buckets: Array< - { - key: number; - key_as_string: string; - doc_count: number; - } & SubAggregateOf - >; - }; - avg: { - value: number | null; - value_as_string?: string; - }; - avg_bucket: { - value: number | null; - }; - boxplot: { - min: number | null; - max: number | null; - q1: number | null; - q2: number | null; - q3: number | null; - }; - bucket_script: { - value: unknown; - }; - cardinality: { - value: number; - }; - children: { - doc_count: number; - } & SubAggregateOf; - composite: { - after_key: CompositeKeysOf; - buckets: Array< - { +> = ValuesType< + Pick< + Record & { + adjacency_matrix: { + buckets: Array< + { + key: string; + doc_count: number; + } & SubAggregateOf + >; + }; + auto_date_histogram: { + interval: string; + buckets: Array< + { + key: number; + key_as_string: string; + doc_count: number; + } & SubAggregateOf + >; + }; + avg: { + value: number | null; + value_as_string?: string; + }; + avg_bucket: { + value: number | null; + }; + boxplot: { + min: number | null; + max: number | null; + q1: number | null; + q2: number | null; + q3: number | null; + }; + bucket_script: { + value: unknown; + }; + cardinality: { + value: number; + }; + children: { doc_count: number; - key: CompositeKeysOf; - } & SubAggregateOf - >; - }; - cumulative_cardinality: { - value: number; - }; - cumulative_sum: { - value: number; - }; - date_histogram: MaybeKeyed< - TAggregationContainer, - { - key: number; - key_as_string: string; - doc_count: number; - } & SubAggregateOf - >; - date_range: MaybeKeyed< - TAggregationContainer, - Partial<{ from: string | number; from_as_string: string }> & - Partial<{ to: string | number; to_as_string: string }> & { + } & SubAggregateOf; + composite: { + after_key: CompositeKeysOf; + buckets: Array< + { + doc_count: number; + key: CompositeKeysOf; + } & SubAggregateOf + >; + }; + cumulative_cardinality: { + value: number; + }; + cumulative_sum: { + value: number; + }; + date_histogram: MaybeKeyed< + TAggregationContainer, + { + key: number; + key_as_string: string; + doc_count: number; + } & SubAggregateOf + >; + date_range: MaybeKeyed< + TAggregationContainer, + Partial<{ from: string | number; from_as_string: string }> & + Partial<{ to: string | number; to_as_string: string }> & { + doc_count: number; + key: string; + } + >; + derivative: + | { + value: number | null; + } + | undefined; + extended_stats: { + count: number; + min: number | null; + max: number | null; + avg: number | null; + sum: number; + sum_of_squares: number | null; + variance: number | null; + variance_population: number | null; + variance_sampling: number | null; + std_deviation: number | null; + std_deviation_population: number | null; + std_deviation_sampling: number | null; + std_deviation_bounds: { + upper: number | null; + lower: number | null; + upper_population: number | null; + lower_population: number | null; + upper_sampling: number | null; + lower_sampling: number | null; + }; + } & ( + | { + min_as_string: string; + max_as_string: string; + avg_as_string: string; + sum_of_squares_as_string: string; + variance_population_as_string: string; + variance_sampling_as_string: string; + std_deviation_as_string: string; + std_deviation_population_as_string: string; + std_deviation_sampling_as_string: string; + std_deviation_bounds_as_string: { + upper: string; + lower: string; + upper_population: string; + lower_population: string; + upper_sampling: string; + lower_sampling: string; + }; + } + | {} + ); + extended_stats_bucket: { + count: number; + min: number | null; + max: number | null; + avg: number | null; + sum: number | null; + sum_of_squares: number | null; + variance: number | null; + variance_population: number | null; + variance_sampling: number | null; + std_deviation: number | null; + std_deviation_population: number | null; + std_deviation_sampling: number | null; + std_deviation_bounds: { + upper: number | null; + lower: number | null; + upper_population: number | null; + lower_population: number | null; + upper_sampling: number | null; + lower_sampling: number | null; + }; + }; + filter: { doc_count: number; - key: string; - } - >; - derivative: - | { - value: number | null; - } - | undefined; - extended_stats: { - count: number; - min: number | null; - max: number | null; - avg: number | null; - sum: number; - sum_of_squares: number | null; - variance: number | null; - variance_population: number | null; - variance_sampling: number | null; - std_deviation: number | null; - std_deviation_population: number | null; - std_deviation_sampling: number | null; - std_deviation_bounds: { - upper: number | null; - lower: number | null; - upper_population: number | null; - lower_population: number | null; - upper_sampling: number | null; - lower_sampling: number | null; - }; - } & ( - | { - min_as_string: string; - max_as_string: string; - avg_as_string: string; - sum_of_squares_as_string: string; - variance_population_as_string: string; - variance_sampling_as_string: string; - std_deviation_as_string: string; - std_deviation_population_as_string: string; - std_deviation_sampling_as_string: string; - std_deviation_bounds_as_string: { - upper: string; - lower: string; - upper_population: string; - lower_population: string; - upper_sampling: string; - lower_sampling: string; + } & SubAggregateOf; + filters: { + buckets: TAggregationContainer extends { filters: { filters: any[] } } + ? Array< + { + doc_count: number; + } & SubAggregateOf + > + : TAggregationContainer extends { filters: { filters: Record } } + ? { + [key in keyof TAggregationContainer['filters']['filters']]: { + doc_count: number; + } & SubAggregateOf; + } & (TAggregationContainer extends { + filters: { other_bucket_key: infer TOtherBucketKey }; + } + ? Record< + TOtherBucketKey & string, + { doc_count: number } & SubAggregateOf + > + : unknown) & + (TAggregationContainer extends { filters: { other_bucket: true } } + ? { + _other: { doc_count: number } & SubAggregateOf< + TAggregationContainer, + TDocument + >; + } + : unknown) + : unknown; + }; + geo_bounds: { + top_left: { + lat: number | null; + lon: number | null; }; - } - | {} - ); - extended_stats_bucket: { - count: number; - min: number | null; - max: number | null; - avg: number | null; - sum: number | null; - sum_of_squares: number | null; - variance: number | null; - variance_population: number | null; - variance_sampling: number | null; - std_deviation: number | null; - std_deviation_population: number | null; - std_deviation_sampling: number | null; - std_deviation_bounds: { - upper: number | null; - lower: number | null; - upper_population: number | null; - lower_population: number | null; - upper_sampling: number | null; - lower_sampling: number | null; - }; - }; - filter: { - doc_count: number; - } & SubAggregateOf; - filters: { - buckets: TAggregationContainer extends { filters: { filters: any[] } } - ? Array< + bottom_right: { + lat: number | null; + lon: number | null; + }; + }; + geo_centroid: { + count: number; + location: { + lat: number; + lon: number; + }; + }; + geo_distance: MaybeKeyed< + TAggregationContainer, + { + from: number; + to?: number; + doc_count: number; + } & SubAggregateOf + >; + geo_hash: { + buckets: Array< { doc_count: number; + key: string; } & SubAggregateOf - > - : TAggregationContainer extends { filters: { filters: Record } } - ? { - [key in keyof TAggregationContainer['filters']['filters']]: { + >; + }; + geotile_grid: { + buckets: Array< + { doc_count: number; - } & SubAggregateOf; - } & (TAggregationContainer extends { filters: { other_bucket_key: infer TOtherBucketKey } } - ? Record< - TOtherBucketKey & string, - { doc_count: number } & SubAggregateOf - > - : unknown) & - (TAggregationContainer extends { filters: { other_bucket: true } } - ? { _other: { doc_count: number } & SubAggregateOf } - : unknown) - : unknown; - }; - geo_bounds: { - top_left: { - lat: number | null; - lon: number | null; - }; - bottom_right: { - lat: number | null; - lon: number | null; - }; - }; - geo_centroid: { - count: number; - location: { - lat: number; - lon: number; - }; - }; - geo_distance: MaybeKeyed< - TAggregationContainer, - { - from: number; - to?: number; - doc_count: number; - } & SubAggregateOf - >; - geo_hash: { - buckets: Array< - { + key: string; + } & SubAggregateOf + >; + }; + global: { doc_count: number; - key: string; - } & SubAggregateOf - >; - }; - geotile_grid: { - buckets: Array< - { + } & SubAggregateOf; + histogram: MaybeKeyed< + TAggregationContainer, + { + key: number; + doc_count: number; + } & SubAggregateOf + >; + ip_range: MaybeKeyed< + TAggregationContainer, + { + key: string; + from?: string; + to?: string; + doc_count: number; + }, + TAggregationContainer extends { ip_range: { ranges: Array } } + ? TRangeType extends { key: infer TKeys } + ? TKeys + : string + : string + >; + inference: { + value: number; + prediction_probability: number; + prediction_score: number; + }; + max: { + value: number | null; + value_as_string?: string; + }; + max_bucket: { + value: number | null; + }; + min: { + value: number | null; + value_as_string?: string; + }; + min_bucket: { + value: number | null; + }; + median_absolute_deviation: { + value: number | null; + }; + moving_avg: + | { + value: number | null; + } + | undefined; + moving_fn: { + value: number | null; + }; + moving_percentiles: TAggregationContainer extends Record + ? Array<{ key: number; value: number | null }> + : Record | undefined; + missing: { doc_count: number; - key: string; - } & SubAggregateOf - >; - }; - global: { - doc_count: number; - } & SubAggregateOf; - histogram: MaybeKeyed< - TAggregationContainer, - { - key: number; - doc_count: number; - } & SubAggregateOf - >; - ip_range: MaybeKeyed< - TAggregationContainer, - { - key: string; - from?: string; - to?: string; - doc_count: number; - }, - TAggregationContainer extends { ip_range: { ranges: Array } } - ? TRangeType extends { key: infer TKeys } - ? TKeys - : string - : string - >; - inference: { - value: number; - prediction_probability: number; - prediction_score: number; - }; - max: { - value: number | null; - value_as_string?: string; - }; - max_bucket: { - value: number | null; - }; - min: { - value: number | null; - value_as_string?: string; - }; - min_bucket: { - value: number | null; - }; - median_absolute_deviation: { - value: number | null; - }; - moving_avg: - | { + } & SubAggregateOf; + multi_terms: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: Array< + { + doc_count: number; + key: string[]; + } & SubAggregateOf + >; + }; + nested: { + doc_count: number; + } & SubAggregateOf; + normalize: { value: number | null; - } - | undefined; - moving_fn: { - value: number | null; - }; - moving_percentiles: TAggregationContainer extends Record - ? Array<{ key: number; value: number | null }> - : Record | undefined; - missing: { - doc_count: number; - } & SubAggregateOf; - multi_terms: { - doc_count_error_upper_bound: number; - sum_other_doc_count: number; - buckets: Array< - { + // TODO: should be perhaps based on input? ie when `format` is specified + value_as_string?: string; + }; + parent: { doc_count: number; - key: string[]; - } & SubAggregateOf - >; - }; - nested: { - doc_count: number; - } & SubAggregateOf; - normalize: { - value: number | null; - // TODO: should be perhaps based on input? ie when `format` is specified - value_as_string?: string; - }; - parent: { - doc_count: number; - } & SubAggregateOf; - percentiles: { - values: TAggregationContainer extends Record - ? Array<{ key: number; value: number | null }> - : Record; - }; - percentile_ranks: { - values: TAggregationContainer extends Record - ? Array<{ key: number; value: number | null }> - : Record; - }; - percentiles_bucket: { - values: TAggregationContainer extends Record - ? Array<{ key: number; value: number | null }> - : Record; - }; - range: MaybeKeyed< - TAggregationContainer, - { - key: string; - from?: number; - from_as_string?: string; - to?: number; - to_as_string?: string; - doc_count: number; - }, - TAggregationContainer extends { range: { ranges: Array } } - ? TRangeType extends { key: infer TKeys } - ? TKeys - : string - : string - >; - rare_terms: Array< - { - key: string | number; - doc_count: number; - } & SubAggregateOf - >; - rate: { - value: number | null; - }; - reverse_nested: { - doc_count: number; - } & SubAggregateOf; - sampler: { - doc_count: number; - } & SubAggregateOf; - scripted_metric: { - value: unknown; - }; - serial_diff: { - value: number | null; - // TODO: should be perhaps based on input? ie when `format` is specified - value_as_string?: string; - }; - significant_terms: { - doc_count: number; - bg_count: number; - buckets: Array< - { - key: string | number; - score: number; + } & SubAggregateOf; + percentiles: { + values: TAggregationContainer extends Record + ? Array<{ key: number; value: number | null }> + : Record; + }; + percentile_ranks: { + values: TAggregationContainer extends Record + ? Array<{ key: number; value: number | null }> + : Record; + }; + percentiles_bucket: { + values: TAggregationContainer extends Record + ? Array<{ key: number; value: number | null }> + : Record; + }; + range: MaybeKeyed< + TAggregationContainer, + { + key: string; + from?: number; + from_as_string?: string; + to?: number; + to_as_string?: string; + doc_count: number; + }, + TAggregationContainer extends { range: { ranges: Array } } + ? TRangeType extends { key: infer TKeys } + ? TKeys + : string + : string + >; + rare_terms: Array< + { + key: string | number; + doc_count: number; + } & SubAggregateOf + >; + rate: { + value: number | null; + }; + reverse_nested: { + doc_count: number; + } & SubAggregateOf; + sampler: { + doc_count: number; + } & SubAggregateOf; + scripted_metric: { + value: unknown; + }; + serial_diff: { + value: number | null; + // TODO: should be perhaps based on input? ie when `format` is specified + value_as_string?: string; + }; + significant_terms: { doc_count: number; bg_count: number; - } & SubAggregateOf - >; - }; - significant_text: { - doc_count: number; - buckets: Array<{ - key: string; - doc_count: number; - score: number; - bg_count: number; - }>; - }; - stats: { - count: number; - min: number | null; - max: number | null; - avg: number | null; - sum: number; - } & ( - | { - min_as_string: string; - max_as_string: string; - avg_as_string: string; - sum_as_string: string; - } - | {} - ); - stats_bucket: { - count: number; - min: number | null; - max: number | null; - avg: number | null; - sum: number; - }; - string_stats: { - count: number; - min_length: number | null; - max_length: number | null; - avg_length: number | null; - entropy: number | null; - distribution: Record; - }; - sum: { - value: number | null; - value_as_string?: string; - }; - sum_bucket: { - value: number | null; - }; - terms: { - doc_count_error_upper_bound: number; - sum_other_doc_count: number; - buckets: Array< - { + buckets: Array< + { + key: string | number; + score: number; + doc_count: number; + bg_count: number; + } & SubAggregateOf + >; + }; + significant_text: { doc_count: number; - key: string | number; - } & SubAggregateOf - >; - }; - top_hits: { - hits: { - total: { + buckets: Array<{ + key: string; + doc_count: number; + score: number; + bg_count: number; + }>; + }; + stats: { + count: number; + min: number | null; + max: number | null; + avg: number | null; + sum: number; + } & ( + | { + min_as_string: string; + max_as_string: string; + avg_as_string: string; + sum_as_string: string; + } + | {} + ); + stats_bucket: { + count: number; + min: number | null; + max: number | null; + avg: number | null; + sum: number; + }; + string_stats: { + count: number; + min_length: number | null; + max_length: number | null; + avg_length: number | null; + entropy: number | null; + distribution: Record; + }; + sum: { + value: number | null; + value_as_string?: string; + }; + sum_bucket: { + value: number | null; + }; + terms: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: Array< + { + doc_count: number; + key: string | number; + } & SubAggregateOf + >; + }; + top_hits: { + hits: { + total: { + value: number; + relation: 'eq' | 'gte'; + }; + max_score: number | null; + hits: TAggregationContainer extends { top_hits: estypes.AggregationsTopHitsAggregation } + ? HitsOf + : estypes.SearchHitsMetadata; + }; + }; + top_metrics: { + top: Array<{ + sort: number[] | string[]; + metrics: Record, string | number | null>; + }>; + }; + weighted_avg: { value: number | null }; + value_count: { value: number; - relation: 'eq' | 'gte'; }; - max_score: number | null; - hits: TAggregationContainer extends { top_hits: estypes.AggregationsTopHitsAggregation } - ? HitsOf - : estypes.SearchHitsMetadata; - }; - }; - top_metrics: { - top: Array<{ - sort: number[] | string[]; - metrics: Record, string | number | null>; - }>; - }; - weighted_avg: { value: number | null }; - value_count: { - value: number; - }; - // t_test: {} not defined -})[ValidAggregationKeysOf & AggregationTypeName]; + // t_test: {} not defined + }, + Exclude, 'aggs' | 'aggregations'> & string + > +>; type AggregateOfMap = { - [TAggregationName in keyof TAggregationMap]: Required[TAggregationName] extends estypes.AggregationsAggregationContainer + [TAggregationName in keyof TAggregationMap]: Required[TAggregationName] extends AggregationsAggregationContainer ? AggregateOf : never; // using never means we effectively ignore optional keys, using {} creates a union type of { ... } | {} }; diff --git a/src/dev/build/lib/integration_tests/version_info.test.ts b/src/dev/build/lib/integration_tests/version_info.test.ts index e7a3a04c04734..9385de6e00a4f 100644 --- a/src/dev/build/lib/integration_tests/version_info.test.ts +++ b/src/dev/build/lib/integration_tests/version_info.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { kibanaPackageJson as pkg } from '@kbn/dev-utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import { getVersionInfo } from '../version_info'; 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/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts index 02b469820f900..cc1ffb5f3e301 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts @@ -10,7 +10,8 @@ import { resolve } from 'path'; import { readFileSync } from 'fs'; import { copyFile } from 'fs/promises'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import Mustache from 'mustache'; import { compressTar, copyAll, mkdirp, write, Config } from '../../../lib'; diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 895c42ad5f47d..a7d8fe684ef95 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -354,6 +354,7 @@ kibana_vars=( xpack.security.showInsecureClusterWarning xpack.securitySolution.alertMergeStrategy xpack.securitySolution.alertIgnoreFields + xpack.securitySolution.maxExceptionsImportSize xpack.securitySolution.maxRuleImportExportSize xpack.securitySolution.maxRuleImportPayloadBytes xpack.securitySolution.maxTimelineImportExportSize diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 6a192baed3fa3..085b4393caa66 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -10,7 +10,8 @@ import { access, link, unlink, chmod } from 'fs'; import { resolve, basename } from 'path'; import { promisify } from 'util'; -import { ToolingLog, kibanaPackageJson } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { kibanaPackageJson } from '@kbn/utils'; import { write, copyAll, mkdirp, exec, Config, Build } from '../../../lib'; import * as dockerTemplates from './templates'; diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile index dbdace85eda01..e9a6ef3539692 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile @@ -2,9 +2,9 @@ # Build stage 0 # Extract Kibana and make various file manipulations. ################################################################################ -ARG BASE_REGISTRY=registry1.dsop.io +ARG BASE_REGISTRY=registry1.dso.mil ARG BASE_IMAGE=redhat/ubi/ubi8 -ARG BASE_TAG=8.4 +ARG BASE_TAG=8.5 FROM ${BASE_REGISTRY}/${BASE_IMAGE}:${BASE_TAG} as prep_files diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yaml b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yaml index 24614039e5eb7..1c7926c2fcbc2 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yaml +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yaml @@ -14,7 +14,7 @@ tags: # Build args passed to Dockerfile ARGs args: BASE_IMAGE: 'redhat/ubi/ubi8' - BASE_TAG: '8.4' + BASE_TAG: '8.5' # Docker image labels labels: @@ -59,4 +59,4 @@ maintainers: - email: "yalabe.dukuly@anchore.com" name: "Yalabe Dukuly" username: "yalabe.dukuly" - cht_member: true \ No newline at end of file + cht_member: true diff --git a/src/dev/chromium_version.ts b/src/dev/chromium_version.ts index 410fcc72fbc0f..1f55330a92bb6 100644 --- a/src/dev/chromium_version.ts +++ b/src/dev/chromium_version.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { run, REPO_ROOT, ToolingLog } from '@kbn/dev-utils'; +import { run, ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import chalk from 'chalk'; import cheerio from 'cheerio'; import fs from 'fs'; 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..40d36ed46ea34 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 @@ -7,7 +7,8 @@ */ import { enumeratePatterns } from '../team_assignment/enumerate_patterns'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; const log = new ToolingLog({ level: 'info', @@ -15,16 +16,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/dev/code_coverage/ingest_coverage/team_assignment/index.js b/src/dev/code_coverage/ingest_coverage/team_assignment/index.js index 0e341a3aac1dc..a38c4ee50b40a 100644 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/index.js +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/index.js @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { run, createFlagError, REPO_ROOT } from '@kbn/dev-utils'; +import { run, createFlagError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { parse } from './parse_owners'; import { flush } from './flush'; import { enumeratePatterns } from './enumerate_patterns'; diff --git a/src/dev/ensure_all_tests_in_ci_group.ts b/src/dev/ensure_all_tests_in_ci_group.ts index aeccefae05d2c..a2d9729d3352b 100644 --- a/src/dev/ensure_all_tests_in_ci_group.ts +++ b/src/dev/ensure_all_tests_in_ci_group.ts @@ -12,7 +12,8 @@ import Fs from 'fs/promises'; import execa from 'execa'; import { safeLoad } from 'js-yaml'; -import { run, REPO_ROOT } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { schema } from '@kbn/config-schema'; const RELATIVE_JOBS_YAML_PATH = '.ci/ci_groups.yml'; diff --git a/src/dev/eslint/run_eslint_with_types.ts b/src/dev/eslint/run_eslint_with_types.ts index 750011dea1031..0f2a10d07d681 100644 --- a/src/dev/eslint/run_eslint_with_types.ts +++ b/src/dev/eslint/run_eslint_with_types.ts @@ -14,7 +14,8 @@ import execa from 'execa'; import * as Rx from 'rxjs'; import { mergeMap, reduce } from 'rxjs/operators'; import { supportsColor } from 'chalk'; -import { REPO_ROOT, run, createFailError } from '@kbn/dev-utils'; +import { run, createFailError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { lastValueFrom } from '@kbn/std'; import { PROJECTS } from '../typescript/projects'; diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index 52b1f816090df..9674694c0d655 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -76,6 +76,6 @@ export const LICENSE_OVERRIDES = { 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint '@elastic/ems-client@8.0.0': ['Elastic License 2.0'], - '@elastic/eui@41.0.0': ['SSPL-1.0 OR Elastic License 2.0'], + '@elastic/eui@41.2.3': ['SSPL-1.0 OR Elastic License 2.0'], 'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry }; diff --git a/src/dev/plugin_discovery/find_plugins.ts b/src/dev/plugin_discovery/find_plugins.ts index f1725f34d1f8e..53a53bc08e15b 100644 --- a/src/dev/plugin_discovery/find_plugins.ts +++ b/src/dev/plugin_discovery/find_plugins.ts @@ -8,11 +8,9 @@ import Path from 'path'; import { getPluginSearchPaths } from '@kbn/config'; -import { - KibanaPlatformPlugin, - REPO_ROOT, - simpleKibanaPlatformPluginDiscovery, -} from '@kbn/dev-utils'; +import { KibanaPlatformPlugin, simpleKibanaPlatformPluginDiscovery } from '@kbn/dev-utils'; + +import { REPO_ROOT } from '@kbn/utils'; export interface SearchOptions { oss: boolean; diff --git a/src/dev/run_build_docs_cli.ts b/src/dev/run_build_docs_cli.ts index aad524b4437d3..8ee75912c1a7e 100644 --- a/src/dev/run_build_docs_cli.ts +++ b/src/dev/run_build_docs_cli.ts @@ -9,7 +9,8 @@ import Path from 'path'; import dedent from 'dedent'; -import { run, REPO_ROOT, createFailError } from '@kbn/dev-utils'; +import { run, createFailError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; const DEFAULT_DOC_REPO_PATH = Path.resolve(REPO_ROOT, '..', 'docs'); diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts index f7974b464fcaf..f9ee7bd84c54f 100644 --- a/src/dev/run_find_plugins_with_circular_deps.ts +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -10,7 +10,8 @@ import dedent from 'dedent'; import { parseDependencyTree, parseCircular, prettyCircular } from 'dpdm'; import { relative } from 'path'; import { getPluginSearchPaths } from '@kbn/config'; -import { REPO_ROOT, run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; interface Options { debug?: boolean; diff --git a/src/dev/run_precommit_hook.js b/src/dev/run_precommit_hook.js index a7bd0a9f57f6e..dfa3a94426bb2 100644 --- a/src/dev/run_precommit_hook.js +++ b/src/dev/run_precommit_hook.js @@ -8,7 +8,8 @@ import SimpleGit from 'simple-git/promise'; -import { run, combineErrors, createFlagError, REPO_ROOT } from '@kbn/dev-utils'; +import { run, combineErrors, createFlagError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import * as Eslint from './eslint'; import * as Stylelint from './stylelint'; import { getFilesForCommit, checkFileCasing } from './precommit_hook'; diff --git a/src/dev/typescript/build_ts_refs.ts b/src/dev/typescript/build_ts_refs.ts index aaa8c0d12fa4d..f3896cf676e27 100644 --- a/src/dev/typescript/build_ts_refs.ts +++ b/src/dev/typescript/build_ts_refs.ts @@ -8,7 +8,8 @@ import Path from 'path'; -import { ToolingLog, REPO_ROOT, ProcRunner } from '@kbn/dev-utils'; +import { ToolingLog, ProcRunner } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { ROOT_REFS_CONFIG_PATH } from './root_refs_config'; import { Project } from './project'; diff --git a/src/dev/typescript/build_ts_refs_cli.ts b/src/dev/typescript/build_ts_refs_cli.ts index c68424c2a98f7..09866315fc8dd 100644 --- a/src/dev/typescript/build_ts_refs_cli.ts +++ b/src/dev/typescript/build_ts_refs_cli.ts @@ -8,7 +8,8 @@ import Path from 'path'; -import { run, REPO_ROOT, createFlagError } from '@kbn/dev-utils'; +import { run, createFlagError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import del from 'del'; import { RefOutputCache } from './ref_output_cache'; diff --git a/src/dev/typescript/ref_output_cache/ref_output_cache.ts b/src/dev/typescript/ref_output_cache/ref_output_cache.ts index b7e641ceb33d5..32b08ec1ba0df 100644 --- a/src/dev/typescript/ref_output_cache/ref_output_cache.ts +++ b/src/dev/typescript/ref_output_cache/ref_output_cache.ts @@ -9,7 +9,8 @@ import Path from 'path'; import Fs from 'fs/promises'; -import { ToolingLog, kibanaPackageJson, extract } from '@kbn/dev-utils'; +import { ToolingLog, extract } from '@kbn/dev-utils'; +import { kibanaPackageJson } from '@kbn/utils'; import del from 'del'; import tempy from 'tempy'; diff --git a/src/dev/typescript/root_refs_config.ts b/src/dev/typescript/root_refs_config.ts index f4aa88f1ea6b2..e20b1ab46cd82 100644 --- a/src/dev/typescript/root_refs_config.ts +++ b/src/dev/typescript/root_refs_config.ts @@ -10,7 +10,8 @@ import Path from 'path'; import Fs from 'fs/promises'; import dedent from 'dedent'; -import { REPO_ROOT, ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import normalize from 'normalize-path'; import { PROJECTS } from './projects'; diff --git a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss index 6e1afd91c476d..fb004dfce4ec0 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss +++ b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss @@ -9,14 +9,6 @@ padding: $euiSizeS; } -.heatmap-chart__empty { - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - .heatmap-chart-icon__subdued { fill: $euiTextSubduedColor; } diff --git a/src/plugins/charts/public/static/components/empty_placeholder.scss b/src/plugins/charts/public/static/components/empty_placeholder.scss new file mode 100644 index 0000000000000..3f98da9eecb6a --- /dev/null +++ b/src/plugins/charts/public/static/components/empty_placeholder.scss @@ -0,0 +1,7 @@ +.chart__empty-placeholder { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/src/plugins/charts/public/static/components/empty_placeholder.tsx b/src/plugins/charts/public/static/components/empty_placeholder.tsx index db3f3fb6739d5..e376120c9cd9e 100644 --- a/src/plugins/charts/public/static/components/empty_placeholder.tsx +++ b/src/plugins/charts/public/static/components/empty_placeholder.tsx @@ -9,15 +9,20 @@ import React from 'react'; import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import './empty_placeholder.scss'; -export const EmptyPlaceholder = (props: { icon: IconType }) => ( +export const EmptyPlaceholder = ({ + icon, + message = , +}: { + icon: IconType; + message?: JSX.Element; +}) => ( <> - - + + -

- -

+

{message}

); diff --git a/src/plugins/console/public/application/contexts/services_context.mock.ts b/src/plugins/console/public/application/contexts/services_context.mock.ts index c19413bdd0413..90a5d9ddce010 100644 --- a/src/plugins/console/public/application/contexts/services_context.mock.ts +++ b/src/plugins/console/public/application/contexts/services_context.mock.ts @@ -7,7 +7,7 @@ */ import { notificationServiceMock } from '../../../../../core/public/mocks'; -import { httpServiceMock } from '../../../../../core/public/mocks'; +import { httpServiceMock, themeServiceMock } from '../../../../../core/public/mocks'; import type { ObjectStorageClient } from '../../../common/types'; import { HistoryMock } from '../../services/history.mock'; @@ -35,6 +35,7 @@ export const serviceContextMock = { objectStorageClient: {} as unknown as ObjectStorageClient, }, docLinkVersion: 'NA', + theme$: themeServiceMock.create().start().theme$, }; }, }; diff --git a/src/plugins/console/public/application/contexts/services_context.tsx b/src/plugins/console/public/application/contexts/services_context.tsx index 53c021d4d0982..5912de0375590 100644 --- a/src/plugins/console/public/application/contexts/services_context.tsx +++ b/src/plugins/console/public/application/contexts/services_context.tsx @@ -7,7 +7,9 @@ */ import React, { createContext, useContext, useEffect } from 'react'; -import { NotificationsSetup } from 'kibana/public'; +import { Observable } from 'rxjs'; +import { NotificationsSetup, CoreTheme } from 'kibana/public'; + import { History, Settings, Storage } from '../../services'; import { ObjectStorageClient } from '../../../common/types'; import { MetricsTracker } from '../../types'; @@ -26,6 +28,7 @@ interface ContextServices { export interface ContextValue { services: ContextServices; docLinkVersion: string; + theme$: Observable; } interface ContextProps { diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts index d025760c19d0a..81aa571b45a20 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts @@ -8,20 +8,21 @@ import { i18n } from '@kbn/i18n'; import { useCallback } from 'react'; + +import { toMountPoint } from '../../../shared_imports'; import { isQuotaExceededError } from '../../../services/history'; +// @ts-ignore +import { retrieveAutoCompleteInfo } from '../../../lib/mappings/mappings'; import { instance as registry } from '../../contexts/editor_context/editor_registry'; import { useRequestActionContext, useServicesContext } from '../../contexts'; +import { StorageQuotaError } from '../../components/storage_quota_error'; import { sendRequestToES } from './send_request_to_es'; import { track } from './track'; -import { toMountPoint } from '../../../../../kibana_react/public'; - -// @ts-ignore -import { retrieveAutoCompleteInfo } from '../../../lib/mappings/mappings'; -import { StorageQuotaError } from '../../components/storage_quota_error'; export const useSendCurrentRequestToES = () => { const { services: { history, settings, notifications, trackUiMetric }, + theme$, } = useServicesContext(); const dispatch = useRequestActionContext(); @@ -83,7 +84,8 @@ export const useSendCurrentRequestToES = () => { settings.setHistoryDisabled(true); notifications.toasts.remove(toast); }, - }) + }), + { theme$ } ), }); } else { @@ -127,5 +129,5 @@ export const useSendCurrentRequestToES = () => { }); } } - }, [dispatch, settings, history, notifications, trackUiMetric]); + }, [dispatch, settings, history, notifications, trackUiMetric, theme$]); }; diff --git a/src/plugins/console/public/application/index.tsx b/src/plugins/console/public/application/index.tsx index 0b41095f8cc19..719975874cd44 100644 --- a/src/plugins/console/public/application/index.tsx +++ b/src/plugins/console/public/application/index.tsx @@ -8,13 +8,16 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { HttpSetup, NotificationsSetup, I18nStart } from 'src/core/public'; -import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts'; -import { Main } from './containers'; +import { Observable } from 'rxjs'; +import { HttpSetup, NotificationsSetup, I18nStart, CoreTheme } from 'src/core/public'; + +import { UsageCollectionSetup } from '../../../usage_collection/public'; +import { KibanaThemeProvider } from '../shared_imports'; import { createStorage, createHistory, createSettings } from '../services'; -import * as localStorageObjectClient from '../lib/local_storage_object_client'; import { createUsageTracker } from '../services/tracker'; -import { UsageCollectionSetup } from '../../../usage_collection/public'; +import * as localStorageObjectClient from '../lib/local_storage_object_client'; +import { Main } from './containers'; +import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts'; import { createApi, createEsHostService } from './lib'; export interface BootDependencies { @@ -24,6 +27,7 @@ export interface BootDependencies { notifications: NotificationsSetup; usageCollection?: UsageCollectionSetup; element: HTMLElement; + theme$: Observable; } export function renderApp({ @@ -33,6 +37,7 @@ export function renderApp({ usageCollection, element, http, + theme$, }: BootDependencies) { const trackUiMetric = createUsageTracker(usageCollection); trackUiMetric.load('opened_app'); @@ -49,26 +54,29 @@ export function renderApp({ render( - - - -
- - - + + + + +
+ + + + , element ); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js index 866e19a1d0d3e..efd7dbd088581 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js @@ -2022,22 +2022,37 @@ ace.define( }, // parses and returns the method method = function () { - const [first, ...rest] = text.split(' '); - text = first.toUpperCase() + rest.join(' '); - ch = ch.toUpperCase(); - switch (ch) { + case 'g': + next('g'); + next('e'); + next('t'); + return 'get'; case 'G': next('G'); next('E'); next('T'); return 'GET'; + case 'h': + next('h'); + next('e'); + next('a'); + next('d'); + return 'head'; case 'H': next('H'); next('E'); next('A'); next('D'); return 'HEAD'; + case 'd': + next('d'); + next('e'); + next('l'); + next('e'); + next('t'); + next('e'); + return 'delete'; case 'D': next('D'); next('E'); @@ -2046,6 +2061,22 @@ ace.define( next('T'); next('E'); return 'DELETE'; + case 'p': + next('p'); + switch (ch) { + case 'u': + next('u'); + next('t'); + return 'put'; + case 'o': + next('o'); + next('s'); + next('t'); + return 'post'; + default: + error('Unexpected \'' + ch + '\''); + } + break; case 'P': next('P'); switch (ch) { 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/console/public/plugin.ts b/src/plugins/console/public/plugin.ts index d61769c23dfe0..f46f60b485d55 100644 --- a/src/plugins/console/public/plugin.ts +++ b/src/plugins/console/public/plugin.ts @@ -52,7 +52,7 @@ export class ConsoleUIPlugin implements Plugin { + mount: async ({ element, theme$ }) => { const [core] = await getStartServices(); const { @@ -69,6 +69,7 @@ export class ConsoleUIPlugin implements Plugin { 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/lib/filter_utils.ts b/src/plugins/dashboard/public/application/lib/filter_utils.ts index a31b83ec2df8f..c6b9ae2d01cf3 100644 --- a/src/plugins/dashboard/public/application/lib/filter_utils.ts +++ b/src/plugins/dashboard/public/application/lib/filter_utils.ts @@ -72,7 +72,7 @@ export const cleanFiltersForComparison = (filters: Filter[]) => { export const cleanFiltersForSerialize = (filters: Filter[]): Filter[] => { return filters.map((filter) => { - if (filter.meta.value) { + if (filter.meta?.value) { delete filter.meta.value; } return filter; diff --git a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts index 31579e92bd1ec..03a03842c0e66 100644 --- a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts @@ -8,10 +8,10 @@ import _ from 'lodash'; +import { getDashboard60Warning, dashboardLoadingErrorStrings } from '../../dashboard_strings'; import { savedObjectToDashboardState } from './convert_dashboard_state'; import { DashboardState, DashboardBuildContext } from '../../types'; import { DashboardConstants, DashboardSavedObject } from '../..'; -import { getDashboard60Warning } from '../../dashboard_strings'; import { migrateLegacyQuery } from './migrate_legacy_query'; import { cleanFiltersForSerialize } from './filter_utils'; import { ViewMode } from '../../services/embeddable'; @@ -52,34 +52,33 @@ export const loadSavedDashboardState = async ({ return; } await indexPatterns.ensureDefaultDataView(); - let savedDashboard: DashboardSavedObject | undefined; try { - savedDashboard = (await savedDashboards.get({ + const savedDashboard = (await savedDashboards.get({ id: savedDashboardId, useResolve: true, })) as DashboardSavedObject; + const savedDashboardState = savedObjectToDashboardState({ + savedDashboard, + usageCollection, + showWriteControls, + savedObjectsTagging, + version: initializerContext.env.packageInfo.version, + }); + + const isViewMode = !showWriteControls || Boolean(savedDashboard.id); + savedDashboardState.viewMode = isViewMode ? ViewMode.VIEW : ViewMode.EDIT; + savedDashboardState.filters = cleanFiltersForSerialize(savedDashboardState.filters); + savedDashboardState.query = migrateLegacyQuery( + savedDashboardState.query || queryString.getDefaultQuery() + ); + + return { savedDashboardState, savedDashboard }; } catch (error) { // E.g. a corrupt or deleted dashboard - notifications.toasts.addDanger(error.message); + notifications.toasts.addDanger( + dashboardLoadingErrorStrings.getDashboardLoadError(error.message) + ); history.push(DashboardConstants.LANDING_PAGE_PATH); return; } - if (!savedDashboard) return; - - const savedDashboardState = savedObjectToDashboardState({ - savedDashboard, - usageCollection, - showWriteControls, - savedObjectsTagging, - version: initializerContext.env.packageInfo.version, - }); - - const isViewMode = !showWriteControls || Boolean(savedDashboard.id); - savedDashboardState.viewMode = isViewMode ? ViewMode.VIEW : ViewMode.EDIT; - savedDashboardState.filters = cleanFiltersForSerialize(savedDashboardState.filters); - savedDashboardState.query = migrateLegacyQuery( - savedDashboardState.query || queryString.getDefaultQuery() - ); - - return { savedDashboardState, savedDashboard }; }; 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/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index ca0f51976f3fb..52961c43cc1a2 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -359,6 +359,14 @@ export const panelStorageErrorStrings = { }), }; +export const dashboardLoadingErrorStrings = { + getDashboardLoadError: (message: string) => + i18n.translate('dashboard.loadingError.errorMessage', { + defaultMessage: 'Error encountered while loading saved dashboard: {message}', + values: { message }, + }), +}; + /* Empty Screen */ 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/data/common/search/aggs/agg_types.ts b/src/plugins/data/common/search/aggs/agg_types.ts index dd930887f9d19..a84fddb19c5fa 100644 --- a/src/plugins/data/common/search/aggs/agg_types.ts +++ b/src/plugins/data/common/search/aggs/agg_types.ts @@ -62,6 +62,8 @@ export const getAggTypes = () => ({ { name: BUCKET_TYPES.SIGNIFICANT_TERMS, fn: buckets.getSignificantTermsBucketAgg }, { name: BUCKET_TYPES.GEOHASH_GRID, fn: buckets.getGeoHashBucketAgg }, { name: BUCKET_TYPES.GEOTILE_GRID, fn: buckets.getGeoTitleBucketAgg }, + { name: BUCKET_TYPES.SAMPLER, fn: buckets.getSamplerBucketAgg }, + { name: BUCKET_TYPES.DIVERSIFIED_SAMPLER, fn: buckets.getDiversifiedSamplerBucketAgg }, ], }); @@ -79,6 +81,8 @@ export const getAggTypesFunctions = () => [ buckets.aggDateHistogram, buckets.aggTerms, buckets.aggMultiTerms, + buckets.aggSampler, + buckets.aggDiversifiedSampler, metrics.aggAvg, metrics.aggBucketAvg, metrics.aggBucketMax, diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index be3fbae26174a..571083c18156f 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -73,6 +73,8 @@ describe('Aggs service', () => { "significant_terms", "geohash_grid", "geotile_grid", + "sampler", + "diversified_sampler", "foo", ] `); @@ -122,6 +124,8 @@ describe('Aggs service', () => { "significant_terms", "geohash_grid", "geotile_grid", + "sampler", + "diversified_sampler", ] `); expect(bStart.types.getAll().metrics.map((t) => t(aggTypesDependencies).name)) diff --git a/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts b/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts index 0c01bff90bfee..671266ef15997 100644 --- a/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts +++ b/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts @@ -19,4 +19,6 @@ export enum BUCKET_TYPES { GEOHASH_GRID = 'geohash_grid', GEOTILE_GRID = 'geotile_grid', DATE_HISTOGRAM = 'date_histogram', + SAMPLER = 'sampler', + DIVERSIFIED_SAMPLER = 'diversified_sampler', } diff --git a/src/plugins/data/common/search/aggs/buckets/diversified_sampler.ts b/src/plugins/data/common/search/aggs/buckets/diversified_sampler.ts new file mode 100644 index 0000000000000..31ebaa094c368 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/diversified_sampler.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { BucketAggType } from './bucket_agg_type'; +import { BaseAggParams } from '../types'; +import { aggDiversifiedSamplerFnName } from './diversified_sampler_fn'; + +export const DIVERSIFIED_SAMPLER_AGG_NAME = 'diversified_sampler'; + +const title = i18n.translate('data.search.aggs.buckets.diversifiedSamplerTitle', { + defaultMessage: 'Diversified sampler', + description: 'Diversified sampler aggregation title', +}); + +export interface AggParamsDiversifiedSampler extends BaseAggParams { + /** + * Is used to provide values used for de-duplication + */ + field: string; + + /** + * Limits how many top-scoring documents are collected in the sample processed on each shard. + */ + shard_size?: number; + + /** + * Limits how many documents are permitted per choice of de-duplicating value + */ + max_docs_per_value?: number; +} + +/** + * Like the sampler aggregation this is a filtering aggregation used to limit any sub aggregations' processing to a sample of the top-scoring documents. + * The diversified_sampler aggregation adds the ability to limit the number of matches that share a common value. + */ +export const getDiversifiedSamplerBucketAgg = () => + new BucketAggType({ + name: DIVERSIFIED_SAMPLER_AGG_NAME, + title, + customLabels: false, + expressionName: aggDiversifiedSamplerFnName, + params: [ + { + name: 'shard_size', + type: 'number', + }, + { + name: 'max_docs_per_value', + type: 'number', + }, + { + name: 'field', + type: 'field', + }, + ], + }); diff --git a/src/plugins/data/common/search/aggs/buckets/diversified_sampler_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/diversified_sampler_fn.test.ts new file mode 100644 index 0000000000000..e874542289bb2 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/diversified_sampler_fn.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 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 { functionWrapper } from '../test_helpers'; +import { aggDiversifiedSampler } from './diversified_sampler_fn'; + +describe('aggDiversifiedSampler', () => { + const fn = functionWrapper(aggDiversifiedSampler()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ id: 'sampler', schema: 'bucket', field: 'author' }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": "sampler", + "params": Object { + "field": "author", + "max_docs_per_value": undefined, + "shard_size": undefined, + }, + "schema": "bucket", + "type": "diversified_sampler", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: 'sampler', + schema: 'bucket', + shard_size: 300, + field: 'author', + max_docs_per_value: 3, + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": "sampler", + "params": Object { + "field": "author", + "max_docs_per_value": 3, + "shard_size": 300, + }, + "schema": "bucket", + "type": "diversified_sampler", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/aggs/buckets/diversified_sampler_fn.ts b/src/plugins/data/common/search/aggs/buckets/diversified_sampler_fn.ts new file mode 100644 index 0000000000000..0e1b235dd576d --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/diversified_sampler_fn.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { AggExpressionFunctionArgs, AggExpressionType, BUCKET_TYPES } from '../'; +import { DIVERSIFIED_SAMPLER_AGG_NAME } from './diversified_sampler'; + +export const aggDiversifiedSamplerFnName = 'aggDiversifiedSampler'; + +type Input = any; +type Arguments = AggExpressionFunctionArgs; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggDiversifiedSamplerFnName, + Input, + Arguments, + Output +>; + +export const aggDiversifiedSampler = (): FunctionDefinition => ({ + name: aggDiversifiedSamplerFnName, + help: i18n.translate('data.search.aggs.function.buckets.diversifiedSampler.help', { + defaultMessage: 'Generates a serialized agg config for a Diversified sampler agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.diversifiedSampler.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.diversifiedSampler.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.diversifiedSampler.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + shard_size: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.diversifiedSampler.shardSize.help', { + defaultMessage: + 'The shard_size parameter limits how many top-scoring documents are collected in the sample processed on each shard.', + }), + }, + max_docs_per_value: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.diversifiedSampler.maxDocsPerValue.help', { + defaultMessage: + 'Limits how many documents are permitted per choice of de-duplicating value.', + }), + }, + field: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.diversifiedSampler.field.help', { + defaultMessage: 'Used to provide values used for de-duplication.', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: DIVERSIFIED_SAMPLER_AGG_NAME, + params: { + ...rest, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/common/search/aggs/buckets/index.ts b/src/plugins/data/common/search/aggs/buckets/index.ts index 421fa0fcfdaf4..bf96a9ef860c0 100644 --- a/src/plugins/data/common/search/aggs/buckets/index.ts +++ b/src/plugins/data/common/search/aggs/buckets/index.ts @@ -38,3 +38,7 @@ export * from './terms_fn'; export * from './terms'; export * from './multi_terms_fn'; export * from './multi_terms'; +export * from './sampler_fn'; +export * from './sampler'; +export * from './diversified_sampler_fn'; +export * from './diversified_sampler'; diff --git a/src/plugins/data/common/search/aggs/buckets/multi_terms.ts b/src/plugins/data/common/search/aggs/buckets/multi_terms.ts index c320c7e242798..02bf6bd12d319 100644 --- a/src/plugins/data/common/search/aggs/buckets/multi_terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/multi_terms.ts @@ -34,6 +34,7 @@ export interface AggParamsMultiTerms extends BaseAggParams { size?: number; otherBucket?: boolean; otherBucketLabel?: string; + separatorLabel?: string; } export const getMultiTermsBucketAgg = () => { @@ -83,6 +84,7 @@ export const getMultiTermsBucketAgg = () => { params: { otherBucketLabel: params.otherBucketLabel, paramsPerField: formats, + separator: agg.params.separatorLabel, }, }; }, @@ -142,6 +144,11 @@ export const getMultiTermsBucketAgg = () => { shouldShow: (agg) => agg.getParam('otherBucket'), write: noop, }, + { + name: 'separatorLabel', + type: 'string', + write: noop, + }, ], }); }; diff --git a/src/plugins/data/common/search/aggs/buckets/multi_terms_fn.ts b/src/plugins/data/common/search/aggs/buckets/multi_terms_fn.ts index 58e49479cd2c1..12b9c6d156548 100644 --- a/src/plugins/data/common/search/aggs/buckets/multi_terms_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/multi_terms_fn.ts @@ -111,6 +111,12 @@ export const aggMultiTerms = (): FunctionDefinition => ({ defaultMessage: 'Represents a custom label for this aggregation', }), }, + separatorLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.multiTerms.separatorLabel.help', { + defaultMessage: 'The separator label used to join each term combination', + }), + }, }, fn: (input, args) => { const { id, enabled, schema, ...rest } = args; diff --git a/src/plugins/data/common/search/aggs/buckets/sampler.ts b/src/plugins/data/common/search/aggs/buckets/sampler.ts new file mode 100644 index 0000000000000..7eb4f74115095 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/sampler.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { BucketAggType } from './bucket_agg_type'; +import { BaseAggParams } from '../types'; +import { aggSamplerFnName } from './sampler_fn'; + +export const SAMPLER_AGG_NAME = 'sampler'; + +const title = i18n.translate('data.search.aggs.buckets.samplerTitle', { + defaultMessage: 'Sampler', + description: 'Sampler aggregation title', +}); + +export interface AggParamsSampler extends BaseAggParams { + /** + * Limits how many top-scoring documents are collected in the sample processed on each shard. + */ + shard_size?: number; +} + +/** + * A filtering aggregation used to limit any sub aggregations' processing to a sample of the top-scoring documents. + */ +export const getSamplerBucketAgg = () => + new BucketAggType({ + name: SAMPLER_AGG_NAME, + title, + customLabels: false, + expressionName: aggSamplerFnName, + params: [ + { + name: 'shard_size', + type: 'number', + }, + ], + }); diff --git a/src/plugins/data/common/search/aggs/buckets/sampler_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/sampler_fn.test.ts new file mode 100644 index 0000000000000..76ef901671e72 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/sampler_fn.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggSampler } from './sampler_fn'; + +describe('aggSampler', () => { + const fn = functionWrapper(aggSampler()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ id: 'sampler', schema: 'bucket' }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": "sampler", + "params": Object { + "shard_size": undefined, + }, + "schema": "bucket", + "type": "sampler", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: 'sampler', + schema: 'bucket', + shard_size: 300, + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": "sampler", + "params": Object { + "shard_size": 300, + }, + "schema": "bucket", + "type": "sampler", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/aggs/buckets/sampler_fn.ts b/src/plugins/data/common/search/aggs/buckets/sampler_fn.ts new file mode 100644 index 0000000000000..2cb30eb70a230 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/sampler_fn.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { AggExpressionFunctionArgs, AggExpressionType, BUCKET_TYPES } from '../'; +import { SAMPLER_AGG_NAME } from './sampler'; + +export const aggSamplerFnName = 'aggSampler'; + +type Input = any; +type Arguments = AggExpressionFunctionArgs; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggSamplerFnName, + Input, + Arguments, + Output +>; + +export const aggSampler = (): FunctionDefinition => ({ + name: aggSamplerFnName, + help: i18n.translate('data.search.aggs.function.buckets.sampler.help', { + defaultMessage: 'Generates a serialized agg config for a Sampler agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.sampler.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.sampler.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.sampler.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + shard_size: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.sampler.shardSize.help', { + defaultMessage: + 'The shard_size parameter limits how many top-scoring documents are collected in the sample processed on each shard.', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: SAMPLER_AGG_NAME, + params: { + ...rest, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index b9a977e0a8a09..9c4866c19714d 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -90,6 +90,8 @@ import { aggFilteredMetric, aggSinglePercentile, } from './'; +import { AggParamsSampler } from './buckets/sampler'; +import { AggParamsDiversifiedSampler } from './buckets/diversified_sampler'; export type { IAggConfig, AggConfigSerialized } from './agg_config'; export type { CreateAggConfigParams, IAggConfigs } from './agg_configs'; @@ -166,6 +168,8 @@ export interface AggParamsMapping { [BUCKET_TYPES.DATE_HISTOGRAM]: AggParamsDateHistogram; [BUCKET_TYPES.TERMS]: AggParamsTerms; [BUCKET_TYPES.MULTI_TERMS]: AggParamsMultiTerms; + [BUCKET_TYPES.SAMPLER]: AggParamsSampler; + [BUCKET_TYPES.DIVERSIFIED_SAMPLER]: AggParamsDiversifiedSampler; [METRIC_TYPES.AVG]: AggParamsAvg; [METRIC_TYPES.CARDINALITY]: AggParamsCardinality; [METRIC_TYPES.COUNT]: BaseAggParams; diff --git a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.test.ts b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.test.ts index 76112980c55fb..8510acf1572c7 100644 --- a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.test.ts +++ b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.test.ts @@ -13,6 +13,7 @@ import { IFieldFormat, SerializedFieldFormat, } from '../../../../../field_formats/common'; +import { MultiFieldKey } from '../buckets/multi_field_key'; import { getAggsFormats } from './get_aggs_formats'; const getAggFormat = ( @@ -119,4 +120,35 @@ describe('getAggsFormats', () => { expect(format.convert('__missing__')).toBe(mapping.params.missingBucketLabel); expect(getFormat).toHaveBeenCalledTimes(3); }); + + test('uses a default separator for multi terms', () => { + const terms = ['source', 'geo.src', 'geo.dest']; + const mapping = { + id: 'multi_terms', + params: { + paramsPerField: Array(terms.length).fill({ id: 'terms' }), + }, + }; + + const format = getAggFormat(mapping, getFormat); + + expect(format.convert(new MultiFieldKey({ key: terms }))).toBe('source › geo.src › geo.dest'); + expect(getFormat).toHaveBeenCalledTimes(terms.length); + }); + + test('uses a custom separator for multi terms when passed', () => { + const terms = ['source', 'geo.src', 'geo.dest']; + const mapping = { + id: 'multi_terms', + params: { + paramsPerField: Array(terms.length).fill({ id: 'terms' }), + separator: ' - ', + }, + }; + + const format = getAggFormat(mapping, getFormat); + + expect(format.convert(new MultiFieldKey({ key: terms }))).toBe('source - geo.src - geo.dest'); + expect(getFormat).toHaveBeenCalledTimes(terms.length); + }); }); diff --git a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts index aade8bc70e4ee..f14f981fdec65 100644 --- a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts +++ b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts @@ -143,9 +143,11 @@ export function getAggsFormats(getFieldFormat: GetFieldFormat): FieldFormatInsta return params.otherBucketLabel; } + const joinTemplate = params.separator ?? ' › '; + return (val as MultiFieldKey).keys .map((valPart, i) => formats[i].convert(valPart, type)) - .join(' › '); + .join(joinTemplate); }; getConverterFor = (type: FieldFormatsContentType) => (val: string) => this.convert(val, type); }, diff --git a/src/plugins/data/common/search/search_source/extract_references.ts b/src/plugins/data/common/search/search_source/extract_references.ts index de32836ced124..954d336cb8a92 100644 --- a/src/plugins/data/common/search/search_source/extract_references.ts +++ b/src/plugins/data/common/search/search_source/extract_references.ts @@ -14,7 +14,7 @@ import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../../data/common'; export const extractReferences = ( state: SerializedSearchSourceFields -): [SerializedSearchSourceFields & { indexRefName?: string }, SavedObjectReference[]] => { +): [SerializedSearchSourceFields, SavedObjectReference[]] => { let searchSourceFields: SerializedSearchSourceFields & { indexRefName?: string } = { ...state }; const references: SavedObjectReference[] = []; if (searchSourceFields.index) { diff --git a/src/plugins/data/common/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts index dee5c09a6b858..77ba2a761fbf0 100644 --- a/src/plugins/data/common/search/search_source/mocks.ts +++ b/src/plugins/data/common/search/search_source/mocks.ts @@ -40,6 +40,10 @@ export const searchSourceInstanceMock: MockedKeys = { export const searchSourceCommonMock: jest.Mocked = { create: jest.fn().mockReturnValue(searchSourceInstanceMock), createEmpty: jest.fn().mockReturnValue(searchSourceInstanceMock), + telemetry: jest.fn(), + getAllMigrations: jest.fn(), + inject: jest.fn(), + extract: jest.fn(), }; export const createSearchSourceMock = (fields?: SearchSourceFields, response?: any) => diff --git a/src/plugins/data/common/search/search_source/search_source_service.test.ts b/src/plugins/data/common/search/search_source/search_source_service.test.ts index dc63b96d5258d..a1b49fc433925 100644 --- a/src/plugins/data/common/search/search_source/search_source_service.test.ts +++ b/src/plugins/data/common/search/search_source/search_source_service.test.ts @@ -28,7 +28,14 @@ describe('SearchSource service', () => { dependencies ); - expect(Object.keys(start)).toEqual(['create', 'createEmpty']); + expect(Object.keys(start)).toEqual([ + 'create', + 'createEmpty', + 'extract', + 'inject', + 'getAllMigrations', + 'telemetry', + ]); }); }); }); diff --git a/src/plugins/data/common/search/search_source/search_source_service.ts b/src/plugins/data/common/search/search_source/search_source_service.ts index 886420365f548..a97596d322ccd 100644 --- a/src/plugins/data/common/search/search_source/search_source_service.ts +++ b/src/plugins/data/common/search/search_source/search_source_service.ts @@ -6,8 +6,18 @@ * Side Public License, v 1. */ -import { createSearchSource, SearchSource, SearchSourceDependencies } from './'; +import { mapValues } from 'lodash'; +import { + createSearchSource, + extractReferences, + injectReferences, + SearchSource, + SearchSourceDependencies, + SerializedSearchSourceFields, +} from './'; import { IndexPatternsContract } from '../..'; +import { mergeMigrationFunctionMaps } from '../../../../kibana_utils/common'; +import { getAllMigrations as filtersGetAllMigrations } from '../../query/persistable_state'; export class SearchSourceService { public setup() {} @@ -24,6 +34,28 @@ export class SearchSourceService { createEmpty: () => { return new SearchSource({}, dependencies); }, + extract: (state: SerializedSearchSourceFields) => { + const [newState, references] = extractReferences(state); + return { state: newState, references }; + }, + inject: injectReferences, + getAllMigrations: () => { + const searchSourceMigrations = {}; + + // we don't know if embeddables have any migrations defined so we need to fetch them and map the received functions so we pass + // them the correct input and that we correctly map the response + const filterMigrations = mapValues(filtersGetAllMigrations(), (migrate) => { + return (state: SerializedSearchSourceFields) => ({ + ...state, + filter: migrate(state.filter), + }); + }); + + return mergeMigrationFunctionMaps(searchSourceMigrations, filterMigrations); + }, + telemetry: () => { + return {}; + }, }; } diff --git a/src/plugins/data/common/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts index acfdf17263169..d496c109f68c8 100644 --- a/src/plugins/data/common/search/search_source/types.ts +++ b/src/plugins/data/common/search/search_source/types.ts @@ -13,6 +13,7 @@ import { Query } from '../..'; import { Filter } from '../../es_query'; import { IndexPattern } from '../..'; import { SearchSource } from './search_source'; +import { PersistableStateService } from '../../../../kibana_utils/common'; /** * search source interface @@ -24,7 +25,8 @@ export type ISearchSource = Pick; * high level search service * @public */ -export interface ISearchStartSearchSource { +export interface ISearchStartSearchSource + extends PersistableStateService { /** * creates {@link SearchSource} based on provided serialized {@link SearchSourceFields} * @param fields @@ -43,15 +45,17 @@ export enum SortDirection { desc = 'desc', } -export interface SortDirectionFormat { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type SortDirectionFormat = { order: SortDirection; format?: string; -} +}; -export interface SortDirectionNumeric { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type SortDirectionNumeric = { order: SortDirection; numeric_type?: 'double' | 'long' | 'date' | 'date_nanos'; -} +}; export type EsQuerySortValue = Record< string, @@ -114,7 +118,8 @@ export interface SearchSourceFields { parent?: SearchSourceFields; } -export interface SerializedSearchSourceFields { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type SerializedSearchSourceFields = { type?: string; /** * {@link Query} @@ -159,7 +164,7 @@ export interface SerializedSearchSourceFields { terminate_after?: number; parent?: SerializedSearchSourceFields; -} +}; export interface SearchSourceOptions { callParentStartHandlers?: boolean; diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index 20e07360a68e5..c7df4354cc76b 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -53,7 +53,7 @@ describe('AggsService - public', () => { test('registers default agg types', () => { service.setup(setupDeps); const start = service.start(startDeps); - expect(start.types.getAll().buckets.length).toBe(12); + expect(start.types.getAll().buckets.length).toBe(14); expect(start.types.getAll().metrics.length).toBe(23); }); @@ -69,7 +69,7 @@ describe('AggsService - public', () => { ); const start = service.start(startDeps); - expect(start.types.getAll().buckets.length).toBe(13); + expect(start.types.getAll().buckets.length).toBe(15); expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); expect(start.types.getAll().metrics.length).toBe(24); expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap index b59756ef1e90e..3262ee70dff86 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap @@ -173,56 +173,74 @@ exports[`Inspector Data View component should render empty state 1`] = ` } > -
- -

- - No data available - -

-
- - -
- - -
-

- +

- The element did not provide any data. - -

+ + No data available + +

+ + + + +
+ + +
+

+ + The element did not provide any data. + +

+
+
+ +
- -
-
-
+
+
+ + diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx index 602db0cd55274..c5eaeb02c05cf 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx @@ -218,6 +218,7 @@ const ScriptFieldComponent = ({ existingConcreteFields, links }: Props) => { <> { indexPatterns = new IndexPatternsFetcher(esClient); }); it('Removes pattern without matching indices', async () => { + // first field caps request returns empty const result = await indexPatterns.validatePatternListActive(patternList); expect(result).toEqual(['b', 'c']); }); + it('Keeps matching and negating patterns', async () => { + // first field caps request returns empty + const result = await indexPatterns.validatePatternListActive(['-a', 'b', 'c']); + expect(result).toEqual(['-a', 'c']); + }); it('Returns all patterns when all match indices', async () => { esClient = { fieldCaps: jest.fn().mockResolvedValue(response), diff --git a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts index c054d547e956f..bceefac22e0f0 100644 --- a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts @@ -133,6 +133,10 @@ export class IndexPatternsFetcher { const result = await Promise.all( patternList .map(async (index) => { + // perserve negated patterns + if (index.startsWith('-')) { + return true; + } const searchResponse = await this.elasticsearchClient.fieldCaps({ index, fields: '_id', diff --git a/src/plugins/dev_tools/kibana.json b/src/plugins/dev_tools/kibana.json index 75a1e82f1d910..9b2ae8a3f995f 100644 --- a/src/plugins/dev_tools/kibana.json +++ b/src/plugins/dev_tools/kibana.json @@ -7,5 +7,6 @@ "name": "Stack Management", "githubTeam": "kibana-stack-management" }, - "requiredPlugins": ["urlForwarding"] + "requiredPlugins": ["urlForwarding"], + "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index a4fdaf28e0eb4..dc72cfda790d4 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -16,6 +16,7 @@ import { i18n } from '@kbn/i18n'; import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; import { ApplicationStart, ChromeStart, ScopedHistory, CoreTheme } from 'src/core/public'; +import { KibanaThemeProvider } from '../../kibana_react/public'; import type { DocTitleService, BreadcrumbService } from './services'; import { DevToolApp } from './dev_tool'; @@ -177,32 +178,34 @@ export function renderApp( ReactDOM.render( - - - {devTools - // Only create routes for devtools that are not disabled - .filter((devTool) => !devTool.isDisabled()) - .map((devTool) => ( - ( - - )} - /> - ))} - - - - - + + + + {devTools + // Only create routes for devtools that are not disabled + .filter((devTool) => !devTool.isDisabled()) + .map((devTool) => ( + ( + + )} + /> + ))} + + + + + + , element ); diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index 6a90ed42417e6..ec7657827d95b 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -97,4 +97,5 @@ export const discoverServiceMock = { storage: { get: jest.fn(), }, + addBasePath: jest.fn(), } as unknown as DiscoverServices; diff --git a/src/plugins/discover/public/application/context/context_app_route.tsx b/src/plugins/discover/public/application/context/context_app_route.tsx index dfc318021b93e..80feea833ec94 100644 --- a/src/plugins/discover/public/application/context/context_app_route.tsx +++ b/src/plugins/discover/public/application/context/context_app_route.tsx @@ -15,6 +15,7 @@ import { ContextApp } from './context_app'; import { getRootBreadcrumbs } from '../../utils/breadcrumbs'; import { LoadingIndicator } from '../../components/common/loading_indicator'; import { useIndexPattern } from '../../utils/use_index_pattern'; +import { useMainRouteBreadcrumb } from '../../utils/use_navigation_props'; export interface ContextAppProps { /** @@ -33,17 +34,18 @@ export function ContextAppRoute(props: ContextAppProps) { const { chrome } = services; const { indexPatternId, id } = useParams(); + const breadcrumb = useMainRouteBreadcrumb(); useEffect(() => { chrome.setBreadcrumbs([ - ...getRootBreadcrumbs(), + ...getRootBreadcrumbs(breadcrumb), { text: i18n.translate('discover.context.breadcrumb', { defaultMessage: 'Surrounding documents', }), }, ]); - }, [chrome]); + }, [chrome, breadcrumb]); const { indexPattern, error } = useIndexPattern(services.indexPatterns, indexPatternId); diff --git a/src/plugins/discover/public/application/doc/single_doc_route.tsx b/src/plugins/discover/public/application/doc/single_doc_route.tsx index e5ddb784b9080..0a5cc3a8a82b6 100644 --- a/src/plugins/discover/public/application/doc/single_doc_route.tsx +++ b/src/plugins/discover/public/application/doc/single_doc_route.tsx @@ -14,6 +14,7 @@ import { getRootBreadcrumbs } from '../../utils/breadcrumbs'; import { Doc } from './components/doc'; import { LoadingIndicator } from '../../components/common/loading_indicator'; import { useIndexPattern } from '../../utils/use_index_pattern'; +import { useMainRouteBreadcrumb } from '../../utils/use_navigation_props'; export interface SingleDocRouteProps { /** @@ -36,18 +37,19 @@ export function SingleDocRoute(props: SingleDocRouteProps) { const { chrome, timefilter } = services; const { indexPatternId, index } = useParams(); + const breadcrumb = useMainRouteBreadcrumb(); const query = useQuery(); const docId = query.get('id') || ''; useEffect(() => { chrome.setBreadcrumbs([ - ...getRootBreadcrumbs(), + ...getRootBreadcrumbs(breadcrumb), { text: `${index}#${docId}`, }, ]); - }, [chrome, index, docId]); + }, [chrome, index, docId, breadcrumb]); useEffect(() => { timefilter.disableAutoRefreshSelector(); diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts index 9f17054de18d4..a2dae5cc99b7d 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -6,23 +6,66 @@ * Side Public License, v 1. */ import { FetchStatus } from '../../types'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { reduce } from 'rxjs/operators'; +import { SearchSource } from '../../../../../data/common'; import { RequestAdapter } from '../../../../../inspector'; import { savedSearchMock } from '../../../__mocks__/saved_search'; import { ReduxLikeStateContainer } from '../../../../../kibana_utils/common'; import { AppState } from '../services/discover_state'; import { discoverServiceMock } from '../../../__mocks__/services'; import { fetchAll } from './fetch_all'; +import { + DataChartsMessage, + DataDocumentsMsg, + DataMainMsg, + DataTotalHitsMsg, + SavedSearchData, +} from './use_saved_search'; + +import { fetchDocuments } from './fetch_documents'; +import { fetchChart } from './fetch_chart'; +import { fetchTotalHits } from './fetch_total_hits'; + +jest.mock('./fetch_documents', () => ({ + fetchDocuments: jest.fn().mockResolvedValue([]), +})); + +jest.mock('./fetch_chart', () => ({ + fetchChart: jest.fn(), +})); + +jest.mock('./fetch_total_hits', () => ({ + fetchTotalHits: jest.fn(), +})); + +const mockFetchDocuments = fetchDocuments as unknown as jest.MockedFunction; +const mockFetchTotalHits = fetchTotalHits as unknown as jest.MockedFunction; +const mockFetchChart = fetchChart as unknown as jest.MockedFunction; + +function subjectCollector(subject: Subject): () => Promise { + const promise = subject + .pipe(reduce((history, value) => history.concat([value]), [] as T[])) + .toPromise(); + + return () => { + subject.complete(); + return promise; + }; +} describe('test fetchAll', () => { - test('changes of fetchStatus when starting with FetchStatus.UNINITIALIZED', async (done) => { - const subjects = { - main$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - documents$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - totalHits$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - charts$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), + let subjects: SavedSearchData; + let deps: Parameters[3]; + let searchSource: SearchSource; + beforeEach(() => { + subjects = { + main$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), + documents$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), + totalHits$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), + charts$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), }; - const deps = { + deps = { appStateContainer: { getState: () => { return { interval: 'auto' }; @@ -31,29 +74,126 @@ describe('test fetchAll', () => { abortController: new AbortController(), data: discoverServiceMock.data, inspectorAdapters: { requests: new RequestAdapter() }, - onResults: jest.fn(), searchSessionId: '123', initialFetchStatus: FetchStatus.UNINITIALIZED, useNewFieldsApi: true, services: discoverServiceMock, }; + searchSource = savedSearchMock.searchSource.createChild(); + + mockFetchDocuments.mockReset().mockResolvedValue([]); + mockFetchTotalHits.mockReset().mockResolvedValue(42); + mockFetchChart + .mockReset() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockResolvedValue({ totalHits: 42, chartData: {} as any, bucketInterval: {} }); + }); + test('changes of fetchStatus when starting with FetchStatus.UNINITIALIZED', async () => { const stateArr: FetchStatus[] = []; subjects.main$.subscribe((value) => stateArr.push(value.fetchStatus)); - const parentSearchSource = savedSearchMock.searchSource; - const childSearchSource = parentSearchSource.createChild(); - - fetchAll(subjects, childSearchSource, false, deps).subscribe({ - complete: () => { - expect(stateArr).toEqual([ - FetchStatus.UNINITIALIZED, - FetchStatus.LOADING, - FetchStatus.COMPLETE, - ]); - done(); - }, - }); + await fetchAll(subjects, searchSource, false, deps); + + expect(stateArr).toEqual([ + FetchStatus.UNINITIALIZED, + FetchStatus.LOADING, + FetchStatus.COMPLETE, + ]); + }); + + test('emits loading and documents on documents$ correctly', async () => { + const collect = subjectCollector(subjects.documents$); + const hits = [ + { _id: '1', _index: 'logs' }, + { _id: '2', _index: 'logs' }, + ]; + mockFetchDocuments.mockResolvedValue(hits); + await fetchAll(subjects, searchSource, false, deps); + expect(await collect()).toEqual([ + { fetchStatus: FetchStatus.UNINITIALIZED }, + { fetchStatus: FetchStatus.LOADING }, + { fetchStatus: FetchStatus.COMPLETE, result: hits }, + ]); + }); + + test('emits loading and hit count on totalHits$ correctly', async () => { + const collect = subjectCollector(subjects.totalHits$); + const hits = [ + { _id: '1', _index: 'logs' }, + { _id: '2', _index: 'logs' }, + ]; + searchSource.getField('index')!.isTimeBased = () => false; + mockFetchDocuments.mockResolvedValue(hits); + mockFetchTotalHits.mockResolvedValue(42); + await fetchAll(subjects, searchSource, false, deps); + expect(await collect()).toEqual([ + { fetchStatus: FetchStatus.UNINITIALIZED }, + { fetchStatus: FetchStatus.LOADING }, + { fetchStatus: FetchStatus.PARTIAL, result: 2 }, + { fetchStatus: FetchStatus.COMPLETE, result: 42 }, + ]); + }); + + test('emits loading and chartData on charts$ correctly', async () => { + const collect = subjectCollector(subjects.charts$); + searchSource.getField('index')!.isTimeBased = () => true; + await fetchAll(subjects, searchSource, false, deps); + expect(await collect()).toEqual([ + { fetchStatus: FetchStatus.UNINITIALIZED }, + { fetchStatus: FetchStatus.LOADING }, + { fetchStatus: FetchStatus.COMPLETE, bucketInterval: {}, chartData: {} }, + ]); + }); + + test('should use charts query to fetch total hit count when chart is visible', async () => { + const collect = subjectCollector(subjects.totalHits$); + searchSource.getField('index')!.isTimeBased = () => true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockFetchChart.mockResolvedValue({ bucketInterval: {}, chartData: {} as any, totalHits: 32 }); + await fetchAll(subjects, searchSource, false, deps); + expect(await collect()).toEqual([ + { fetchStatus: FetchStatus.UNINITIALIZED }, + { fetchStatus: FetchStatus.LOADING }, + { fetchStatus: FetchStatus.PARTIAL, result: 0 }, // From documents query + { fetchStatus: FetchStatus.COMPLETE, result: 32 }, + ]); + expect(mockFetchTotalHits).not.toHaveBeenCalled(); + }); + + test('should only fail totalHits$ query not main$ for error from that query', async () => { + const collectTotalHits = subjectCollector(subjects.totalHits$); + const collectMain = subjectCollector(subjects.main$); + searchSource.getField('index')!.isTimeBased = () => false; + mockFetchTotalHits.mockRejectedValue({ msg: 'Oh noes!' }); + mockFetchDocuments.mockResolvedValue([{ _id: '1', _index: 'logs' }]); + await fetchAll(subjects, searchSource, false, deps); + expect(await collectTotalHits()).toEqual([ + { fetchStatus: FetchStatus.UNINITIALIZED }, + { fetchStatus: FetchStatus.LOADING }, + { fetchStatus: FetchStatus.PARTIAL, result: 1 }, + { fetchStatus: FetchStatus.ERROR, error: { msg: 'Oh noes!' } }, + ]); + expect(await collectMain()).toEqual([ + { fetchStatus: FetchStatus.UNINITIALIZED }, + { fetchStatus: FetchStatus.LOADING }, + { fetchStatus: FetchStatus.PARTIAL }, + { fetchStatus: FetchStatus.COMPLETE, foundDocuments: true }, + ]); + }); + + test('should not set COMPLETE if an ERROR has been set on main$', async () => { + const collectMain = subjectCollector(subjects.main$); + searchSource.getField('index')!.isTimeBased = () => false; + mockFetchDocuments.mockRejectedValue({ msg: 'This query failed' }); + await fetchAll(subjects, searchSource, false, deps); + expect(await collectMain()).toEqual([ + { fetchStatus: FetchStatus.UNINITIALIZED }, + { fetchStatus: FetchStatus.LOADING }, + { fetchStatus: FetchStatus.PARTIAL }, // From totalHits query + { fetchStatus: FetchStatus.ERROR, error: { msg: 'This query failed' } }, + // Here should be no COMPLETE coming anymore + ]); }); }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index 471616c9d4261..29279152ca321 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -5,11 +5,11 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { forkJoin, of } from 'rxjs'; import { sendCompleteMsg, sendErrorMsg, sendLoadingMsg, + sendNoResultsFoundMsg, sendPartialMsg, sendResetMsg, } from './use_saved_search_messages'; @@ -23,11 +23,25 @@ import { Adapters } from '../../../../../inspector'; import { AppState } from '../services/discover_state'; import { FetchStatus } from '../../types'; import { DataPublicPluginStart } from '../../../../../data/public'; -import { SavedSearchData } from './use_saved_search'; +import { + DataCharts$, + DataDocuments$, + DataMain$, + DataTotalHits$, + SavedSearchData, +} from './use_saved_search'; import { DiscoverServices } from '../../../build_services'; import { ReduxLikeStateContainer } from '../../../../../kibana_utils/common'; import { DataViewType } from '../../../../../data_views/common'; +/** + * This function starts fetching all required queries in Discover. This will be the query to load the individual + * documents, and depending on whether a chart is shown either the aggregation query to load the chart data + * or a query to retrieve just the total hits. + * + * This method returns a promise, which will resolve (without a value), as soon as all queries that have been started + * have been completed (failed or successfully). + */ export function fetchAll( dataSubjects: SavedSearchData, searchSource: ISearchSource, @@ -42,57 +56,137 @@ export function fetchAll( services: DiscoverServices; useNewFieldsApi: boolean; } -) { +): Promise { const { initialFetchStatus, appStateContainer, services, useNewFieldsApi, data } = fetchDeps; - const indexPattern = searchSource.getField('index')!; + /** + * Method to create a an error handler that will forward the received error + * to the specified subjects. It will ignore AbortErrors and will use the data + * plugin to show a toast for the error (e.g. allowing better insights into shard failures). + */ + const sendErrorTo = ( + ...errorSubjects: Array + ) => { + return (error: Error) => { + if (error instanceof Error && error.name === 'AbortError') { + return; + } - if (reset) { - sendResetMsg(dataSubjects, initialFetchStatus); - } + data.search.showError(error); + errorSubjects.forEach((subject) => sendErrorMsg(subject, error)); + }; + }; - sendLoadingMsg(dataSubjects.main$); - - const { hideChart, sort } = appStateContainer.getState(); - // Update the base searchSource, base for all child fetches - updateSearchSource(searchSource, false, { - indexPattern, - services, - sort: sort as SortOrder[], - useNewFieldsApi, - }); - - const subFetchDeps = { - ...fetchDeps, - onResults: (foundDocuments: boolean) => { - if (!foundDocuments) { - sendCompleteMsg(dataSubjects.main$, foundDocuments); - } else { + try { + const indexPattern = searchSource.getField('index')!; + + if (reset) { + sendResetMsg(dataSubjects, initialFetchStatus); + } + + const { hideChart, sort } = appStateContainer.getState(); + + // Update the base searchSource, base for all child fetches + updateSearchSource(searchSource, false, { + indexPattern, + services, + sort: sort as SortOrder[], + useNewFieldsApi, + }); + + // Mark all subjects as loading + sendLoadingMsg(dataSubjects.main$); + sendLoadingMsg(dataSubjects.documents$); + sendLoadingMsg(dataSubjects.totalHits$); + sendLoadingMsg(dataSubjects.charts$); + + const isChartVisible = + !hideChart && indexPattern.isTimeBased() && indexPattern.type !== DataViewType.ROLLUP; + + // Start fetching all required requests + const documents = fetchDocuments(searchSource.createCopy(), fetchDeps); + const charts = isChartVisible ? fetchChart(searchSource.createCopy(), fetchDeps) : undefined; + const totalHits = !isChartVisible + ? fetchTotalHits(searchSource.createCopy(), fetchDeps) + : undefined; + + /** + * This method checks the passed in hit count and will send a PARTIAL message to main$ + * if there are results, indicating that we have finished some of the requests that have been + * sent. If there are no results we already COMPLETE main$ with no results found, so Discover + * can show the "no results" screen. We know at that point, that the other query returning + * will neither carry any data, since there are no documents. + */ + const checkHitCount = (hitsCount: number) => { + if (hitsCount > 0) { sendPartialMsg(dataSubjects.main$); + } else { + sendNoResultsFoundMsg(dataSubjects.main$); } - }, - }; + }; - const isChartVisible = - !hideChart && indexPattern.isTimeBased() && indexPattern.type !== DataViewType.ROLLUP; - - const all = forkJoin({ - documents: fetchDocuments(dataSubjects, searchSource.createCopy(), subFetchDeps), - totalHits: !isChartVisible - ? fetchTotalHits(dataSubjects, searchSource.createCopy(), subFetchDeps) - : of(null), - chart: isChartVisible - ? fetchChart(dataSubjects, searchSource.createCopy(), subFetchDeps) - : of(null), - }); - - all.subscribe( - () => sendCompleteMsg(dataSubjects.main$, true), - (error) => { - if (error instanceof Error && error.name === 'AbortError') return; - data.search.showError(error); - sendErrorMsg(dataSubjects.main$, error); - } - ); - return all; + // Handle results of the individual queries and forward the results to the corresponding dataSubjects + + documents + .then((docs) => { + // If the total hits (or chart) query is still loading, emit a partial + // hit count that's at least our retrieved document count + if (dataSubjects.totalHits$.getValue().fetchStatus === FetchStatus.LOADING) { + dataSubjects.totalHits$.next({ + fetchStatus: FetchStatus.PARTIAL, + result: docs.length, + }); + } + + dataSubjects.documents$.next({ + fetchStatus: FetchStatus.COMPLETE, + result: docs, + }); + + checkHitCount(docs.length); + }) + // Only the document query should send its errors to main$, to cause the full Discover app + // to get into an error state. The other queries will not cause all of Discover to error out + // but their errors will be shown in-place (e.g. of the chart). + .catch(sendErrorTo(dataSubjects.documents$, dataSubjects.main$)); + + charts + ?.then((chart) => { + dataSubjects.totalHits$.next({ + fetchStatus: FetchStatus.COMPLETE, + result: chart.totalHits, + }); + + dataSubjects.charts$.next({ + fetchStatus: FetchStatus.COMPLETE, + chartData: chart.chartData, + bucketInterval: chart.bucketInterval, + }); + + checkHitCount(chart.totalHits); + }) + .catch(sendErrorTo(dataSubjects.charts$, dataSubjects.totalHits$)); + + totalHits + ?.then((hitCount) => { + dataSubjects.totalHits$.next({ fetchStatus: FetchStatus.COMPLETE, result: hitCount }); + checkHitCount(hitCount); + }) + .catch(sendErrorTo(dataSubjects.totalHits$)); + + // Return a promise that will resolve once all the requests have finished or failed + return Promise.allSettled([documents, charts, totalHits]).then(() => { + // Send a complete message to main$ once all queries are done and if main$ + // is not already in an ERROR state, e.g. because the document query has failed. + // This will only complete main$, if it hasn't already been completed previously + // by a query finding no results. + if (dataSubjects.main$.getValue().fetchStatus !== FetchStatus.ERROR) { + sendCompleteMsg(dataSubjects.main$); + } + }); + } catch (error) { + sendErrorMsg(dataSubjects.main$, error); + // We also want to return a resolved promise in an error case, since it just indicates we're done with querying. + return Promise.resolve(); + } } diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts index 5f57484aaa653..b8c2f643acae7 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts @@ -5,8 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { FetchStatus } from '../../types'; -import { BehaviorSubject, of, throwError as throwErrorRx } from 'rxjs'; +import { of, throwError as throwErrorRx } from 'rxjs'; import { RequestAdapter } from '../../../../../inspector'; import { savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; import { fetchChart, updateSearchSource } from './fetch_chart'; @@ -16,15 +15,6 @@ import { discoverServiceMock } from '../../../__mocks__/services'; import { calculateBounds, IKibanaSearchResponse } from '../../../../../data/common'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -function getDataSubjects() { - return { - main$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - documents$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - totalHits$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - charts$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - }; -} - describe('test fetchCharts', () => { test('updateSearchSource helper function', () => { const chartAggConfigs = updateSearchSource( @@ -61,8 +51,7 @@ describe('test fetchCharts', () => { `); }); - test('changes of fetchStatus when starting with FetchStatus.UNINITIALIZED', async (done) => { - const subjects = getDataSubjects(); + test('resolves with summarized chart data', async () => { const deps = { appStateContainer: { getState: () => { @@ -82,12 +71,6 @@ describe('test fetchCharts', () => { deps.data.query.timefilter.timefilter.calculateBounds = (timeRange) => calculateBounds(timeRange); - const stateArrChart: FetchStatus[] = []; - const stateArrHits: FetchStatus[] = []; - - subjects.charts$.subscribe((value) => stateArrChart.push(value.fetchStatus)); - subjects.totalHits$.subscribe((value) => stateArrHits.push(value.fetchStatus)); - savedSearchMockWithTimeField.searchSource.fetch$ = () => of({ id: 'Fjk5bndxTHJWU2FldVRVQ0tYR0VqOFEcRWtWNDhOdG5SUzJYcFhONVVZVTBJQToxMDMwOQ==', @@ -95,7 +78,7 @@ describe('test fetchCharts', () => { took: 2, timed_out: false, _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, - hits: { max_score: null, hits: [] }, + hits: { max_score: null, hits: [], total: 42 }, aggregations: { '2': { buckets: [ @@ -115,25 +98,13 @@ describe('test fetchCharts', () => { isRestored: false, } as unknown as IKibanaSearchResponse>); - fetchChart(subjects, savedSearchMockWithTimeField.searchSource, deps).subscribe({ - complete: () => { - expect(stateArrChart).toEqual([ - FetchStatus.UNINITIALIZED, - FetchStatus.LOADING, - FetchStatus.COMPLETE, - ]); - expect(stateArrHits).toEqual([ - FetchStatus.UNINITIALIZED, - FetchStatus.LOADING, - FetchStatus.COMPLETE, - ]); - done(); - }, - }); + const result = await fetchChart(savedSearchMockWithTimeField.searchSource, deps); + expect(result).toHaveProperty('totalHits', 42); + expect(result).toHaveProperty('bucketInterval.description', '0 milliseconds'); + expect(result).toHaveProperty('chartData'); }); - test('change of fetchStatus on fetch error', async (done) => { - const subjects = getDataSubjects(); + test('rejects promise on query failure', async () => { const deps = { appStateContainer: { getState: () => { @@ -149,26 +120,8 @@ describe('test fetchCharts', () => { savedSearchMockWithTimeField.searchSource.fetch$ = () => throwErrorRx({ msg: 'Oh noes!' }); - const stateArrChart: FetchStatus[] = []; - const stateArrHits: FetchStatus[] = []; - - subjects.charts$.subscribe((value) => stateArrChart.push(value.fetchStatus)); - subjects.totalHits$.subscribe((value) => stateArrHits.push(value.fetchStatus)); - - fetchChart(subjects, savedSearchMockWithTimeField.searchSource, deps).subscribe({ - error: () => { - expect(stateArrChart).toEqual([ - FetchStatus.UNINITIALIZED, - FetchStatus.LOADING, - FetchStatus.ERROR, - ]); - expect(stateArrHits).toEqual([ - FetchStatus.UNINITIALIZED, - FetchStatus.LOADING, - FetchStatus.ERROR, - ]); - done(); - }, + await expect(fetchChart(savedSearchMockWithTimeField.searchSource, deps)).rejects.toEqual({ + msg: 'Oh noes!', }); }); }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.ts index 59377970acb12..7f74f693eb784 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; -import { filter } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { DataPublicPluginStart, isCompleteResponse, @@ -16,40 +16,36 @@ import { import { Adapters } from '../../../../../inspector'; import { getChartAggConfigs, getDimensions } from './index'; import { tabifyAggResponse } from '../../../../../data/common'; -import { buildPointSeriesData } from '../components/chart/point_series'; -import { FetchStatus } from '../../types'; -import { SavedSearchData } from './use_saved_search'; +import { buildPointSeriesData, Chart } from '../components/chart/point_series'; +import { TimechartBucketInterval } from './use_saved_search'; import { AppState } from '../services/discover_state'; import { ReduxLikeStateContainer } from '../../../../../kibana_utils/common'; -import { sendErrorMsg, sendLoadingMsg } from './use_saved_search_messages'; + +interface Result { + totalHits: number; + chartData: Chart; + bucketInterval: TimechartBucketInterval | undefined; +} export function fetchChart( - data$: SavedSearchData, searchSource: ISearchSource, { abortController, appStateContainer, data, inspectorAdapters, - onResults, searchSessionId, }: { abortController: AbortController; appStateContainer: ReduxLikeStateContainer; data: DataPublicPluginStart; inspectorAdapters: Adapters; - onResults: (foundDocuments: boolean) => void; searchSessionId: string; } -) { - const { charts$, totalHits$ } = data$; - +): Promise { const interval = appStateContainer.getState().interval ?? 'auto'; const chartAggConfigs = updateSearchSource(searchSource, interval, data); - sendLoadingMsg(charts$); - sendLoadingMsg(totalHits$); - const executionContext = { type: 'application', name: 'discover', @@ -74,15 +70,9 @@ export function fetchChart( }, executionContext, }) - .pipe(filter((res) => isCompleteResponse(res))); - - fetch$.subscribe( - (res) => { - try { - const totalHitsNr = res.rawResponse.hits.total as number; - totalHits$.next({ fetchStatus: FetchStatus.COMPLETE, result: totalHitsNr }); - onResults(totalHitsNr > 0); - + .pipe( + filter((res) => isCompleteResponse(res)), + map((res) => { const bucketAggConfig = chartAggConfigs.aggs[1]; const tabifiedData = tabifyAggResponse(chartAggConfigs, res.rawResponse); const dimensions = getDimensions(chartAggConfigs, data); @@ -90,27 +80,15 @@ export function fetchChart( ? bucketAggConfig?.buckets?.getInterval() : undefined; const chartData = buildPointSeriesData(tabifiedData, dimensions!); - charts$.next({ - fetchStatus: FetchStatus.COMPLETE, + return { chartData, bucketInterval, - }); - } catch (e) { - charts$.next({ - fetchStatus: FetchStatus.ERROR, - error: e, - }); - } - }, - (error) => { - if (error instanceof Error && error.name === 'AbortError') { - return; - } - sendErrorMsg(charts$, error); - sendErrorMsg(totalHits$, error); - } - ); - return fetch$; + totalHits: res.rawResponse.hits.total as number, + }; + }) + ); + + return fetch$.toPromise(); } export function updateSearchSource( diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts index 291da255b5068..1342378f5a90b 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts @@ -6,74 +6,37 @@ * Side Public License, v 1. */ import { fetchDocuments } from './fetch_documents'; -import { FetchStatus } from '../../types'; -import { BehaviorSubject, throwError as throwErrorRx } from 'rxjs'; +import { throwError as throwErrorRx, of } from 'rxjs'; import { RequestAdapter } from '../../../../../inspector'; import { savedSearchMock } from '../../../__mocks__/saved_search'; import { discoverServiceMock } from '../../../__mocks__/services'; - -function getDataSubjects() { - return { - main$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - documents$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - totalHits$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - charts$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - }; -} +import { IKibanaSearchResponse } from 'src/plugins/data/common'; +import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; + +const getDeps = () => ({ + abortController: new AbortController(), + inspectorAdapters: { requests: new RequestAdapter() }, + onResults: jest.fn(), + searchSessionId: '123', + services: discoverServiceMock, +}); describe('test fetchDocuments', () => { - test('changes of fetchStatus are correct when starting with FetchStatus.UNINITIALIZED', async (done) => { - const subjects = getDataSubjects(); - const { documents$ } = subjects; - const deps = { - abortController: new AbortController(), - inspectorAdapters: { requests: new RequestAdapter() }, - onResults: jest.fn(), - searchSessionId: '123', - services: discoverServiceMock, - }; - - const stateArr: FetchStatus[] = []; - - documents$.subscribe((value) => stateArr.push(value.fetchStatus)); - - fetchDocuments(subjects, savedSearchMock.searchSource, deps).subscribe({ - complete: () => { - expect(stateArr).toEqual([ - FetchStatus.UNINITIALIZED, - FetchStatus.LOADING, - FetchStatus.COMPLETE, - ]); - done(); - }, - }); + test('resolves with returned documents', async () => { + const hits = [ + { _id: '1', foo: 'bar' }, + { _id: '2', foo: 'baz' }, + ]; + savedSearchMock.searchSource.fetch$ = () => + of({ rawResponse: { hits: { hits } } } as unknown as IKibanaSearchResponse); + expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).resolves.toEqual(hits); }); - test('change of fetchStatus on fetch error', async (done) => { - const subjects = getDataSubjects(); - const { documents$ } = subjects; - const deps = { - abortController: new AbortController(), - inspectorAdapters: { requests: new RequestAdapter() }, - onResults: jest.fn(), - searchSessionId: '123', - services: discoverServiceMock, - }; + test('rejects on query failure', () => { savedSearchMock.searchSource.fetch$ = () => throwErrorRx({ msg: 'Oh noes!' }); - const stateArr: FetchStatus[] = []; - - documents$.subscribe((value) => stateArr.push(value.fetchStatus)); - - fetchDocuments(subjects, savedSearchMock.searchSource, deps).subscribe({ - error: () => { - expect(stateArr).toEqual([ - FetchStatus.UNINITIALIZED, - FetchStatus.LOADING, - FetchStatus.ERROR, - ]); - done(); - }, + expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).rejects.toEqual({ + msg: 'Oh noes!', }); }); }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.ts index b23dd3a0ed932..0c83b85b2bc62 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.ts @@ -6,34 +6,30 @@ * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; -import { filter } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { Adapters } from '../../../../../inspector/common'; import { isCompleteResponse, ISearchSource } from '../../../../../data/common'; -import { FetchStatus } from '../../types'; -import { SavedSearchData } from './use_saved_search'; -import { sendErrorMsg, sendLoadingMsg } from './use_saved_search_messages'; import { SAMPLE_SIZE_SETTING } from '../../../../common'; import { DiscoverServices } from '../../../build_services'; +/** + * Requests the documents for Discover. This will return a promise that will resolve + * with the documents. + */ export const fetchDocuments = ( - data$: SavedSearchData, searchSource: ISearchSource, { abortController, inspectorAdapters, - onResults, searchSessionId, services, }: { abortController: AbortController; inspectorAdapters: Adapters; - onResults: (foundDocuments: boolean) => void; searchSessionId: string; services: DiscoverServices; } ) => { - const { documents$, totalHits$ } = data$; - searchSource.setField('size', services.uiSettings.get(SAMPLE_SIZE_SETTING)); searchSource.setField('trackTotalHits', false); searchSource.setField('highlightAll', true); @@ -46,8 +42,6 @@ export const fetchDocuments = ( searchSource.setOverwriteDataViewType(undefined); } - sendLoadingMsg(documents$); - const executionContext = { type: 'application', name: 'discover', @@ -71,34 +65,10 @@ export const fetchDocuments = ( }, executionContext, }) - .pipe(filter((res) => isCompleteResponse(res))); - - fetch$.subscribe( - (res) => { - const documents = res.rawResponse.hits.hits; - - // If the total hits query is still loading for hits, emit a partial - // hit count that's at least our document count - if (totalHits$.getValue().fetchStatus === FetchStatus.LOADING) { - totalHits$.next({ - fetchStatus: FetchStatus.PARTIAL, - result: documents.length, - }); - } - - documents$.next({ - fetchStatus: FetchStatus.COMPLETE, - result: documents, - }); - onResults(documents.length > 0); - }, - (error) => { - if (error instanceof Error && error.name === 'AbortError') { - return; - } + .pipe( + filter((res) => isCompleteResponse(res)), + map((res) => res.rawResponse.hits.hits) + ); - sendErrorMsg(documents$, error); - } - ); - return fetch$; + return fetch$.toPromise(); }; diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts index c593c9c157422..7b564906f95a7 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts @@ -5,76 +5,34 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { FetchStatus } from '../../types'; -import { BehaviorSubject, throwError as throwErrorRx } from 'rxjs'; +import { throwError as throwErrorRx, of } from 'rxjs'; import { RequestAdapter } from '../../../../../inspector'; import { savedSearchMock } from '../../../__mocks__/saved_search'; import { fetchTotalHits } from './fetch_total_hits'; import { discoverServiceMock } from '../../../__mocks__/services'; - -function getDataSubjects() { - return { - main$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - documents$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - totalHits$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - charts$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - }; -} +import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import { IKibanaSearchResponse } from 'src/plugins/data/common'; + +const getDeps = () => ({ + abortController: new AbortController(), + inspectorAdapters: { requests: new RequestAdapter() }, + searchSessionId: '123', + data: discoverServiceMock.data, +}); describe('test fetchTotalHits', () => { - test('changes of fetchStatus are correct when starting with FetchStatus.UNINITIALIZED', async (done) => { - const subjects = getDataSubjects(); - const { totalHits$ } = subjects; - - const deps = { - abortController: new AbortController(), - inspectorAdapters: { requests: new RequestAdapter() }, - onResults: jest.fn(), - searchSessionId: '123', - data: discoverServiceMock.data, - }; - - const stateArr: FetchStatus[] = []; + test('resolves returned promise with hit count', async () => { + savedSearchMock.searchSource.fetch$ = () => + of({ rawResponse: { hits: { total: 45 } } } as IKibanaSearchResponse); - totalHits$.subscribe((value) => stateArr.push(value.fetchStatus)); - - fetchTotalHits(subjects, savedSearchMock.searchSource, deps).subscribe({ - complete: () => { - expect(stateArr).toEqual([ - FetchStatus.UNINITIALIZED, - FetchStatus.LOADING, - FetchStatus.COMPLETE, - ]); - done(); - }, - }); + await expect(fetchTotalHits(savedSearchMock.searchSource, getDeps())).resolves.toBe(45); }); - test('change of fetchStatus on fetch error', async (done) => { - const subjects = getDataSubjects(); - const { totalHits$ } = subjects; - const deps = { - abortController: new AbortController(), - inspectorAdapters: { requests: new RequestAdapter() }, - onResults: jest.fn(), - searchSessionId: '123', - data: discoverServiceMock.data, - }; + test('rejects in case of an error', async () => { savedSearchMock.searchSource.fetch$ = () => throwErrorRx({ msg: 'Oh noes!' }); - const stateArr: FetchStatus[] = []; - - totalHits$.subscribe((value) => stateArr.push(value.fetchStatus)); - - fetchTotalHits(subjects, savedSearchMock.searchSource, deps).subscribe({ - error: () => { - expect(stateArr).toEqual([ - FetchStatus.UNINITIALIZED, - FetchStatus.LOADING, - FetchStatus.ERROR, - ]); - done(); - }, + await expect(fetchTotalHits(savedSearchMock.searchSource, getDeps())).rejects.toEqual({ + msg: 'Oh noes!', }); }); }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts index 197e00ce0449f..55fc9c1c17235 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts @@ -7,36 +7,23 @@ */ import { i18n } from '@kbn/i18n'; -import { filter } from 'rxjs/operators'; -import { - DataPublicPluginStart, - isCompleteResponse, - ISearchSource, -} from '../../../../../data/public'; +import { filter, map } from 'rxjs/operators'; +import { isCompleteResponse, ISearchSource } from '../../../../../data/public'; import { DataViewType } from '../../../../../data_views/common'; import { Adapters } from '../../../../../inspector/common'; -import { FetchStatus } from '../../types'; -import { SavedSearchData } from './use_saved_search'; -import { sendErrorMsg, sendLoadingMsg } from './use_saved_search_messages'; export function fetchTotalHits( - data$: SavedSearchData, searchSource: ISearchSource, { abortController, - data, inspectorAdapters, - onResults, searchSessionId, }: { abortController: AbortController; - data: DataPublicPluginStart; - onResults: (foundDocuments: boolean) => void; inspectorAdapters: Adapters; searchSessionId: string; } ) { - const { totalHits$ } = data$; searchSource.setField('trackTotalHits', true); searchSource.setField('size', 0); searchSource.removeField('sort'); @@ -50,8 +37,6 @@ export function fetchTotalHits( searchSource.setOverwriteDataViewType(undefined); } - sendLoadingMsg(totalHits$); - const executionContext = { type: 'application', name: 'discover', @@ -75,21 +60,10 @@ export function fetchTotalHits( sessionId: searchSessionId, executionContext, }) - .pipe(filter((res) => isCompleteResponse(res))); - - fetch$.subscribe( - (res) => { - const totalHitsNr = res.rawResponse.hits.total as number; - totalHits$.next({ fetchStatus: FetchStatus.COMPLETE, result: totalHitsNr }); - onResults(totalHitsNr > 0); - }, - (error) => { - if (error instanceof Error && error.name === 'AbortError') { - return; - } - sendErrorMsg(totalHits$, error); - } - ); + .pipe( + filter((res) => isCompleteResponse(res)), + map((res) => res.rawResponse.hits.total as number) + ); - return fetch$; + return fetch$.toPromise(); } diff --git a/src/plugins/discover/public/application/main/utils/use_saved_search.ts b/src/plugins/discover/public/application/main/utils/use_saved_search.ts index 0f4b9058316a0..f37fdef4bd655 100644 --- a/src/plugins/discover/public/application/main/utils/use_saved_search.ts +++ b/src/plugins/discover/public/application/main/utils/use_saved_search.ts @@ -159,7 +159,7 @@ export const useSavedSearch = ({ initialFetchStatus, }); - const subscription = fetch$.subscribe((val) => { + const subscription = fetch$.subscribe(async (val) => { if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) { return; } @@ -167,28 +167,26 @@ export const useSavedSearch = ({ refs.current.abortController?.abort(); refs.current.abortController = new AbortController(); - try { - fetchAll(dataSubjects, searchSource, val === 'reset', { - abortController: refs.current.abortController, - appStateContainer: stateContainer.appStateContainer, - inspectorAdapters, - data, - initialFetchStatus, - searchSessionId: searchSessionManager.getNextSearchSessionId(), - services, - useNewFieldsApi, - }).subscribe({ - complete: () => { - // if this function was set and is executed, another refresh fetch can be triggered - refs.current.autoRefreshDone?.(); - refs.current.autoRefreshDone = undefined; - }, - }); - } catch (error) { - main$.next({ - fetchStatus: FetchStatus.ERROR, - error, - }); + const autoRefreshDone = refs.current.autoRefreshDone; + + await fetchAll(dataSubjects, searchSource, val === 'reset', { + abortController: refs.current.abortController, + appStateContainer: stateContainer.appStateContainer, + inspectorAdapters, + data, + initialFetchStatus, + searchSessionId: searchSessionManager.getNextSearchSessionId(), + services, + useNewFieldsApi, + }); + + // If the autoRefreshCallback is still the same as when we started i.e. there was no newer call + // replacing this current one, call it to make sure we tell that the auto refresh is done + // and a new one can be scheduled. + if (autoRefreshDone === refs.current.autoRefreshDone) { + // if this function was set and is executed, another refresh fetch can be triggered + refs.current.autoRefreshDone?.(); + refs.current.autoRefreshDone = undefined; } }); diff --git a/src/plugins/discover/public/application/main/utils/use_saved_search_messages.test.ts b/src/plugins/discover/public/application/main/utils/use_saved_search_messages.test.ts index 2fa264690329e..0d74061ac46a3 100644 --- a/src/plugins/discover/public/application/main/utils/use_saved_search_messages.test.ts +++ b/src/plugins/discover/public/application/main/utils/use_saved_search_messages.test.ts @@ -9,14 +9,16 @@ import { sendCompleteMsg, sendErrorMsg, sendLoadingMsg, + sendNoResultsFoundMsg, sendPartialMsg, } from './use_saved_search_messages'; import { FetchStatus } from '../../types'; import { BehaviorSubject } from 'rxjs'; import { DataMainMsg } from './use_saved_search'; +import { filter } from 'rxjs/operators'; describe('test useSavedSearch message generators', () => { - test('sendCompleteMsg', async (done) => { + test('sendCompleteMsg', (done) => { const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.LOADING }); main$.subscribe((value) => { if (value.fetchStatus !== FetchStatus.LOADING) { @@ -28,7 +30,18 @@ describe('test useSavedSearch message generators', () => { }); sendCompleteMsg(main$, true); }); - test('sendPartialMessage', async (done) => { + test('sendNoResultsFoundMsg', (done) => { + const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.LOADING }); + main$ + .pipe(filter(({ fetchStatus }) => fetchStatus !== FetchStatus.LOADING)) + .subscribe((value) => { + expect(value.fetchStatus).toBe(FetchStatus.COMPLETE); + expect(value.foundDocuments).toBe(false); + done(); + }); + sendNoResultsFoundMsg(main$); + }); + test('sendPartialMessage', (done) => { const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.LOADING }); main$.subscribe((value) => { if (value.fetchStatus !== FetchStatus.LOADING) { @@ -38,7 +51,7 @@ describe('test useSavedSearch message generators', () => { }); sendPartialMsg(main$); }); - test('sendLoadingMsg', async (done) => { + test('sendLoadingMsg', (done) => { const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.COMPLETE }); main$.subscribe((value) => { if (value.fetchStatus !== FetchStatus.COMPLETE) { @@ -48,7 +61,7 @@ describe('test useSavedSearch message generators', () => { }); sendLoadingMsg(main$); }); - test('sendErrorMsg', async (done) => { + test('sendErrorMsg', (done) => { const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.PARTIAL }); main$.subscribe((value) => { if (value.fetchStatus === FetchStatus.ERROR) { @@ -60,7 +73,7 @@ describe('test useSavedSearch message generators', () => { sendErrorMsg(main$, new Error('Pls help!')); }); - test('sendCompleteMsg cleaning error state message', async (done) => { + test('sendCompleteMsg cleaning error state message', (done) => { const initialState = { fetchStatus: FetchStatus.ERROR, error: new Error('Oh noes!'), diff --git a/src/plugins/discover/public/application/main/utils/use_saved_search_messages.ts b/src/plugins/discover/public/application/main/utils/use_saved_search_messages.ts index 325d63eb6d21a..a2d42147a9e8f 100644 --- a/src/plugins/discover/public/application/main/utils/use_saved_search_messages.ts +++ b/src/plugins/discover/public/application/main/utils/use_saved_search_messages.ts @@ -15,6 +15,15 @@ import { SavedSearchData, } from './use_saved_search'; +/** + * Sends COMPLETE message to the main$ observable with the information + * that no documents have been found, allowing Discover to show a no + * results message. + */ +export function sendNoResultsFoundMsg(main$: DataMain$) { + sendCompleteMsg(main$, false); +} + /** * Send COMPLETE message via main observable used when * 1.) first fetch resolved, and there are no documents diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx index 30e0cf24f7d52..27f4268224904 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx @@ -27,8 +27,7 @@ import { import { DocViewer } from '../../services/doc_views/components/doc_viewer/doc_viewer'; import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; import { DiscoverServices } from '../../build_services'; -import { getContextUrl } from '../../utils/get_context_url'; -import { getSingleDocUrl } from '../../utils/get_single_doc_url'; +import { useNavigationProps } from '../../utils/use_navigation_props'; import { ElasticSearchHit } from '../../types'; interface Props { @@ -103,6 +102,15 @@ export function DiscoverGridFlyout({ [activePage, setPage] ); + const { singleDocProps, surrDocsProps } = useNavigationProps({ + indexPatternId: indexPattern.id!, + rowIndex: hit._index, + rowId: hit._id, + filterManager: services.filterManager, + addBasePath: services.addBasePath, + columns, + }); + return ( {i18n.translate('discover.grid.tableRow.viewSingleDocumentLinkTextSimple', { defaultMessage: 'Single document', @@ -157,13 +165,7 @@ export function DiscoverGridFlyout({ size="xs" iconType="documents" flush="left" - href={getContextUrl( - String(hit._id), - indexPattern.id, - columns, - services.filterManager, - services.addBasePath - )} + {...surrDocsProps} data-test-subj="docTableRowAction" > {i18n.translate('discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple', { diff --git a/src/plugins/discover/public/components/doc_table/components/table_row.tsx b/src/plugins/discover/public/components/doc_table/components/table_row.tsx index 2eee9a177e4f8..2d9e8fa6e9584 100644 --- a/src/plugins/discover/public/components/doc_table/components/table_row.tsx +++ b/src/plugins/discover/public/components/doc_table/components/table_row.tsx @@ -15,12 +15,11 @@ import { flattenHit } from '../../../../../data/common'; import { DocViewer } from '../../../services/doc_views/components/doc_viewer/doc_viewer'; import { FilterManager, IndexPattern } from '../../../../../data/public'; import { TableCell } from './table_row/table_cell'; -import { DocViewFilterFn } from '../../../services/doc_views/doc_views_types'; -import { getContextUrl } from '../../../utils/get_context_url'; -import { getSingleDocUrl } from '../../../utils/get_single_doc_url'; -import { TableRowDetails } from './table_row_details'; import { formatRow, formatTopLevelObject } from '../lib/row_formatter'; +import { useNavigationProps } from '../../../utils/use_navigation_props'; +import { DocViewFilterFn } from '../../../services/doc_views/doc_views_types'; import { ElasticSearchHit } from '../../../types'; +import { TableRowDetails } from './table_row_details'; export type DocTableRow = ElasticSearchHit & { isAnchor?: boolean; @@ -100,13 +99,14 @@ export const TableRow = ({ [filter, flattenedRow, indexPattern.fields] ); - const getContextAppHref = () => { - return getContextUrl(row._id, indexPattern.id!, columns, filterManager, addBasePath); - }; - - const getSingleDocHref = () => { - return addBasePath(getSingleDocUrl(indexPattern.id!, row._index, row._id)); - }; + const { singleDocProps, surrDocsProps } = useNavigationProps({ + indexPatternId: indexPattern.id!, + rowIndex: row._index, + rowId: row._id, + filterManager, + addBasePath, + columns, + }); const rowCells = [ @@ -208,8 +208,8 @@ export const TableRow = ({ open={open} colLength={(columns.length || 1) + 2} isTimeBased={indexPattern.isTimeBased()} - getContextAppHref={getContextAppHref} - getSingleDocHref={getSingleDocHref} + singleDocProps={singleDocProps} + surrDocsProps={surrDocsProps} > string; - getSingleDocHref: () => string; + singleDocProps: DiscoverNavigationProps; + surrDocsProps: DiscoverNavigationProps; children: JSX.Element; } @@ -22,8 +23,8 @@ export const TableRowDetails = ({ open, colLength, isTimeBased, - getContextAppHref, - getSingleDocHref, + singleDocProps, + surrDocsProps, children, }: TableRowDetailsProps) => { if (!open) { @@ -54,7 +55,7 @@ export const TableRowDetails = ({ {isTimeBased && ( - + - + } > -
- - - -
- - -

- An Error Occurred -

-
- - - + + + +
+
- - -
-
- Could not fetch data at this time. Refresh the tab to try again. - +

-
- - + + + - - - - -
+ + + +

+
+
+ +
- - - -
+
+ + + `; diff --git a/src/plugins/discover/public/utils/breadcrumbs.ts b/src/plugins/discover/public/utils/breadcrumbs.ts index 4a3df34e2da75..4d79598ce5389 100644 --- a/src/plugins/discover/public/utils/breadcrumbs.ts +++ b/src/plugins/discover/public/utils/breadcrumbs.ts @@ -10,13 +10,13 @@ import { ChromeStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { SavedSearch } from '../services/saved_searches'; -export function getRootBreadcrumbs() { +export function getRootBreadcrumbs(breadcrumb?: string) { return [ { text: i18n.translate('discover.rootBreadcrumb', { defaultMessage: 'Discover', }), - href: '#/', + href: breadcrumb || '#/', }, ]; } diff --git a/src/plugins/discover/public/utils/get_context_url.test.ts b/src/plugins/discover/public/utils/get_context_url.test.ts deleted file mode 100644 index d6d1db5ca393b..0000000000000 --- a/src/plugins/discover/public/utils/get_context_url.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { getContextUrl } from './get_context_url'; -import { FilterManager } from '../../../data/public/query/filter_manager'; -const filterManager = { - getGlobalFilters: () => [], - getAppFilters: () => [], -} as unknown as FilterManager; -const addBasePath = (path: string) => `/base${path}`; - -describe('Get context url', () => { - test('returning a valid context url', async () => { - const url = await getContextUrl( - 'docId', - 'ipId', - ['test1', 'test2'], - filterManager, - addBasePath - ); - expect(url).toMatchInlineSnapshot( - `"/base/app/discover#/context/ipId/docId?_g=(filters:!())&_a=(columns:!(test1,test2),filters:!())"` - ); - }); - - test('returning a valid context url when docId contains whitespace', async () => { - const url = await getContextUrl( - 'doc Id', - 'ipId', - ['test1', 'test2'], - filterManager, - addBasePath - ); - expect(url).toMatchInlineSnapshot( - `"/base/app/discover#/context/ipId/doc%20Id?_g=(filters:!())&_a=(columns:!(test1,test2),filters:!())"` - ); - }); -}); diff --git a/src/plugins/discover/public/utils/get_context_url.tsx b/src/plugins/discover/public/utils/get_context_url.tsx deleted file mode 100644 index 68c0e935f17e9..0000000000000 --- a/src/plugins/discover/public/utils/get_context_url.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { stringify } from 'query-string'; -import rison from 'rison-node'; -import { url } from '../../../kibana_utils/common'; -import { esFilters, FilterManager } from '../../../data/public'; -import { DiscoverServices } from '../build_services'; - -/** - * Helper function to generate an URL to a document in Discover's context view - */ -export function getContextUrl( - documentId: string, - indexPatternId: string, - columns: string[], - filterManager: FilterManager, - addBasePath: DiscoverServices['addBasePath'] -) { - const globalFilters = filterManager.getGlobalFilters(); - const appFilters = filterManager.getAppFilters(); - - const hash = stringify( - url.encodeQuery({ - _g: rison.encode({ - filters: globalFilters || [], - }), - _a: rison.encode({ - columns, - filters: (appFilters || []).map(esFilters.disableFilter), - }), - }), - { encode: false, sort: false } - ); - - return addBasePath( - `/app/discover#/context/${encodeURIComponent(indexPatternId)}/${encodeURIComponent( - documentId - )}?${hash}` - ); -} diff --git a/src/plugins/discover/public/utils/use_navigation_props.test.tsx b/src/plugins/discover/public/utils/use_navigation_props.test.tsx new file mode 100644 index 0000000000000..29d4976f265c3 --- /dev/null +++ b/src/plugins/discover/public/utils/use_navigation_props.test.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { ReactElement } from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { createFilterManagerMock } from '../../../data/public/query/filter_manager/filter_manager.mock'; +import { + getContextHash, + HistoryState, + useNavigationProps, + UseNavigationProps, +} from './use_navigation_props'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { setServices } from '../kibana_services'; +import { DiscoverServices } from '../build_services'; + +const filterManager = createFilterManagerMock(); +const defaultProps = { + indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + rowIndex: 'kibana_sample_data_ecommerce', + rowId: 'QmsYdX0BQ6gV8MTfoPYE', + columns: ['customer_first_name', 'products.manufacturer'], + filterManager, + addBasePath: jest.fn(), +} as UseNavigationProps; +const basePathPrefix = 'localhost:5601/xqj'; + +const getSearch = () => { + return `?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)) + &_a=(columns:!(${defaultProps.columns.join()}),filters:!(),index:${defaultProps.indexPatternId} + ,interval:auto,query:(language:kuery,query:''),sort:!(!(order_date,desc)))`; +}; + +const getSingeDocRoute = () => { + return `/doc/${defaultProps.indexPatternId}/${defaultProps.rowIndex}`; +}; + +const getContextRoute = () => { + return `/context/${defaultProps.indexPatternId}/${defaultProps.rowId}`; +}; + +const render = () => { + const history = createMemoryHistory({ + initialEntries: ['/' + getSearch()], + }); + setServices({ history: () => history } as unknown as DiscoverServices); + const wrapper = ({ children }: { children: ReactElement }) => ( + {children} + ); + return { + result: renderHook(() => useNavigationProps(defaultProps), { wrapper }).result, + history, + }; +}; + +describe('useNavigationProps', () => { + test('should provide valid breadcrumb for single doc page from main view', () => { + const { result, history } = render(); + + result.current.singleDocProps.onClick?.(); + expect(history.location.pathname).toEqual(getSingeDocRoute()); + expect(history.location.search).toEqual(`?id=${defaultProps.rowId}`); + expect(history.location.state?.breadcrumb).toEqual(`#/${getSearch()}`); + }); + + test('should provide valid breadcrumb for context page from main view', () => { + const { result, history } = render(); + + result.current.surrDocsProps.onClick?.(); + expect(history.location.pathname).toEqual(getContextRoute()); + expect(history.location.search).toEqual( + `?${getContextHash(defaultProps.columns, filterManager)}` + ); + expect(history.location.state?.breadcrumb).toEqual(`#/${getSearch()}`); + }); + + test('should create valid links to the context and single doc pages from embeddable', () => { + const { result } = renderHook(() => + useNavigationProps({ + ...defaultProps, + addBasePath: (val: string) => `${basePathPrefix}${val}`, + }) + ); + + expect(result.current.singleDocProps.href!).toEqual( + `${basePathPrefix}/app/discover#${getSingeDocRoute()}?id=${defaultProps.rowId}` + ); + expect(result.current.surrDocsProps.href!).toEqual( + `${basePathPrefix}/app/discover#${getContextRoute()}?${getContextHash( + defaultProps.columns, + filterManager + )}` + ); + }); +}); diff --git a/src/plugins/discover/public/utils/use_navigation_props.tsx b/src/plugins/discover/public/utils/use_navigation_props.tsx new file mode 100644 index 0000000000000..6f1dedf75e730 --- /dev/null +++ b/src/plugins/discover/public/utils/use_navigation_props.tsx @@ -0,0 +1,132 @@ +/* + * Copyright 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 { useMemo, useRef } from 'react'; +import { useHistory, matchPath } from 'react-router-dom'; +import { stringify } from 'query-string'; +import rison from 'rison-node'; +import { esFilters, FilterManager } from '../../../data/public'; +import { url } from '../../../kibana_utils/common'; +import { getServices } from '../kibana_services'; + +export type DiscoverNavigationProps = { onClick: () => void } | { href: string }; + +export interface UseNavigationProps { + indexPatternId: string; + rowIndex: string; + rowId: string; + columns: string[]; + filterManager: FilterManager; + addBasePath: (url: string) => string; +} + +export type HistoryState = { breadcrumb?: string } | undefined; + +export const getContextHash = (columns: string[], filterManager: FilterManager) => { + const globalFilters = filterManager.getGlobalFilters(); + const appFilters = filterManager.getAppFilters(); + + const hash = stringify( + url.encodeQuery({ + _g: rison.encode({ + filters: globalFilters || [], + }), + _a: rison.encode({ + columns, + filters: (appFilters || []).map(esFilters.disableFilter), + }), + }), + { encode: false, sort: false } + ); + + return hash; +}; + +/** + * When it's context route, breadcrumb link should point to the main discover page anyway. + * Otherwise, we are on main page and should create breadcrumb link from it. + * Current history object should be used in callback, since url state might be changed + * after expanded document opened. + */ +const getCurrentBreadcrumbs = (isContextRoute: boolean, prevBreadcrumb?: string) => { + const { history: getHistory } = getServices(); + const currentHistory = getHistory(); + return isContextRoute + ? prevBreadcrumb + : '#' + currentHistory?.location.pathname + currentHistory?.location.search; +}; + +export const useMainRouteBreadcrumb = () => { + // useRef needed to retrieve initial breadcrumb link from the push state without updates + return useRef(useHistory().location.state?.breadcrumb).current; +}; + +export const useNavigationProps = ({ + indexPatternId, + rowIndex, + rowId, + columns, + filterManager, + addBasePath, +}: UseNavigationProps) => { + const history = useHistory(); + const prevBreadcrumb = useRef(history?.location.state?.breadcrumb).current; + const contextSearchHash = useMemo( + () => getContextHash(columns, filterManager), + [columns, filterManager] + ); + + /** + * When history can be accessed via hooks, + * it is discover main or context route. + */ + if (!!history) { + const isContextRoute = matchPath(history.location.pathname, { + path: '/context/:indexPatternId/:id', + exact: true, + }); + + const onOpenSingleDoc = () => { + history.push({ + pathname: `/doc/${indexPatternId}/${rowIndex}`, + search: `?id=${encodeURIComponent(rowId)}`, + state: { breadcrumb: getCurrentBreadcrumbs(!!isContextRoute, prevBreadcrumb) }, + }); + }; + + const onOpenSurrDocs = () => + history.push({ + pathname: `/context/${encodeURIComponent(indexPatternId)}/${encodeURIComponent( + String(rowId) + )}`, + search: `?${contextSearchHash}`, + state: { breadcrumb: getCurrentBreadcrumbs(!!isContextRoute, prevBreadcrumb) }, + }); + + return { + singleDocProps: { onClick: onOpenSingleDoc }, + surrDocsProps: { onClick: onOpenSurrDocs }, + }; + } + + // for embeddable absolute href should be kept + return { + singleDocProps: { + href: addBasePath( + `/app/discover#/doc/${indexPatternId}/${rowIndex}?id=${encodeURIComponent(rowId)}` + ), + }, + surrDocsProps: { + href: addBasePath( + `/app/discover#/context/${encodeURIComponent(indexPatternId)}/${encodeURIComponent( + rowId + )}?${contextSearchHash}` + ), + }, + }; +}; 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/home/server/plugin.ts b/src/plugins/home/server/plugin.ts index 6f082dd561e93..04d2d80898b50 100644 --- a/src/plugins/home/server/plugin.ts +++ b/src/plugins/home/server/plugin.ts @@ -27,12 +27,13 @@ export interface HomeServerPluginSetupDependencies { } export class HomeServerPlugin implements Plugin { - private readonly tutorialsRegistry = new TutorialsRegistry(); + private readonly tutorialsRegistry; private readonly sampleDataRegistry: SampleDataRegistry; private customIntegrations?: CustomIntegrationsPluginSetup; constructor(private readonly initContext: PluginInitializerContext) { this.sampleDataRegistry = new SampleDataRegistry(this.initContext); + this.tutorialsRegistry = new TutorialsRegistry(this.initContext); } public setup(core: CoreSetup, plugins: HomeServerPluginSetupDependencies): HomeServerPluginSetup { diff --git a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts index 4c80c8858a475..aeebecf6cab32 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts @@ -29,6 +29,7 @@ export enum TutorialsCategory { export type Platform = 'WINDOWS' | 'OSX' | 'DEB' | 'RPM'; export interface TutorialContext { + kibanaBranch: string; [key: string]: unknown; } export type TutorialProvider = (context: TutorialContext) => TutorialSchema; diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts index ee73c8e13f62b..dec1d23e05787 100644 --- a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts @@ -69,6 +69,7 @@ const validTutorialProvider = VALID_TUTORIAL; describe('TutorialsRegistry', () => { let mockCoreSetup: MockedKeys; + let mockInitContext: ReturnType; let testProvider: TutorialProvider; let testScopedTutorialContextFactory: ScopedTutorialContextFactory; let mockCustomIntegrationsPluginSetup: jest.Mocked; @@ -80,6 +81,7 @@ describe('TutorialsRegistry', () => { describe('GET /api/kibana/home/tutorials', () => { beforeEach(() => { mockCoreSetup = coreMock.createSetup(); + mockInitContext = coreMock.createPluginInitializerContext(); }); test('has a router that retrieves registered tutorials', () => { @@ -90,13 +92,19 @@ describe('TutorialsRegistry', () => { describe('setup', () => { test('exposes proper contract', () => { - const setup = new TutorialsRegistry().setup(mockCoreSetup, mockCustomIntegrationsPluginSetup); + const setup = new TutorialsRegistry(mockInitContext).setup( + mockCoreSetup, + mockCustomIntegrationsPluginSetup + ); expect(setup).toHaveProperty('registerTutorial'); expect(setup).toHaveProperty('addScopedTutorialContextFactory'); }); test('registerTutorial throws when registering a tutorial with an invalid schema', () => { - const setup = new TutorialsRegistry().setup(mockCoreSetup, mockCustomIntegrationsPluginSetup); + const setup = new TutorialsRegistry(mockInitContext).setup( + mockCoreSetup, + mockCustomIntegrationsPluginSetup + ); testProvider = ({}) => invalidTutorialProvider; expect(() => setup.registerTutorial(testProvider)).toThrowErrorMatchingInlineSnapshot( `"Unable to register tutorial spec because its invalid. Error: [name]: is not allowed to be empty"` @@ -104,7 +112,10 @@ describe('TutorialsRegistry', () => { }); test('registerTutorial registers a tutorial with a valid schema', () => { - const setup = new TutorialsRegistry().setup(mockCoreSetup, mockCustomIntegrationsPluginSetup); + const setup = new TutorialsRegistry(mockInitContext).setup( + mockCoreSetup, + mockCustomIntegrationsPluginSetup + ); testProvider = ({}) => validTutorialProvider; expect(() => setup.registerTutorial(testProvider)).not.toThrowError(); expect(mockCustomIntegrationsPluginSetup.registerCustomIntegration.mock.calls).toEqual([ @@ -129,7 +140,10 @@ describe('TutorialsRegistry', () => { }); test('addScopedTutorialContextFactory throws when given a scopedTutorialContextFactory that is not a function', () => { - const setup = new TutorialsRegistry().setup(mockCoreSetup, mockCustomIntegrationsPluginSetup); + const setup = new TutorialsRegistry(mockInitContext).setup( + mockCoreSetup, + mockCustomIntegrationsPluginSetup + ); const testItem = {} as TutorialProvider; expect(() => setup.addScopedTutorialContextFactory(testItem) @@ -139,7 +153,10 @@ describe('TutorialsRegistry', () => { }); test('addScopedTutorialContextFactory adds a scopedTutorialContextFactory when given a function', () => { - const setup = new TutorialsRegistry().setup(mockCoreSetup, mockCustomIntegrationsPluginSetup); + const setup = new TutorialsRegistry(mockInitContext).setup( + mockCoreSetup, + mockCustomIntegrationsPluginSetup + ); testScopedTutorialContextFactory = ({}) => 'string'; expect(() => setup.addScopedTutorialContextFactory(testScopedTutorialContextFactory) @@ -149,7 +166,7 @@ describe('TutorialsRegistry', () => { describe('start', () => { test('exposes proper contract', () => { - const start = new TutorialsRegistry().start( + const start = new TutorialsRegistry(mockInitContext).start( coreMock.createStart(), mockCustomIntegrationsPluginSetup ); diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.ts index 723c92e6dfaf4..7d93a57b2073d 100644 --- a/src/plugins/home/server/services/tutorials/tutorials_registry.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.ts @@ -6,11 +6,12 @@ * Side Public License, v 1. */ -import { CoreSetup, CoreStart } from 'src/core/server'; +import { CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/server'; import { TutorialProvider, TutorialContextFactory, ScopedTutorialContextFactory, + TutorialContext, } from './lib/tutorials_registry_types'; import { TutorialSchema, tutorialSchema } from './lib/tutorial_schema'; import { builtInTutorials } from '../../tutorials/register'; @@ -71,12 +72,14 @@ export class TutorialsRegistry { private tutorialProviders: TutorialProvider[] = []; // pre-register all the tutorials we know we want in here private readonly scopedTutorialContextFactories: TutorialContextFactory[] = []; + constructor(private readonly initContext: PluginInitializerContext) {} + public setup(core: CoreSetup, customIntegrations?: CustomIntegrationsPluginSetup) { const router = core.http.createRouter(); router.get( { path: '/api/kibana/home/tutorials', validate: false }, async (context, req, res) => { - const initialContext = {}; + const initialContext = this.baseTutorialContext; const scopedContext = this.scopedTutorialContextFactories.reduce( (accumulatedContext, contextFactory) => { return { ...accumulatedContext, ...contextFactory(req) }; @@ -92,7 +95,7 @@ export class TutorialsRegistry { ); return { registerTutorial: (specProvider: TutorialProvider) => { - const emptyContext = {}; + const emptyContext = this.baseTutorialContext; let tutorial: TutorialSchema; try { tutorial = tutorialSchema.validate(specProvider(emptyContext)); @@ -132,12 +135,16 @@ export class TutorialsRegistry { if (customIntegrations) { builtInTutorials.forEach((provider) => { - const tutorial = provider({}); + const tutorial = provider(this.baseTutorialContext); registerBeatsTutorialsWithCustomIntegrations(core, customIntegrations, tutorial); }); } return {}; } + + private get baseTutorialContext(): TutorialContext { + return { kibanaBranch: this.initContext.env.packageInfo.branch }; + } } /** @public */ diff --git a/src/plugins/home/server/tutorials/activemq_logs/index.ts b/src/plugins/home/server/tutorials/activemq_logs/index.ts index a277b37838562..cc84f9a536b22 100644 --- a/src/plugins/home/server/tutorials/activemq_logs/index.ts +++ b/src/plugins/home/server/tutorials/activemq_logs/index.ts @@ -56,8 +56,8 @@ export function activemqLogsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/activemq_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/activemq_metrics/index.ts b/src/plugins/home/server/tutorials/activemq_metrics/index.ts index 9a001c149cda0..9c98c9c2ffc7a 100644 --- a/src/plugins/home/server/tutorials/activemq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/activemq_metrics/index.ts @@ -54,8 +54,8 @@ export function activemqMetricsSpecProvider(context: TutorialContext): TutorialS }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web'], }; diff --git a/src/plugins/home/server/tutorials/aerospike_metrics/index.ts b/src/plugins/home/server/tutorials/aerospike_metrics/index.ts index 3e574f2c75496..1cc350af579cb 100644 --- a/src/plugins/home/server/tutorials/aerospike_metrics/index.ts +++ b/src/plugins/home/server/tutorials/aerospike_metrics/index.ts @@ -54,8 +54,8 @@ export function aerospikeMetricsSpecProvider(context: TutorialContext): Tutorial }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/apache_logs/index.ts b/src/plugins/home/server/tutorials/apache_logs/index.ts index 6e588fd86588d..aea8e3c188d94 100644 --- a/src/plugins/home/server/tutorials/apache_logs/index.ts +++ b/src/plugins/home/server/tutorials/apache_logs/index.ts @@ -57,8 +57,8 @@ export function apacheLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/apache_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/apache_metrics/index.ts b/src/plugins/home/server/tutorials/apache_metrics/index.ts index 17b495d1460c5..0af719610c24d 100644 --- a/src/plugins/home/server/tutorials/apache_metrics/index.ts +++ b/src/plugins/home/server/tutorials/apache_metrics/index.ts @@ -56,8 +56,8 @@ export function apacheMetricsSpecProvider(context: TutorialContext): TutorialSch completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/apache_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/auditbeat/index.ts b/src/plugins/home/server/tutorials/auditbeat/index.ts index 96e5d4bcda393..666fcf15635c3 100644 --- a/src/plugins/home/server/tutorials/auditbeat/index.ts +++ b/src/plugins/home/server/tutorials/auditbeat/index.ts @@ -56,8 +56,8 @@ processes, users, logins, sockets information, file accesses, and more. \ completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/auditbeat/screenshot.png', onPrem: onPremInstructions(platforms, context), - elasticCloud: cloudInstructions(platforms), - onPremElasticCloud: onPremCloudInstructions(platforms), + elasticCloud: cloudInstructions(platforms, context), + onPremElasticCloud: onPremCloudInstructions(platforms, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/auditd_logs/index.ts b/src/plugins/home/server/tutorials/auditd_logs/index.ts index 6993196d93417..24857045ccc28 100644 --- a/src/plugins/home/server/tutorials/auditd_logs/index.ts +++ b/src/plugins/home/server/tutorials/auditd_logs/index.ts @@ -57,8 +57,8 @@ export function auditdLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/auditd_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['os_system'], }; } diff --git a/src/plugins/home/server/tutorials/aws_logs/index.ts b/src/plugins/home/server/tutorials/aws_logs/index.ts index 62fbcc4eebc18..60187490318ae 100644 --- a/src/plugins/home/server/tutorials/aws_logs/index.ts +++ b/src/plugins/home/server/tutorials/aws_logs/index.ts @@ -57,8 +57,8 @@ export function awsLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/aws_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['aws', 'cloud', 'datastore', 'security', 'network'], }; } diff --git a/src/plugins/home/server/tutorials/aws_metrics/index.ts b/src/plugins/home/server/tutorials/aws_metrics/index.ts index 6bf1bf64bff9f..6541b4f5f29c8 100644 --- a/src/plugins/home/server/tutorials/aws_metrics/index.ts +++ b/src/plugins/home/server/tutorials/aws_metrics/index.ts @@ -58,8 +58,8 @@ export function awsMetricsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/aws_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['aws', 'cloud', 'datastore', 'security', 'network'], }; } diff --git a/src/plugins/home/server/tutorials/azure_logs/index.ts b/src/plugins/home/server/tutorials/azure_logs/index.ts index 3c9438d9a6298..163496813567a 100644 --- a/src/plugins/home/server/tutorials/azure_logs/index.ts +++ b/src/plugins/home/server/tutorials/azure_logs/index.ts @@ -58,8 +58,8 @@ export function azureLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/azure_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['azure', 'cloud', 'network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/azure_metrics/index.ts b/src/plugins/home/server/tutorials/azure_metrics/index.ts index 310f954104634..edf4062812b42 100644 --- a/src/plugins/home/server/tutorials/azure_metrics/index.ts +++ b/src/plugins/home/server/tutorials/azure_metrics/index.ts @@ -57,8 +57,8 @@ export function azureMetricsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/azure_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['azure', 'cloud', 'network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/barracuda_logs/index.ts b/src/plugins/home/server/tutorials/barracuda_logs/index.ts index cdfd75b9728b9..7cf333ec6f7e5 100644 --- a/src/plugins/home/server/tutorials/barracuda_logs/index.ts +++ b/src/plugins/home/server/tutorials/barracuda_logs/index.ts @@ -55,8 +55,8 @@ export function barracudaLogsSpecProvider(context: TutorialContext): TutorialSch }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/bluecoat_logs/index.ts b/src/plugins/home/server/tutorials/bluecoat_logs/index.ts index a7db5b04ee40d..f35cd0ac4e450 100644 --- a/src/plugins/home/server/tutorials/bluecoat_logs/index.ts +++ b/src/plugins/home/server/tutorials/bluecoat_logs/index.ts @@ -54,8 +54,8 @@ export function bluecoatLogsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/cef_logs/index.ts b/src/plugins/home/server/tutorials/cef_logs/index.ts index 1366198d610d7..bf1f402a09a65 100644 --- a/src/plugins/home/server/tutorials/cef_logs/index.ts +++ b/src/plugins/home/server/tutorials/cef_logs/index.ts @@ -61,8 +61,8 @@ export function cefLogsSpecProvider(context: TutorialContext): TutorialSchema { }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/ceph_metrics/index.ts b/src/plugins/home/server/tutorials/ceph_metrics/index.ts index 6a53789d26f7c..e7d2c67ec2a99 100644 --- a/src/plugins/home/server/tutorials/ceph_metrics/index.ts +++ b/src/plugins/home/server/tutorials/ceph_metrics/index.ts @@ -54,8 +54,8 @@ export function cephMetricsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/checkpoint_logs/index.ts b/src/plugins/home/server/tutorials/checkpoint_logs/index.ts index b5ea6be42403b..83ce8d27ec861 100644 --- a/src/plugins/home/server/tutorials/checkpoint_logs/index.ts +++ b/src/plugins/home/server/tutorials/checkpoint_logs/index.ts @@ -54,8 +54,8 @@ export function checkpointLogsSpecProvider(context: TutorialContext): TutorialSc }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/cisco_logs/index.ts b/src/plugins/home/server/tutorials/cisco_logs/index.ts index 922cfbf1e23ee..3c855996873af 100644 --- a/src/plugins/home/server/tutorials/cisco_logs/index.ts +++ b/src/plugins/home/server/tutorials/cisco_logs/index.ts @@ -57,8 +57,8 @@ export function ciscoLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/cisco_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts index 5564d11be4d19..a4172fae4ff4d 100644 --- a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts +++ b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts @@ -51,8 +51,8 @@ export function cloudwatchLogsSpecProvider(context: TutorialContext): TutorialSc }, completionTimeMinutes: 10, onPrem: onPremInstructions([], context), - elasticCloud: cloudInstructions(), - onPremElasticCloud: onPremCloudInstructions(), + elasticCloud: cloudInstructions(context), + onPremElasticCloud: onPremCloudInstructions(context), integrationBrowserCategories: ['aws', 'cloud', 'datastore', 'security', 'network'], }; } diff --git a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts index 535c8aaa90768..d53fd7f1f73aa 100644 --- a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts @@ -59,8 +59,8 @@ export function cockroachdbMetricsSpecProvider(context: TutorialContext): Tutori completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/cockroachdb_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security', 'network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/consul_metrics/index.ts b/src/plugins/home/server/tutorials/consul_metrics/index.ts index ca7179d55fd89..26fff9e58f511 100644 --- a/src/plugins/home/server/tutorials/consul_metrics/index.ts +++ b/src/plugins/home/server/tutorials/consul_metrics/index.ts @@ -56,8 +56,8 @@ export function consulMetricsSpecProvider(context: TutorialContext): TutorialSch completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/consul_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security', 'network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/coredns_logs/index.ts b/src/plugins/home/server/tutorials/coredns_logs/index.ts index 1261c67135001..876e6e09d61d6 100644 --- a/src/plugins/home/server/tutorials/coredns_logs/index.ts +++ b/src/plugins/home/server/tutorials/coredns_logs/index.ts @@ -57,8 +57,8 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/coredns_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security', 'network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/coredns_metrics/index.ts b/src/plugins/home/server/tutorials/coredns_metrics/index.ts index 3abc14314a6ba..b854f4d448361 100644 --- a/src/plugins/home/server/tutorials/coredns_metrics/index.ts +++ b/src/plugins/home/server/tutorials/coredns_metrics/index.ts @@ -54,8 +54,8 @@ export function corednsMetricsSpecProvider(context: TutorialContext): TutorialSc completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/coredns_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security', 'network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/couchbase_metrics/index.ts b/src/plugins/home/server/tutorials/couchbase_metrics/index.ts index 5c29aa2d9a524..2a71a6d0457f1 100644 --- a/src/plugins/home/server/tutorials/couchbase_metrics/index.ts +++ b/src/plugins/home/server/tutorials/couchbase_metrics/index.ts @@ -54,8 +54,8 @@ export function couchbaseMetricsSpecProvider(context: TutorialContext): Tutorial }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security', 'network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts index 00bea11d13d99..a379b3b04f4c7 100644 --- a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts @@ -59,8 +59,8 @@ export function couchdbMetricsSpecProvider(context: TutorialContext): TutorialSc completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/couchdb_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security', 'network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts b/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts index a48ed4288210b..2c5a32b63f75f 100644 --- a/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts +++ b/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts @@ -58,8 +58,8 @@ export function crowdstrikeLogsSpecProvider(context: TutorialContext): TutorialS }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/cylance_logs/index.ts b/src/plugins/home/server/tutorials/cylance_logs/index.ts index 64b79a41cd2e0..d8b72963678fa 100644 --- a/src/plugins/home/server/tutorials/cylance_logs/index.ts +++ b/src/plugins/home/server/tutorials/cylance_logs/index.ts @@ -54,8 +54,8 @@ export function cylanceLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/docker_metrics/index.ts b/src/plugins/home/server/tutorials/docker_metrics/index.ts index ab80e6d644dbc..e36d590650454 100644 --- a/src/plugins/home/server/tutorials/docker_metrics/index.ts +++ b/src/plugins/home/server/tutorials/docker_metrics/index.ts @@ -56,8 +56,8 @@ export function dockerMetricsSpecProvider(context: TutorialContext): TutorialSch completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/docker_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['containers', 'os_system'], }; } diff --git a/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts b/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts index 9864d376966bb..f01119e6ba1d2 100644 --- a/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts +++ b/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts @@ -54,8 +54,8 @@ export function dropwizardMetricsSpecProvider(context: TutorialContext): Tutoria }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['elastic_stack', 'datastore'], }; } diff --git a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts index 6415781d02c06..a1df2d8a4085e 100644 --- a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts +++ b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts @@ -56,8 +56,8 @@ export function elasticsearchLogsSpecProvider(context: TutorialContext): Tutoria completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/elasticsearch_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['containers', 'os_system'], }; } diff --git a/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts b/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts index 3961d7f78c86c..009e441c725d9 100644 --- a/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts +++ b/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts @@ -54,8 +54,8 @@ export function elasticsearchMetricsSpecProvider(context: TutorialContext): Tuto }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['elastic_stack', 'datastore'], }; } diff --git a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts index 55c85a5bdd2a4..d39b182b81eaf 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts @@ -60,8 +60,8 @@ export function envoyproxyLogsSpecProvider(context: TutorialContext): TutorialSc completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/envoyproxy_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['elastic_stack', 'datastore'], }; } diff --git a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts index e2f3b84739685..84ea8099e3d93 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts @@ -47,8 +47,8 @@ export function envoyproxyMetricsSpecProvider(context: TutorialContext): Tutoria }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['elastic_stack', 'datastore'], }; } diff --git a/src/plugins/home/server/tutorials/etcd_metrics/index.ts b/src/plugins/home/server/tutorials/etcd_metrics/index.ts index 9ed153c21c257..c4c68e80d40eb 100644 --- a/src/plugins/home/server/tutorials/etcd_metrics/index.ts +++ b/src/plugins/home/server/tutorials/etcd_metrics/index.ts @@ -54,8 +54,8 @@ export function etcdMetricsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['elastic_stack', 'datastore'], }; } diff --git a/src/plugins/home/server/tutorials/f5_logs/index.ts b/src/plugins/home/server/tutorials/f5_logs/index.ts index a407d1d3d5142..381fdd487eb24 100644 --- a/src/plugins/home/server/tutorials/f5_logs/index.ts +++ b/src/plugins/home/server/tutorials/f5_logs/index.ts @@ -55,8 +55,8 @@ export function f5LogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/f5_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/fortinet_logs/index.ts b/src/plugins/home/server/tutorials/fortinet_logs/index.ts index 2f6af3ba47280..6a73c5f8e3f66 100644 --- a/src/plugins/home/server/tutorials/fortinet_logs/index.ts +++ b/src/plugins/home/server/tutorials/fortinet_logs/index.ts @@ -54,8 +54,8 @@ export function fortinetLogsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/gcp_logs/index.ts b/src/plugins/home/server/tutorials/gcp_logs/index.ts index 23d8e3364eb69..d02c08cd2be9a 100644 --- a/src/plugins/home/server/tutorials/gcp_logs/index.ts +++ b/src/plugins/home/server/tutorials/gcp_logs/index.ts @@ -59,8 +59,8 @@ export function gcpLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/gcp_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['google_cloud', 'cloud', 'network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/gcp_metrics/index.ts b/src/plugins/home/server/tutorials/gcp_metrics/index.ts index 7f397c1e1be7b..ea5351d010a42 100644 --- a/src/plugins/home/server/tutorials/gcp_metrics/index.ts +++ b/src/plugins/home/server/tutorials/gcp_metrics/index.ts @@ -57,8 +57,8 @@ export function gcpMetricsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/gcp_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['google_cloud', 'cloud', 'network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/golang_metrics/index.ts b/src/plugins/home/server/tutorials/golang_metrics/index.ts index 50d09e42e8791..e179e69734ad5 100644 --- a/src/plugins/home/server/tutorials/golang_metrics/index.ts +++ b/src/plugins/home/server/tutorials/golang_metrics/index.ts @@ -57,8 +57,8 @@ export function golangMetricsSpecProvider(context: TutorialContext): TutorialSch }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['google_cloud', 'cloud', 'network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/gsuite_logs/index.ts b/src/plugins/home/server/tutorials/gsuite_logs/index.ts index 718558321cf78..ba193bdb08c08 100644 --- a/src/plugins/home/server/tutorials/gsuite_logs/index.ts +++ b/src/plugins/home/server/tutorials/gsuite_logs/index.ts @@ -54,8 +54,8 @@ export function gsuiteLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/haproxy_logs/index.ts b/src/plugins/home/server/tutorials/haproxy_logs/index.ts index c3765317ecbe0..05fc23fa16bcd 100644 --- a/src/plugins/home/server/tutorials/haproxy_logs/index.ts +++ b/src/plugins/home/server/tutorials/haproxy_logs/index.ts @@ -57,8 +57,8 @@ export function haproxyLogsSpecProvider(context: TutorialContext): TutorialSchem completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/haproxy_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts index 49f1d32dc4c82..fa7c451889ba3 100644 --- a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts +++ b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts @@ -54,8 +54,8 @@ export function haproxyMetricsSpecProvider(context: TutorialContext): TutorialSc }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts index 21b60a9ab5a5c..90b35d0e78842 100644 --- a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts +++ b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts @@ -56,8 +56,8 @@ export function ibmmqLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/ibmmq_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts index 706003f0eab48..6329df6836b06 100644 --- a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts @@ -55,8 +55,8 @@ export function ibmmqMetricsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/ibmmq_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/icinga_logs/index.ts b/src/plugins/home/server/tutorials/icinga_logs/index.ts index dc730022262c2..c65e92d0fe856 100644 --- a/src/plugins/home/server/tutorials/icinga_logs/index.ts +++ b/src/plugins/home/server/tutorials/icinga_logs/index.ts @@ -57,8 +57,8 @@ export function icingaLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/icinga_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/iis_logs/index.ts b/src/plugins/home/server/tutorials/iis_logs/index.ts index 0dbc5bbdc75b8..423f2f917c84e 100644 --- a/src/plugins/home/server/tutorials/iis_logs/index.ts +++ b/src/plugins/home/server/tutorials/iis_logs/index.ts @@ -58,8 +58,8 @@ export function iisLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/iis_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/iis_metrics/index.ts b/src/plugins/home/server/tutorials/iis_metrics/index.ts index d57e4688ba753..3c3159c2838d1 100644 --- a/src/plugins/home/server/tutorials/iis_metrics/index.ts +++ b/src/plugins/home/server/tutorials/iis_metrics/index.ts @@ -57,8 +57,8 @@ export function iisMetricsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/iis_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/imperva_logs/index.ts b/src/plugins/home/server/tutorials/imperva_logs/index.ts index 1cbe707f813ee..35e0a668ec7f0 100644 --- a/src/plugins/home/server/tutorials/imperva_logs/index.ts +++ b/src/plugins/home/server/tutorials/imperva_logs/index.ts @@ -54,8 +54,8 @@ export function impervaLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/infoblox_logs/index.ts b/src/plugins/home/server/tutorials/infoblox_logs/index.ts index 8dce2bf00b2e2..21d1fcf9a156c 100644 --- a/src/plugins/home/server/tutorials/infoblox_logs/index.ts +++ b/src/plugins/home/server/tutorials/infoblox_logs/index.ts @@ -54,8 +54,8 @@ export function infobloxLogsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network'], }; } diff --git a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts index d0a0f97e26037..3968aff312380 100644 --- a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts @@ -13,271 +13,317 @@ import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; import { cloudPasswordAndResetLink } from './cloud_instructions'; -export const createAuditbeatInstructions = (context?: TutorialContext) => ({ - INSTALL: { - OSX: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.osxTitle', { - defaultMessage: 'Download and install Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', - values: { - linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'tar xzvf auditbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'cd auditbeat-{config.kibana.version}-darwin-x86_64/', - ], - }, - DEB: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.debTitle', { - defaultMessage: 'Download and install Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', - values: { - linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-amd64.deb', - 'sudo dpkg -i auditbeat-{config.kibana.version}-amd64.deb', - ], - textPost: i18n.translate('home.tutorials.common.auditbeatInstructions.install.debTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', - values: { - linkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', - }, - }), - }, - RPM: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.rpmTitle', { - defaultMessage: 'Download and install Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', - values: { - linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-x86_64.rpm', - 'sudo rpm -vi auditbeat-{config.kibana.version}-x86_64.rpm', - ], - textPost: i18n.translate('home.tutorials.common.auditbeatInstructions.install.rpmTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', - values: { - linkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', - }, - }), - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.windowsTitle', { - defaultMessage: 'Download and install Auditbeat', - }), - textPre: i18n.translate( - 'home.tutorials.common.auditbeatInstructions.install.windowsTextPre', - { - defaultMessage: - 'First time using Auditbeat? See the [Quick Start]({guideLinkUrl}).\n\ +export const createAuditbeatInstructions = (context: TutorialContext) => { + const SSL_DOC_URL = `https://www.elastic.co/guide/en/beats/auditbeat/${context.kibanaBranch}/configuration-ssl.html#ca-sha256`; + + return { + INSTALL: { + OSX: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Auditbeat', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'tar xzvf auditbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'cd auditbeat-{config.kibana.version}-darwin-x86_64/', + ], + }, + DEB: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Auditbeat', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-amd64.deb', + 'sudo dpkg -i auditbeat-{config.kibana.version}-amd64.deb', + ], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.install.debTextPost', + { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', + }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Auditbeat', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-x86_64.rpm', + 'sudo rpm -vi auditbeat-{config.kibana.version}-x86_64.rpm', + ], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.install.rpmTextPost', + { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', + }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Auditbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.install.windowsTextPre', + { + defaultMessage: + 'First time using Auditbeat? See the [Quick Start]({guideLinkUrl}).\n\ 1. Download the Auditbeat Windows zip file from the [Download]({auditbeatLinkUrl}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the `{directoryName}` directory to `Auditbeat`.\n\ 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ 5. From the PowerShell prompt, run the following commands to install Auditbeat as a Windows service.', + values: { + folderPath: '`C:\\Program Files`', + guideLinkUrl: + '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', + auditbeatLinkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', + directoryName: 'auditbeat-{config.kibana.version}-windows', + }, + } + ), + commands: ['cd "C:\\Program Files\\Auditbeat"', '.\\install-service-auditbeat.ps1'], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.install.windowsTextPost', + { + defaultMessage: + 'Modify the settings under {propertyName} in the {auditbeatPath} file to point to your Elasticsearch installation.', + values: { + propertyName: '`output.elasticsearch`', + auditbeatPath: '`C:\\Program Files\\Auditbeat\\auditbeat.yml`', + }, + } + ), + }, + }, + START: { + OSX: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.osxTitle', { + defaultMessage: 'Start Auditbeat', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.osxTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['./auditbeat setup', './auditbeat -e'], + }, + DEB: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.debTitle', { + defaultMessage: 'Start Auditbeat', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.debTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['sudo auditbeat setup', 'sudo service auditbeat start'], + }, + RPM: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Auditbeat', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.rpmTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['sudo auditbeat setup', 'sudo service auditbeat start'], + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Auditbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.start.windowsTextPre', + { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + } + ), + commands: ['.\\auditbeat.exe setup', 'Start-Service auditbeat'], + }, + }, + CONFIG: { + OSX: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - folderPath: '`C:\\Program Files`', - guideLinkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', - auditbeatLinkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', - directoryName: 'auditbeat-{config.kibana.version}-windows', + path: '`auditbeat.yml`', }, - } - ), - commands: ['cd "C:\\Program Files\\Auditbeat"', '.\\install-service-auditbeat.ps1'], - textPost: i18n.translate( - 'home.tutorials.common.auditbeatInstructions.install.windowsTextPost', - { - defaultMessage: - 'Modify the settings under {propertyName} in the {auditbeatPath} file to point to your Elasticsearch installation.', + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.config.osxTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + DEB: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - propertyName: '`output.elasticsearch`', - auditbeatPath: '`C:\\Program Files\\Auditbeat\\auditbeat.yml`', + path: '`/etc/auditbeat/auditbeat.yml`', }, - } - ), - }, - }, - START: { - OSX: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.osxTitle', { - defaultMessage: 'Start Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.osxTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['./auditbeat setup', './auditbeat -e'], - }, - DEB: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.debTitle', { - defaultMessage: 'Start Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.debTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['sudo auditbeat setup', 'sudo service auditbeat start'], - }, - RPM: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.rpmTitle', { - defaultMessage: 'Start Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.rpmTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['sudo auditbeat setup', 'sudo service auditbeat start'], - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.windowsTitle', { - defaultMessage: 'Start Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.windowsTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['.\\auditbeat.exe setup', 'Start-Service auditbeat'], - }, - }, - CONFIG: { - OSX: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.osxTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.osxTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`auditbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.auditbeatInstructions.config.osxTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - DEB: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.debTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.debTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/auditbeat/auditbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.auditbeatInstructions.config.debTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - RPM: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.rpmTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.rpmTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/auditbeat/auditbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.auditbeatInstructions.config.rpmTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.windowsTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`C:\\Program Files\\Auditbeat\\auditbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.auditbeatInstructions.config.windowsTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.config.debTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', + path: '`/etc/auditbeat/auditbeat.yml`', }, - } - ), + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.config.rpmTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.config.windowsTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Auditbeat\\auditbeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.config.windowsTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, }, - }, -}); + }; +}; export const createAuditbeatCloudInstructions = () => ({ CONFIG: { @@ -383,7 +429,7 @@ export function auditbeatStatusCheck() { }; } -export function onPremInstructions(platforms: readonly Platform[], context?: TutorialContext) { +export function onPremInstructions(platforms: readonly Platform[], context: TutorialContext) { const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(context); const variants = []; @@ -414,8 +460,8 @@ export function onPremInstructions(platforms: readonly Platform[], context?: Tut }; } -export function onPremCloudInstructions(platforms: readonly Platform[]) { - const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(); +export function onPremCloudInstructions(platforms: readonly Platform[], context: TutorialContext) { + const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(context); const TRYCLOUD_OPTION1 = createTrycloudOption1(); const TRYCLOUD_OPTION2 = createTrycloudOption2(); @@ -450,8 +496,8 @@ export function onPremCloudInstructions(platforms: readonly Platform[]) { }; } -export function cloudInstructions(platforms: readonly Platform[]) { - const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(); +export function cloudInstructions(platforms: readonly Platform[], context: TutorialContext) { + const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(context); const AUDITBEAT_CLOUD_INSTRUCTIONS = createAuditbeatCloudInstructions(); const variants = []; diff --git a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts index c6aa44932ee45..89445510f2b3d 100644 --- a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts @@ -13,268 +13,307 @@ import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; import { cloudPasswordAndResetLink } from './cloud_instructions'; -export const createFilebeatInstructions = (context?: TutorialContext) => ({ - INSTALL: { - OSX: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.install.osxTitle', { - defaultMessage: 'Download and install Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', - values: { - linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'tar xzvf filebeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'cd filebeat-{config.kibana.version}-darwin-x86_64/', - ], - }, - DEB: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTitle', { - defaultMessage: 'Download and install Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', - values: { - linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-amd64.deb', - 'sudo dpkg -i filebeat-{config.kibana.version}-amd64.deb', - ], - textPost: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', - values: { - linkUrl: 'https://www.elastic.co/downloads/beats/filebeat', - }, - }), - }, - RPM: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTitle', { - defaultMessage: 'Download and install Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', - values: { - linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-x86_64.rpm', - 'sudo rpm -vi filebeat-{config.kibana.version}-x86_64.rpm', - ], - textPost: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', - values: { - linkUrl: 'https://www.elastic.co/downloads/beats/filebeat', - }, - }), - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.install.windowsTitle', { - defaultMessage: 'Download and install Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.windowsTextPre', { - defaultMessage: - 'First time using Filebeat? See the [Quick Start]({guideLinkUrl}).\n\ +export const createFilebeatInstructions = (context: TutorialContext) => { + const SSL_DOC_URL = `https://www.elastic.co/guide/en/beats/filebeat/${context.kibanaBranch}/configuration-ssl.html#ca-sha256`; + + return { + INSTALL: { + OSX: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'tar xzvf filebeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'cd filebeat-{config.kibana.version}-darwin-x86_64/', + ], + }, + DEB: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-amd64.deb', + 'sudo dpkg -i filebeat-{config.kibana.version}-amd64.deb', + ], + textPost: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/filebeat', + }, + }), + }, + RPM: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-x86_64.rpm', + 'sudo rpm -vi filebeat-{config.kibana.version}-x86_64.rpm', + ], + textPost: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/filebeat', + }, + }), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.filebeatInstructions.install.windowsTextPre', + { + defaultMessage: + 'First time using Filebeat? See the [Quick Start]({guideLinkUrl}).\n\ 1. Download the Filebeat Windows zip file from the [Download]({filebeatLinkUrl}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the `{directoryName}` directory to `Filebeat`.\n\ 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ 5. From the PowerShell prompt, run the following commands to install Filebeat as a Windows service.', - values: { - folderPath: '`C:\\Program Files`', - guideLinkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', - filebeatLinkUrl: 'https://www.elastic.co/downloads/beats/filebeat', - directoryName: 'filebeat-{config.kibana.version}-windows', - }, - }), - commands: ['cd "C:\\Program Files\\Filebeat"', '.\\install-service-filebeat.ps1'], - textPost: i18n.translate( - 'home.tutorials.common.filebeatInstructions.install.windowsTextPost', - { + values: { + folderPath: '`C:\\Program Files`', + guideLinkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', + filebeatLinkUrl: 'https://www.elastic.co/downloads/beats/filebeat', + directoryName: 'filebeat-{config.kibana.version}-windows', + }, + } + ), + commands: ['cd "C:\\Program Files\\Filebeat"', '.\\install-service-filebeat.ps1'], + textPost: i18n.translate( + 'home.tutorials.common.filebeatInstructions.install.windowsTextPost', + { + defaultMessage: + 'Modify the settings under {propertyName} in the {filebeatPath} file to point to your Elasticsearch installation.', + values: { + propertyName: '`output.elasticsearch`', + filebeatPath: '`C:\\Program Files\\Filebeat\\filebeat.yml`', + }, + } + ), + }, + }, + START: { + OSX: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.start.osxTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.osxTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['./filebeat setup', './filebeat -e'], + }, + DEB: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.start.debTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.debTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['sudo filebeat setup', 'sudo service filebeat start'], + }, + RPM: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.rpmTextPre', { defaultMessage: - 'Modify the settings under {propertyName} in the {filebeatPath} file to point to your Elasticsearch installation.', + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['sudo filebeat setup', 'sudo service filebeat start'], + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.windowsTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['.\\filebeat.exe setup', 'Start-Service filebeat'], + }, + }, + CONFIG: { + OSX: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - propertyName: '`output.elasticsearch`', - filebeatPath: '`C:\\Program Files\\Filebeat\\filebeat.yml`', + path: '`filebeat.yml`', }, - } - ), - }, - }, - START: { - OSX: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.start.osxTitle', { - defaultMessage: 'Start Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.osxTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['./filebeat setup', './filebeat -e'], - }, - DEB: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.start.debTitle', { - defaultMessage: 'Start Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.debTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['sudo filebeat setup', 'sudo service filebeat start'], - }, - RPM: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.start.rpmTitle', { - defaultMessage: 'Start Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.rpmTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['sudo filebeat setup', 'sudo service filebeat start'], - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.start.windowsTitle', { - defaultMessage: 'Start Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.windowsTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['.\\filebeat.exe setup', 'Start-Service filebeat'], - }, - }, - CONFIG: { - OSX: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.config.osxTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.osxTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`filebeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.filebeatInstructions.config.osxTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - DEB: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.config.debTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.debTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/filebeat/filebeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.filebeatInstructions.config.debTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - RPM: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.config.rpmTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.rpmTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/filebeat/filebeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.filebeatInstructions.config.rpmTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.config.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.windowsTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`C:\\Program Files\\Filebeat\\filebeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.filebeatInstructions.config.windowsTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.filebeatInstructions.config.osxTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + DEB: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', + path: '`/etc/filebeat/filebeat.yml`', }, - } - ), + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.filebeatInstructions.config.debTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/filebeat/filebeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.filebeatInstructions.config.rpmTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate( + 'home.tutorials.common.filebeatInstructions.config.windowsTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Filebeat\\filebeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.filebeatInstructions.config.windowsTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, }, - }, -}); + }; +}; export const createFilebeatCloudInstructions = () => ({ CONFIG: { @@ -430,7 +469,7 @@ export function filebeatStatusCheck(moduleName: string) { export function onPremInstructions( moduleName: string, platforms: readonly Platform[] = [], - context?: TutorialContext + context: TutorialContext ) { const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(context); @@ -463,8 +502,12 @@ export function onPremInstructions( }; } -export function onPremCloudInstructions(moduleName: string, platforms: readonly Platform[] = []) { - const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(); +export function onPremCloudInstructions( + moduleName: string, + platforms: readonly Platform[] = [], + context: TutorialContext +) { + const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(context); const TRYCLOUD_OPTION1 = createTrycloudOption1(); const TRYCLOUD_OPTION2 = createTrycloudOption2(); @@ -500,8 +543,12 @@ export function onPremCloudInstructions(moduleName: string, platforms: readonly }; } -export function cloudInstructions(moduleName: string, platforms: readonly Platform[] = []) { - const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(); +export function cloudInstructions( + moduleName: string, + platforms: readonly Platform[] = [], + context: TutorialContext +) { + const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(context); const FILEBEAT_CLOUD_INSTRUCTIONS = createFilebeatCloudInstructions(); const variants = []; diff --git a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts index 24a6fe3719f8f..60d6fa5cb813b 100644 --- a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts @@ -13,171 +13,203 @@ import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; import { cloudPasswordAndResetLink } from './cloud_instructions'; -export const createFunctionbeatInstructions = (context?: TutorialContext) => ({ - INSTALL: { - OSX: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.install.osxTitle', { - defaultMessage: 'Download and install Functionbeat', - }), - textPre: i18n.translate('home.tutorials.common.functionbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Functionbeat? See the [Quick Start]({link}).', - values: { - link: '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/functionbeat/functionbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'tar xzvf functionbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'cd functionbeat-{config.kibana.version}-darwin-x86_64/', - ], - }, - LINUX: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.install.linuxTitle', { - defaultMessage: 'Download and install Functionbeat', - }), - textPre: i18n.translate( - 'home.tutorials.common.functionbeatInstructions.install.linuxTextPre', - { - defaultMessage: 'First time using Functionbeat? See the [Quick Start]({link}).', - values: { - link: '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', - }, - } - ), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/functionbeat/functionbeat-{config.kibana.version}-linux-x86_64.tar.gz', - 'tar xzvf functionbeat-{config.kibana.version}-linux-x86_64.tar.gz', - 'cd functionbeat-{config.kibana.version}-linux-x86_64/', - ], - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.install.windowsTitle', { - defaultMessage: 'Download and install Functionbeat', - }), - textPre: i18n.translate( - 'home.tutorials.common.functionbeatInstructions.install.windowsTextPre', - { - defaultMessage: - 'First time using Functionbeat? See the [Quick Start]({functionbeatLink}).\n\ +export const createFunctionbeatInstructions = (context: TutorialContext) => { + const SSL_DOC_URL = `https://www.elastic.co/guide/en/beats/functionbeat/${context.kibanaBranch}/configuration-ssl.html#ca-sha256`; + + return { + INSTALL: { + OSX: { + title: i18n.translate('home.tutorials.common.functionbeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Functionbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.install.osxTextPre', + { + defaultMessage: 'First time using Functionbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', + }, + } + ), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/functionbeat/functionbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'tar xzvf functionbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'cd functionbeat-{config.kibana.version}-darwin-x86_64/', + ], + }, + LINUX: { + title: i18n.translate('home.tutorials.common.functionbeatInstructions.install.linuxTitle', { + defaultMessage: 'Download and install Functionbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.install.linuxTextPre', + { + defaultMessage: 'First time using Functionbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', + }, + } + ), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/functionbeat/functionbeat-{config.kibana.version}-linux-x86_64.tar.gz', + 'tar xzvf functionbeat-{config.kibana.version}-linux-x86_64.tar.gz', + 'cd functionbeat-{config.kibana.version}-linux-x86_64/', + ], + }, + WINDOWS: { + title: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.install.windowsTitle', + { + defaultMessage: 'Download and install Functionbeat', + } + ), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.install.windowsTextPre', + { + defaultMessage: + 'First time using Functionbeat? See the [Quick Start]({functionbeatLink}).\n\ 1. Download the Functionbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Functionbeat`.\n\ 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ 5. From the PowerShell prompt, go to the Functionbeat directory:', - values: { - directoryName: '`functionbeat-{config.kibana.version}-windows`', - folderPath: '`C:\\Program Files`', - functionbeatLink: - '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', - elasticLink: 'https://www.elastic.co/downloads/beats/functionbeat', - }, - } - ), - commands: ['cd "C:\\Program Files\\Functionbeat"'], + values: { + directoryName: '`functionbeat-{config.kibana.version}-windows`', + folderPath: '`C:\\Program Files`', + functionbeatLink: + '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', + elasticLink: 'https://www.elastic.co/downloads/beats/functionbeat', + }, + } + ), + commands: ['cd "C:\\Program Files\\Functionbeat"'], + }, }, - }, - DEPLOY: { - OSX_LINUX: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.deploy.osxTitle', { - defaultMessage: 'Deploy Functionbeat to AWS Lambda', - }), - textPre: i18n.translate('home.tutorials.common.functionbeatInstructions.deploy.osxTextPre', { - defaultMessage: - 'This installs Functionbeat as a Lambda function.\ + DEPLOY: { + OSX_LINUX: { + title: i18n.translate('home.tutorials.common.functionbeatInstructions.deploy.osxTitle', { + defaultMessage: 'Deploy Functionbeat to AWS Lambda', + }), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.deploy.osxTextPre', + { + defaultMessage: + 'This installs Functionbeat as a Lambda function.\ The `setup` command checks the Elasticsearch configuration and loads the \ Kibana index pattern. It is normally safe to omit this command.', - }), - commands: ['./functionbeat setup', './functionbeat deploy fn-cloudwatch-logs'], - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.deploy.windowsTitle', { - defaultMessage: 'Deploy Functionbeat to AWS Lambda', - }), - textPre: i18n.translate( - 'home.tutorials.common.functionbeatInstructions.deploy.windowsTextPre', - { - defaultMessage: - 'This installs Functionbeat as a Lambda function.\ + } + ), + commands: ['./functionbeat setup', './functionbeat deploy fn-cloudwatch-logs'], + }, + WINDOWS: { + title: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.deploy.windowsTitle', + { + defaultMessage: 'Deploy Functionbeat to AWS Lambda', + } + ), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.deploy.windowsTextPre', + { + defaultMessage: + 'This installs Functionbeat as a Lambda function.\ The `setup` command checks the Elasticsearch configuration and loads the \ Kibana index pattern. It is normally safe to omit this command.', - } - ), - commands: ['.\\functionbeat.exe setup', '.\\functionbeat.exe deploy fn-cloudwatch-logs'], - }, - }, - CONFIG: { - OSX_LINUX: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.config.osxTitle', { - defaultMessage: 'Configure the Elastic cluster', - }), - textPre: i18n.translate('home.tutorials.common.functionbeatInstructions.config.osxTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`functionbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.functionbeatInstructions.config.osxTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - } - ), + } + ), + commands: ['.\\functionbeat.exe setup', '.\\functionbeat.exe deploy fn-cloudwatch-logs'], + }, }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.config.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate( - 'home.tutorials.common.functionbeatInstructions.config.windowsTextPre', - { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`C:\\Program Files\\Functionbeat\\functionbeat.yml`', - }, - } - ), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.functionbeatInstructions.config.windowsTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - } - ), + CONFIG: { + OSX_LINUX: { + title: i18n.translate('home.tutorials.common.functionbeatInstructions.config.osxTitle', { + defaultMessage: 'Configure the Elastic cluster', + }), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.config.osxTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`functionbeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.config.osxTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + WINDOWS: { + title: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.config.windowsTitle', + { + defaultMessage: 'Edit the configuration', + } + ), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.config.windowsTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Functionbeat\\functionbeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.config.windowsTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, }, - }, -}); + }; +}; export const createFunctionbeatCloudInstructions = () => ({ CONFIG: { @@ -336,7 +368,7 @@ export function functionbeatStatusCheck() { }; } -export function onPremInstructions(platforms: Platform[], context?: TutorialContext) { +export function onPremInstructions(platforms: Platform[], context: TutorialContext) { const FUNCTIONBEAT_INSTRUCTIONS = createFunctionbeatInstructions(context); return { @@ -386,10 +418,10 @@ export function onPremInstructions(platforms: Platform[], context?: TutorialCont }; } -export function onPremCloudInstructions() { +export function onPremCloudInstructions(context: TutorialContext) { const TRYCLOUD_OPTION1 = createTrycloudOption1(); const TRYCLOUD_OPTION2 = createTrycloudOption2(); - const FUNCTIONBEAT_INSTRUCTIONS = createFunctionbeatInstructions(); + const FUNCTIONBEAT_INSTRUCTIONS = createFunctionbeatInstructions(context); return { instructionSets: [ @@ -444,8 +476,8 @@ export function onPremCloudInstructions() { }; } -export function cloudInstructions() { - const FUNCTIONBEAT_INSTRUCTIONS = createFunctionbeatInstructions(); +export function cloudInstructions(context: TutorialContext) { + const FUNCTIONBEAT_INSTRUCTIONS = createFunctionbeatInstructions(context); const FUNCTIONBEAT_CLOUD_INSTRUCTIONS = createFunctionbeatCloudInstructions(); return { diff --git a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts index ce3e76a5f827e..5cbd1641bf09a 100644 --- a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts @@ -13,247 +13,298 @@ import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; import { cloudPasswordAndResetLink } from './cloud_instructions'; -export const createHeartbeatInstructions = (context?: TutorialContext) => ({ - INSTALL: { - OSX: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.osxTitle', { - defaultMessage: 'Download and install Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', - values: { link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html' }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'tar xzvf heartbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'cd heartbeat-{config.kibana.version}-darwin-x86_64/', - ], - }, - DEB: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTitle', { - defaultMessage: 'Download and install Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', - values: { link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html' }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-amd64.deb', - 'sudo dpkg -i heartbeat-{config.kibana.version}-amd64.deb', - ], - textPost: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', - values: { link: 'https://www.elastic.co/downloads/beats/heartbeat' }, - }), - }, - RPM: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.rpmTitle', { - defaultMessage: 'Download and install Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', - values: { link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html' }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-x86_64.rpm', - 'sudo rpm -vi heartbeat-{config.kibana.version}-x86_64.rpm', - ], - textPost: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', - values: { link: 'https://www.elastic.co/downloads/beats/heartbeat' }, - }), - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.windowsTitle', { - defaultMessage: 'Download and install Heartbeat', - }), - textPre: i18n.translate( - 'home.tutorials.common.heartbeatInstructions.install.windowsTextPre', - { - defaultMessage: - 'First time using Heartbeat? See the [Quick Start]({heartbeatLink}).\n\ +export const createHeartbeatInstructions = (context: TutorialContext) => { + const SSL_DOC_URL = `https://www.elastic.co/guide/en/beats/heartbeat/${context.kibanaBranch}/configuration-ssl.html#ca-sha256`; + + return { + INSTALL: { + OSX: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Heartbeat', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'tar xzvf heartbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'cd heartbeat-{config.kibana.version}-darwin-x86_64/', + ], + }, + DEB: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Heartbeat', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-amd64.deb', + 'sudo dpkg -i heartbeat-{config.kibana.version}-amd64.deb', + ], + textPost: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.install.debTextPost', + { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', + values: { link: 'https://www.elastic.co/downloads/beats/heartbeat' }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Heartbeat', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-x86_64.rpm', + 'sudo rpm -vi heartbeat-{config.kibana.version}-x86_64.rpm', + ], + textPost: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.install.debTextPost', + { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', + values: { link: 'https://www.elastic.co/downloads/beats/heartbeat' }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Heartbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.install.windowsTextPre', + { + defaultMessage: + 'First time using Heartbeat? See the [Quick Start]({heartbeatLink}).\n\ 1. Download the Heartbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Heartbeat`.\n\ 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ 5. From the PowerShell prompt, run the following commands to install Heartbeat as a Windows service.', - values: { - directoryName: '`heartbeat-{config.kibana.version}-windows`', - folderPath: '`C:\\Program Files`', - heartbeatLink: - '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', - elasticLink: 'https://www.elastic.co/downloads/beats/heartbeat', - }, - } - ), - commands: ['cd "C:\\Program Files\\Heartbeat"', '.\\install-service-heartbeat.ps1'], - }, - }, - START: { - OSX: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.osxTitle', { - defaultMessage: 'Start Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.osxTextPre', { - defaultMessage: 'The `setup` command loads the Kibana index pattern.', - }), - commands: ['./heartbeat setup', './heartbeat -e'], - }, - DEB: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.debTitle', { - defaultMessage: 'Start Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.debTextPre', { - defaultMessage: 'The `setup` command loads the Kibana index pattern.', - }), - commands: ['sudo heartbeat setup', 'sudo service heartbeat-elastic start'], - }, - RPM: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.rpmTitle', { - defaultMessage: 'Start Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.rpmTextPre', { - defaultMessage: 'The `setup` command loads the Kibana index pattern.', - }), - commands: ['sudo heartbeat setup', 'sudo service heartbeat-elastic start'], - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.windowsTitle', { - defaultMessage: 'Start Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.windowsTextPre', { - defaultMessage: 'The `setup` command loads the Kibana index pattern.', - }), - commands: ['.\\heartbeat.exe setup', 'Start-Service heartbeat'], - }, - }, - CONFIG: { - OSX: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.osxTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.osxTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`heartbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.heartbeatInstructions.config.osxTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - DEB: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.debTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.debTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/heartbeat/heartbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.heartbeatInstructions.config.debTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), + values: { + directoryName: '`heartbeat-{config.kibana.version}-windows`', + folderPath: '`C:\\Program Files`', + heartbeatLink: + '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', + elasticLink: 'https://www.elastic.co/downloads/beats/heartbeat', + }, + } + ), + commands: ['cd "C:\\Program Files\\Heartbeat"', '.\\install-service-heartbeat.ps1'], + }, }, - RPM: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.rpmTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.rpmTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/heartbeat/heartbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.heartbeatInstructions.config.rpmTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), + START: { + OSX: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.osxTitle', { + defaultMessage: 'Start Heartbeat', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.osxTextPre', { + defaultMessage: 'The `setup` command loads the Kibana index pattern.', + }), + commands: ['./heartbeat setup', './heartbeat -e'], + }, + DEB: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.debTitle', { + defaultMessage: 'Start Heartbeat', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.debTextPre', { + defaultMessage: 'The `setup` command loads the Kibana index pattern.', + }), + commands: ['sudo heartbeat setup', 'sudo service heartbeat-elastic start'], + }, + RPM: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Heartbeat', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.rpmTextPre', { + defaultMessage: 'The `setup` command loads the Kibana index pattern.', + }), + commands: ['sudo heartbeat setup', 'sudo service heartbeat-elastic start'], + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Heartbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.start.windowsTextPre', + { + defaultMessage: 'The `setup` command loads the Kibana index pattern.', + } + ), + commands: ['.\\heartbeat.exe setup', 'Start-Service heartbeat'], + }, }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.windowsTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`C:\\Program Files\\Heartbeat\\heartbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.heartbeatInstructions.config.windowsTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', + CONFIG: { + OSX: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', + path: '`heartbeat.yml`', }, - } - ), + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.config.osxTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + DEB: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/heartbeat/heartbeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.config.debTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/heartbeat/heartbeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.config.rpmTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.config.windowsTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Heartbeat\\heartbeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.config.windowsTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, }, - }, -}); + }; +}; export const createHeartbeatCloudInstructions = () => ({ CONFIG: { @@ -486,7 +537,7 @@ export function heartbeatStatusCheck() { }; } -export function onPremInstructions(platforms: Platform[], context?: TutorialContext) { +export function onPremInstructions(platforms: Platform[], context: TutorialContext) { const HEARTBEAT_INSTRUCTIONS = createHeartbeatInstructions(context); return { @@ -542,10 +593,10 @@ export function onPremInstructions(platforms: Platform[], context?: TutorialCont }; } -export function onPremCloudInstructions() { +export function onPremCloudInstructions(context: TutorialContext) { const TRYCLOUD_OPTION1 = createTrycloudOption1(); const TRYCLOUD_OPTION2 = createTrycloudOption2(); - const HEARTBEAT_INSTRUCTIONS = createHeartbeatInstructions(); + const HEARTBEAT_INSTRUCTIONS = createHeartbeatInstructions(context); return { instructionSets: [ @@ -608,8 +659,8 @@ export function onPremCloudInstructions() { }; } -export function cloudInstructions() { - const HEARTBEAT_INSTRUCTIONS = createHeartbeatInstructions(); +export function cloudInstructions(context: TutorialContext) { + const HEARTBEAT_INSTRUCTIONS = createHeartbeatInstructions(context); const HEARTBEAT_CLOUD_INSTRUCTIONS = createHeartbeatCloudInstructions(); return { diff --git a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts index d6f2fcb232f12..02cd53dddbc1f 100644 --- a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts @@ -13,268 +13,310 @@ import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; import { cloudPasswordAndResetLink } from './cloud_instructions'; -export const createMetricbeatInstructions = (context?: TutorialContext) => ({ - INSTALL: { - OSX: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.osxTitle', { - defaultMessage: 'Download and install Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', - values: { - link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'tar xzvf metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'cd metricbeat-{config.kibana.version}-darwin-x86_64/', - ], - }, - DEB: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTitle', { - defaultMessage: 'Download and install Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', - values: { - link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-amd64.deb', - 'sudo dpkg -i metricbeat-{config.kibana.version}-amd64.deb', - ], - textPost: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', - values: { link: 'https://www.elastic.co/downloads/beats/metricbeat' }, - }), - }, - RPM: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.rpmTitle', { - defaultMessage: 'Download and install Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', - values: { - link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-x86_64.rpm', - 'sudo rpm -vi metricbeat-{config.kibana.version}-x86_64.rpm', - ], - textPost: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', - values: { link: 'https://www.elastic.co/downloads/beats/metricbeat' }, - }), - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.windowsTitle', { - defaultMessage: 'Download and install Metricbeat', - }), - textPre: i18n.translate( - 'home.tutorials.common.metricbeatInstructions.install.windowsTextPre', - { - defaultMessage: - 'First time using Metricbeat? See the [Quick Start]({metricbeatLink}).\n\ +export const createMetricbeatInstructions = (context: TutorialContext) => { + const SSL_DOC_URL = `https://www.elastic.co/guide/en/beats/metricbeat/${context.kibanaBranch}/configuration-ssl.html#ca-sha256`; + + return { + INSTALL: { + OSX: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'tar xzvf metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'cd metricbeat-{config.kibana.version}-darwin-x86_64/', + ], + }, + DEB: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-amd64.deb', + 'sudo dpkg -i metricbeat-{config.kibana.version}-amd64.deb', + ], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.install.debTextPost', + { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', + values: { link: 'https://www.elastic.co/downloads/beats/metricbeat' }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-x86_64.rpm', + 'sudo rpm -vi metricbeat-{config.kibana.version}-x86_64.rpm', + ], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.install.debTextPost', + { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', + values: { link: 'https://www.elastic.co/downloads/beats/metricbeat' }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.install.windowsTextPre', + { + defaultMessage: + 'First time using Metricbeat? See the [Quick Start]({metricbeatLink}).\n\ 1. Download the Metricbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Metricbeat`.\n\ 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ 5. From the PowerShell prompt, run the following commands to install Metricbeat as a Windows service.', - values: { - directoryName: '`metricbeat-{config.kibana.version}-windows`', - folderPath: '`C:\\Program Files`', - metricbeatLink: - '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', - elasticLink: 'https://www.elastic.co/downloads/beats/metricbeat', - }, - } - ), - commands: ['cd "C:\\Program Files\\Metricbeat"', '.\\install-service-metricbeat.ps1'], - textPost: i18n.translate( - 'home.tutorials.common.metricbeatInstructions.install.windowsTextPost', - { - defaultMessage: - 'Modify the settings under `output.elasticsearch` in the {path} file to point to your Elasticsearch installation.', - values: { path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`' }, - } - ), - }, - }, - START: { - OSX: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.osxTitle', { - defaultMessage: 'Start Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.osxTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['./metricbeat setup', './metricbeat -e'], - }, - DEB: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.debTitle', { - defaultMessage: 'Start Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.debTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['sudo metricbeat setup', 'sudo service metricbeat start'], - }, - RPM: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.rpmTitle', { - defaultMessage: 'Start Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.rpmTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['sudo metricbeat setup', 'sudo service metricbeat start'], - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.windowsTitle', { - defaultMessage: 'Start Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.windowsTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['.\\metricbeat.exe setup', 'Start-Service metricbeat'], - }, - }, - CONFIG: { - OSX: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.osxTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.config.osxTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`metricbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.metricbeatInstructions.config.osxTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - DEB: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.debTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.config.debTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/metricbeat/metricbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.metricbeatInstructions.config.debTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), + values: { + directoryName: '`metricbeat-{config.kibana.version}-windows`', + folderPath: '`C:\\Program Files`', + metricbeatLink: + '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + elasticLink: 'https://www.elastic.co/downloads/beats/metricbeat', + }, + } + ), + commands: ['cd "C:\\Program Files\\Metricbeat"', '.\\install-service-metricbeat.ps1'], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.install.windowsTextPost', + { + defaultMessage: + 'Modify the settings under `output.elasticsearch` in the {path} file to point to your Elasticsearch installation.', + values: { path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`' }, + } + ), + }, }, - RPM: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.rpmTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.config.rpmTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/metricbeat/metricbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.metricbeatInstructions.config.rpmTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), + START: { + OSX: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.osxTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.osxTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['./metricbeat setup', './metricbeat -e'], + }, + DEB: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.debTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.debTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['sudo metricbeat setup', 'sudo service metricbeat start'], + }, + RPM: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.rpmTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['sudo metricbeat setup', 'sudo service metricbeat start'], + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.start.windowsTextPre', + { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + } + ), + commands: ['.\\metricbeat.exe setup', 'Start-Service metricbeat'], + }, }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate( - 'home.tutorials.common.metricbeatInstructions.config.windowsTextPre', - { + CONFIG: { + OSX: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.config.osxTextPre', { defaultMessage: 'Modify {path} to set the connection information:', values: { - path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`', + path: '`metricbeat.yml`', }, - } - ), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.metricbeatInstructions.config.windowsTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.config.osxTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + DEB: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', + path: '`/etc/metricbeat/metricbeat.yml`', }, - } - ), + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.config.debTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/metricbeat/metricbeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.config.rpmTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.config.windowsTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.config.windowsTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, }, - }, -}); + }; +}; export const createMetricbeatCloudInstructions = () => ({ CONFIG: { @@ -442,7 +484,7 @@ export function metricbeatStatusCheck(moduleName: string) { }; } -export function onPremInstructions(moduleName: string, context?: TutorialContext) { +export function onPremInstructions(moduleName: string, context: TutorialContext) { const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(context); return { @@ -498,10 +540,10 @@ export function onPremInstructions(moduleName: string, context?: TutorialContext }; } -export function onPremCloudInstructions(moduleName: string) { +export function onPremCloudInstructions(moduleName: string, context: TutorialContext) { const TRYCLOUD_OPTION1 = createTrycloudOption1(); const TRYCLOUD_OPTION2 = createTrycloudOption2(); - const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(); + const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(context); return { instructionSets: [ @@ -564,8 +606,8 @@ export function onPremCloudInstructions(moduleName: string) { }; } -export function cloudInstructions(moduleName: string) { - const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(); +export function cloudInstructions(moduleName: string, context: TutorialContext) { + const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(context); const METRICBEAT_CLOUD_INSTRUCTIONS = createMetricbeatCloudInstructions(); return { diff --git a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts index 7e90795448a6c..2c33285899f65 100644 --- a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts @@ -13,94 +13,106 @@ import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; import { cloudPasswordAndResetLink } from './cloud_instructions'; -export const createWinlogbeatInstructions = (context?: TutorialContext) => ({ - INSTALL: { - WINDOWS: { - title: i18n.translate('home.tutorials.common.winlogbeatInstructions.install.windowsTitle', { - defaultMessage: 'Download and install Winlogbeat', - }), - textPre: i18n.translate( - 'home.tutorials.common.winlogbeatInstructions.install.windowsTextPre', - { - defaultMessage: - 'First time using Winlogbeat? See the [Quick Start]({winlogbeatLink}).\n\ +export const createWinlogbeatInstructions = (context: TutorialContext) => { + const SSL_DOC_URL = `https://www.elastic.co/guide/en/beats/winlogbeat/${context.kibanaBranch}/configuration-ssl.html#ca-sha256`; + + return { + INSTALL: { + WINDOWS: { + title: i18n.translate('home.tutorials.common.winlogbeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Winlogbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.winlogbeatInstructions.install.windowsTextPre', + { + defaultMessage: + 'First time using Winlogbeat? See the [Quick Start]({winlogbeatLink}).\n\ 1. Download the Winlogbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Winlogbeat`.\n\ 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ 5. From the PowerShell prompt, run the following commands to install Winlogbeat as a Windows service.', - values: { - directoryName: '`winlogbeat-{config.kibana.version}-windows`', - folderPath: '`C:\\Program Files`', - winlogbeatLink: - '{config.docs.beats.winlogbeat}/winlogbeat-installation-configuration.html', - elasticLink: 'https://www.elastic.co/downloads/beats/winlogbeat', - }, - } - ), - commands: ['cd "C:\\Program Files\\Winlogbeat"', '.\\install-service-winlogbeat.ps1'], - textPost: i18n.translate( - 'home.tutorials.common.winlogbeatInstructions.install.windowsTextPost', - { - defaultMessage: - 'Modify the settings under `output.elasticsearch` in the {path} file to point to your Elasticsearch installation.', - values: { path: '`C:\\Program Files\\Winlogbeat\\winlogbeat.yml`' }, - } - ), + values: { + directoryName: '`winlogbeat-{config.kibana.version}-windows`', + folderPath: '`C:\\Program Files`', + winlogbeatLink: + '{config.docs.beats.winlogbeat}/winlogbeat-installation-configuration.html', + elasticLink: 'https://www.elastic.co/downloads/beats/winlogbeat', + }, + } + ), + commands: ['cd "C:\\Program Files\\Winlogbeat"', '.\\install-service-winlogbeat.ps1'], + textPost: i18n.translate( + 'home.tutorials.common.winlogbeatInstructions.install.windowsTextPost', + { + defaultMessage: + 'Modify the settings under `output.elasticsearch` in the {path} file to point to your Elasticsearch installation.', + values: { path: '`C:\\Program Files\\Winlogbeat\\winlogbeat.yml`' }, + } + ), + }, }, - }, - START: { - WINDOWS: { - title: i18n.translate('home.tutorials.common.winlogbeatInstructions.start.windowsTitle', { - defaultMessage: 'Start Winlogbeat', - }), - textPre: i18n.translate('home.tutorials.common.winlogbeatInstructions.start.windowsTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['.\\winlogbeat.exe setup', 'Start-Service winlogbeat'], + START: { + WINDOWS: { + title: i18n.translate('home.tutorials.common.winlogbeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Winlogbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.winlogbeatInstructions.start.windowsTextPre', + { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + } + ), + commands: ['.\\winlogbeat.exe setup', 'Start-Service winlogbeat'], + }, }, - }, - CONFIG: { - WINDOWS: { - title: i18n.translate('home.tutorials.common.winlogbeatInstructions.config.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate( - 'home.tutorials.common.winlogbeatInstructions.config.windowsTextPre', - { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`C:\\Program Files\\Winlogbeat\\winlogbeat.yml`', - }, - } - ), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.winlogbeatInstructions.config.windowsTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - } - ), + CONFIG: { + WINDOWS: { + title: i18n.translate('home.tutorials.common.winlogbeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate( + 'home.tutorials.common.winlogbeatInstructions.config.windowsTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Winlogbeat\\winlogbeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.winlogbeatInstructions.config.windowsTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, }, - }, -}); + }; +}; export const createWinlogbeatCloudInstructions = () => ({ CONFIG: { @@ -158,7 +170,7 @@ export function winlogbeatStatusCheck() { }; } -export function onPremInstructions(context?: TutorialContext) { +export function onPremInstructions(context: TutorialContext) { const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(context); return { @@ -186,10 +198,10 @@ export function onPremInstructions(context?: TutorialContext) { }; } -export function onPremCloudInstructions() { +export function onPremCloudInstructions(context: TutorialContext) { const TRYCLOUD_OPTION1 = createTrycloudOption1(); const TRYCLOUD_OPTION2 = createTrycloudOption2(); - const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(); + const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(context); return { instructionSets: [ @@ -218,8 +230,8 @@ export function onPremCloudInstructions() { }; } -export function cloudInstructions() { - const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(); +export function cloudInstructions(context: TutorialContext) { + const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(context); const WINLOGBEAT_CLOUD_INSTRUCTIONS = createWinlogbeatCloudInstructions(); return { diff --git a/src/plugins/home/server/tutorials/iptables_logs/index.ts b/src/plugins/home/server/tutorials/iptables_logs/index.ts index 6d298e88a2dfb..f4469de3336cc 100644 --- a/src/plugins/home/server/tutorials/iptables_logs/index.ts +++ b/src/plugins/home/server/tutorials/iptables_logs/index.ts @@ -60,8 +60,8 @@ export function iptablesLogsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/iptables_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/juniper_logs/index.ts b/src/plugins/home/server/tutorials/juniper_logs/index.ts index 7430e4705a5f4..a6d34d1e8447f 100644 --- a/src/plugins/home/server/tutorials/juniper_logs/index.ts +++ b/src/plugins/home/server/tutorials/juniper_logs/index.ts @@ -54,8 +54,8 @@ export function juniperLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/kafka_logs/index.ts b/src/plugins/home/server/tutorials/kafka_logs/index.ts index 9ccc06eb222c7..6e377f3c1f295 100644 --- a/src/plugins/home/server/tutorials/kafka_logs/index.ts +++ b/src/plugins/home/server/tutorials/kafka_logs/index.ts @@ -57,8 +57,8 @@ export function kafkaLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/kafka_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/kafka_metrics/index.ts b/src/plugins/home/server/tutorials/kafka_metrics/index.ts index 973ec06b58fdf..5e6250989d0ab 100644 --- a/src/plugins/home/server/tutorials/kafka_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kafka_metrics/index.ts @@ -54,8 +54,8 @@ export function kafkaMetricsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/kibana_logs/index.ts b/src/plugins/home/server/tutorials/kibana_logs/index.ts index 9863a53700a55..969e4972875f4 100644 --- a/src/plugins/home/server/tutorials/kibana_logs/index.ts +++ b/src/plugins/home/server/tutorials/kibana_logs/index.ts @@ -53,8 +53,8 @@ export function kibanaLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/kibana_metrics/index.ts b/src/plugins/home/server/tutorials/kibana_metrics/index.ts index 3d0eb691ede51..ff8ec0eb6e43c 100644 --- a/src/plugins/home/server/tutorials/kibana_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kibana_metrics/index.ts @@ -54,8 +54,8 @@ export function kibanaMetricsSpecProvider(context: TutorialContext): TutorialSch }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts index 9c66125ee0cfe..acd65e0bdc69d 100644 --- a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts @@ -59,8 +59,8 @@ export function kubernetesMetricsSpecProvider(context: TutorialContext): Tutoria completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/kubernetes_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['containers', 'kubernetes'], }; } diff --git a/src/plugins/home/server/tutorials/logstash_logs/index.ts b/src/plugins/home/server/tutorials/logstash_logs/index.ts index 688ad8245b78d..5978241d7e669 100644 --- a/src/plugins/home/server/tutorials/logstash_logs/index.ts +++ b/src/plugins/home/server/tutorials/logstash_logs/index.ts @@ -56,8 +56,8 @@ export function logstashLogsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['custom'], }; } diff --git a/src/plugins/home/server/tutorials/logstash_metrics/index.ts b/src/plugins/home/server/tutorials/logstash_metrics/index.ts index 9ae4bcdcecbf1..d8d7db1b464b1 100644 --- a/src/plugins/home/server/tutorials/logstash_metrics/index.ts +++ b/src/plugins/home/server/tutorials/logstash_metrics/index.ts @@ -55,8 +55,8 @@ export function logstashMetricsSpecProvider(context: TutorialContext): TutorialS }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['custom'], }; } diff --git a/src/plugins/home/server/tutorials/memcached_metrics/index.ts b/src/plugins/home/server/tutorials/memcached_metrics/index.ts index 891567f72ca7c..a48db78e89d88 100644 --- a/src/plugins/home/server/tutorials/memcached_metrics/index.ts +++ b/src/plugins/home/server/tutorials/memcached_metrics/index.ts @@ -54,8 +54,8 @@ export function memcachedMetricsSpecProvider(context: TutorialContext): Tutorial }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['custom'], }; } diff --git a/src/plugins/home/server/tutorials/microsoft_logs/index.ts b/src/plugins/home/server/tutorials/microsoft_logs/index.ts index 88893e22bc9ff..39400f4661071 100644 --- a/src/plugins/home/server/tutorials/microsoft_logs/index.ts +++ b/src/plugins/home/server/tutorials/microsoft_logs/index.ts @@ -57,8 +57,8 @@ export function microsoftLogsSpecProvider(context: TutorialContext): TutorialSch completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/microsoft_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security', 'azure'], }; } diff --git a/src/plugins/home/server/tutorials/misp_logs/index.ts b/src/plugins/home/server/tutorials/misp_logs/index.ts index ea2147a296534..4fb70aa1018f7 100644 --- a/src/plugins/home/server/tutorials/misp_logs/index.ts +++ b/src/plugins/home/server/tutorials/misp_logs/index.ts @@ -57,8 +57,8 @@ export function mispLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/misp_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security', 'azure'], }; } diff --git a/src/plugins/home/server/tutorials/mongodb_logs/index.ts b/src/plugins/home/server/tutorials/mongodb_logs/index.ts index a7f9869d440ed..28e323a2b15a9 100644 --- a/src/plugins/home/server/tutorials/mongodb_logs/index.ts +++ b/src/plugins/home/server/tutorials/mongodb_logs/index.ts @@ -57,8 +57,8 @@ export function mongodbLogsSpecProvider(context: TutorialContext): TutorialSchem completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/mongodb_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts index cc0ecc0574fa9..db843d09abfd8 100644 --- a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts @@ -59,8 +59,8 @@ export function mongodbMetricsSpecProvider(context: TutorialContext): TutorialSc completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/mongodb_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/mssql_logs/index.ts b/src/plugins/home/server/tutorials/mssql_logs/index.ts index 06cafd95283c8..5e19a2204b22c 100644 --- a/src/plugins/home/server/tutorials/mssql_logs/index.ts +++ b/src/plugins/home/server/tutorials/mssql_logs/index.ts @@ -54,8 +54,8 @@ export function mssqlLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/mssql_metrics/index.ts b/src/plugins/home/server/tutorials/mssql_metrics/index.ts index e3c9e3c338209..3e73714784f0f 100644 --- a/src/plugins/home/server/tutorials/mssql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mssql_metrics/index.ts @@ -57,8 +57,8 @@ export function mssqlMetricsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/mssql_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/munin_metrics/index.ts b/src/plugins/home/server/tutorials/munin_metrics/index.ts index 12621d05d0766..963e9b63e9ba8 100644 --- a/src/plugins/home/server/tutorials/munin_metrics/index.ts +++ b/src/plugins/home/server/tutorials/munin_metrics/index.ts @@ -54,8 +54,8 @@ export function muninMetricsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/mysql_logs/index.ts b/src/plugins/home/server/tutorials/mysql_logs/index.ts index b0c6f0e69dcfb..9af0a3d078cab 100644 --- a/src/plugins/home/server/tutorials/mysql_logs/index.ts +++ b/src/plugins/home/server/tutorials/mysql_logs/index.ts @@ -57,8 +57,8 @@ export function mysqlLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/mysql_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/mysql_metrics/index.ts b/src/plugins/home/server/tutorials/mysql_metrics/index.ts index 09c55dc81ff84..8339561d060d6 100644 --- a/src/plugins/home/server/tutorials/mysql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mysql_metrics/index.ts @@ -56,8 +56,8 @@ export function mysqlMetricsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/mysql_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/nats_logs/index.ts b/src/plugins/home/server/tutorials/nats_logs/index.ts index b6ef0a192d92f..971f0c2849bda 100644 --- a/src/plugins/home/server/tutorials/nats_logs/index.ts +++ b/src/plugins/home/server/tutorials/nats_logs/index.ts @@ -58,8 +58,8 @@ export function natsLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/nats_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/nats_metrics/index.ts b/src/plugins/home/server/tutorials/nats_metrics/index.ts index 54f034ad44b19..cdd633d88140c 100644 --- a/src/plugins/home/server/tutorials/nats_metrics/index.ts +++ b/src/plugins/home/server/tutorials/nats_metrics/index.ts @@ -56,8 +56,8 @@ export function natsMetricsSpecProvider(context: TutorialContext): TutorialSchem completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/nats_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/netflow_logs/index.ts b/src/plugins/home/server/tutorials/netflow_logs/index.ts index c659d9c1d31b1..7a81159503468 100644 --- a/src/plugins/home/server/tutorials/netflow_logs/index.ts +++ b/src/plugins/home/server/tutorials/netflow_logs/index.ts @@ -56,8 +56,8 @@ export function netflowLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/netscout_logs/index.ts b/src/plugins/home/server/tutorials/netscout_logs/index.ts index e6c22947f8057..2b1a469a9bbb7 100644 --- a/src/plugins/home/server/tutorials/netscout_logs/index.ts +++ b/src/plugins/home/server/tutorials/netscout_logs/index.ts @@ -54,8 +54,8 @@ export function netscoutLogsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/nginx_logs/index.ts b/src/plugins/home/server/tutorials/nginx_logs/index.ts index e6f2fc4efb01c..3797f2496ee17 100644 --- a/src/plugins/home/server/tutorials/nginx_logs/index.ts +++ b/src/plugins/home/server/tutorials/nginx_logs/index.ts @@ -57,8 +57,8 @@ export function nginxLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/nginx_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/nginx_metrics/index.ts b/src/plugins/home/server/tutorials/nginx_metrics/index.ts index 680dd664912d3..f32e9388c1f5b 100644 --- a/src/plugins/home/server/tutorials/nginx_metrics/index.ts +++ b/src/plugins/home/server/tutorials/nginx_metrics/index.ts @@ -61,8 +61,8 @@ which must be enabled in your Nginx installation. \ completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/nginx_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/o365_logs/index.ts b/src/plugins/home/server/tutorials/o365_logs/index.ts index 3cd4d3a5c5e18..cbdabc7223b32 100644 --- a/src/plugins/home/server/tutorials/o365_logs/index.ts +++ b/src/plugins/home/server/tutorials/o365_logs/index.ts @@ -60,8 +60,8 @@ export function o365LogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/o365_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/okta_logs/index.ts b/src/plugins/home/server/tutorials/okta_logs/index.ts index aad18409de329..f45ffbfb800b5 100644 --- a/src/plugins/home/server/tutorials/okta_logs/index.ts +++ b/src/plugins/home/server/tutorials/okta_logs/index.ts @@ -58,8 +58,8 @@ export function oktaLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/okta_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts index 02625b341549b..d2611fb77895e 100644 --- a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts +++ b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts @@ -48,8 +48,8 @@ export function openmetricsMetricsSpecProvider(context: TutorialContext): Tutori }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/oracle_metrics/index.ts b/src/plugins/home/server/tutorials/oracle_metrics/index.ts index 14cf5392c5231..263f2f5ab184b 100644 --- a/src/plugins/home/server/tutorials/oracle_metrics/index.ts +++ b/src/plugins/home/server/tutorials/oracle_metrics/index.ts @@ -55,8 +55,8 @@ export function oracleMetricsSpecProvider(context: TutorialContext): TutorialSch }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/osquery_logs/index.ts b/src/plugins/home/server/tutorials/osquery_logs/index.ts index 4f87fc4e256e1..1c77222ce43a0 100644 --- a/src/plugins/home/server/tutorials/osquery_logs/index.ts +++ b/src/plugins/home/server/tutorials/osquery_logs/index.ts @@ -60,8 +60,8 @@ export function osqueryLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security', 'os_system'], }; } diff --git a/src/plugins/home/server/tutorials/panw_logs/index.ts b/src/plugins/home/server/tutorials/panw_logs/index.ts index f5158c48f30d5..4b44038c07ade 100644 --- a/src/plugins/home/server/tutorials/panw_logs/index.ts +++ b/src/plugins/home/server/tutorials/panw_logs/index.ts @@ -60,8 +60,8 @@ export function panwLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/panw_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts index 40b35984fb17a..0a033e6378729 100644 --- a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts +++ b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts @@ -54,8 +54,8 @@ export function phpfpmMetricsSpecProvider(context: TutorialContext): TutorialSch }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/postgresql_logs/index.ts b/src/plugins/home/server/tutorials/postgresql_logs/index.ts index 3a092e61b0bd9..a628f422dfb72 100644 --- a/src/plugins/home/server/tutorials/postgresql_logs/index.ts +++ b/src/plugins/home/server/tutorials/postgresql_logs/index.ts @@ -60,8 +60,8 @@ export function postgresqlLogsSpecProvider(context: TutorialContext): TutorialSc completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/postgresql_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts index 501ea252cd16f..0ef48c33a7475 100644 --- a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts @@ -56,8 +56,8 @@ export function postgresqlMetricsSpecProvider(context: TutorialContext): Tutoria }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/prometheus_metrics/index.ts b/src/plugins/home/server/tutorials/prometheus_metrics/index.ts index 2f422e5e3be70..92a08bcce0ca4 100644 --- a/src/plugins/home/server/tutorials/prometheus_metrics/index.ts +++ b/src/plugins/home/server/tutorials/prometheus_metrics/index.ts @@ -55,8 +55,8 @@ export function prometheusMetricsSpecProvider(context: TutorialContext): Tutoria }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['monitoring', 'datastore'], }; } diff --git a/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts b/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts index 8a1634e7da038..be6576de45a98 100644 --- a/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts +++ b/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts @@ -54,8 +54,8 @@ export function rabbitmqLogsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts index abfc895088d91..4487a187fa373 100644 --- a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts @@ -60,8 +60,8 @@ export function rabbitmqMetricsSpecProvider(context: TutorialContext): TutorialS completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/rabbitmq_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/radware_logs/index.ts b/src/plugins/home/server/tutorials/radware_logs/index.ts index 3e918a0a4064c..4abd897c0aff3 100644 --- a/src/plugins/home/server/tutorials/radware_logs/index.ts +++ b/src/plugins/home/server/tutorials/radware_logs/index.ts @@ -54,8 +54,8 @@ export function radwareLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/redis_logs/index.ts b/src/plugins/home/server/tutorials/redis_logs/index.ts index f6aada27dec48..bb5d902d089e2 100644 --- a/src/plugins/home/server/tutorials/redis_logs/index.ts +++ b/src/plugins/home/server/tutorials/redis_logs/index.ts @@ -63,8 +63,8 @@ Note that the `slowlog` fileset is experimental. \ completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/redis_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['datastore', 'message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/redis_metrics/index.ts b/src/plugins/home/server/tutorials/redis_metrics/index.ts index 2bb300c48ff65..d2e8ed1efb779 100644 --- a/src/plugins/home/server/tutorials/redis_metrics/index.ts +++ b/src/plugins/home/server/tutorials/redis_metrics/index.ts @@ -56,8 +56,8 @@ export function redisMetricsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/redis_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore', 'message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts index 62e1386f29dbb..85d6dce9adc52 100644 --- a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts +++ b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts @@ -55,8 +55,8 @@ export function redisenterpriseMetricsSpecProvider(context: TutorialContext): Tu completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/redisenterprise_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore', 'message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/santa_logs/index.ts b/src/plugins/home/server/tutorials/santa_logs/index.ts index da9f2e940066e..65a7bb0bd26cb 100644 --- a/src/plugins/home/server/tutorials/santa_logs/index.ts +++ b/src/plugins/home/server/tutorials/santa_logs/index.ts @@ -58,8 +58,8 @@ export function santaLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/santa_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security', 'os_system'], }; } diff --git a/src/plugins/home/server/tutorials/sonicwall_logs/index.ts b/src/plugins/home/server/tutorials/sonicwall_logs/index.ts index 04bf7a3968320..40eb324014b15 100644 --- a/src/plugins/home/server/tutorials/sonicwall_logs/index.ts +++ b/src/plugins/home/server/tutorials/sonicwall_logs/index.ts @@ -54,8 +54,8 @@ export function sonicwallLogsSpecProvider(context: TutorialContext): TutorialSch }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/sophos_logs/index.ts b/src/plugins/home/server/tutorials/sophos_logs/index.ts index 4fadcecb6e1bd..c6d6f7318b6ed 100644 --- a/src/plugins/home/server/tutorials/sophos_logs/index.ts +++ b/src/plugins/home/server/tutorials/sophos_logs/index.ts @@ -54,8 +54,8 @@ export function sophosLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/squid_logs/index.ts b/src/plugins/home/server/tutorials/squid_logs/index.ts index 2d8f055d7fa6b..f325dbbd650ca 100644 --- a/src/plugins/home/server/tutorials/squid_logs/index.ts +++ b/src/plugins/home/server/tutorials/squid_logs/index.ts @@ -54,8 +54,8 @@ export function squidLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/stan_metrics/index.ts b/src/plugins/home/server/tutorials/stan_metrics/index.ts index 0b3c0352b663d..50f2b9dbd2e87 100644 --- a/src/plugins/home/server/tutorials/stan_metrics/index.ts +++ b/src/plugins/home/server/tutorials/stan_metrics/index.ts @@ -56,8 +56,8 @@ export function stanMetricsSpecProvider(context: TutorialContext): TutorialSchem completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/stan_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['message_queue', 'kubernetes'], }; } diff --git a/src/plugins/home/server/tutorials/statsd_metrics/index.ts b/src/plugins/home/server/tutorials/statsd_metrics/index.ts index 1be010a01d5a6..c6ea0cf7ee879 100644 --- a/src/plugins/home/server/tutorials/statsd_metrics/index.ts +++ b/src/plugins/home/server/tutorials/statsd_metrics/index.ts @@ -45,8 +45,8 @@ export function statsdMetricsSpecProvider(context: TutorialContext): TutorialSch completionTimeMinutes: 10, // previewImagePath: '', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['message_queue', 'kubernetes'], }; } diff --git a/src/plugins/home/server/tutorials/suricata_logs/index.ts b/src/plugins/home/server/tutorials/suricata_logs/index.ts index 373522e333379..a511be4a7a968 100644 --- a/src/plugins/home/server/tutorials/suricata_logs/index.ts +++ b/src/plugins/home/server/tutorials/suricata_logs/index.ts @@ -58,8 +58,8 @@ export function suricataLogsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/suricata_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/system_logs/index.ts b/src/plugins/home/server/tutorials/system_logs/index.ts index fcc5745f48252..1de6d9df10ffb 100644 --- a/src/plugins/home/server/tutorials/system_logs/index.ts +++ b/src/plugins/home/server/tutorials/system_logs/index.ts @@ -56,8 +56,8 @@ export function systemLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['os_system', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/system_metrics/index.ts b/src/plugins/home/server/tutorials/system_metrics/index.ts index 1348535d9bb72..10a6c741721b8 100644 --- a/src/plugins/home/server/tutorials/system_metrics/index.ts +++ b/src/plugins/home/server/tutorials/system_metrics/index.ts @@ -58,8 +58,8 @@ It collects system wide statistics and statistics per process and filesystem. \ completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/system_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['os_system', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/tomcat_logs/index.ts b/src/plugins/home/server/tutorials/tomcat_logs/index.ts index 3258d3eff5a16..2f24354742771 100644 --- a/src/plugins/home/server/tutorials/tomcat_logs/index.ts +++ b/src/plugins/home/server/tutorials/tomcat_logs/index.ts @@ -54,8 +54,8 @@ export function tomcatLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/traefik_logs/index.ts b/src/plugins/home/server/tutorials/traefik_logs/index.ts index 30b9db4022137..7411e396a5655 100644 --- a/src/plugins/home/server/tutorials/traefik_logs/index.ts +++ b/src/plugins/home/server/tutorials/traefik_logs/index.ts @@ -56,8 +56,8 @@ export function traefikLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/traefik_metrics/index.ts b/src/plugins/home/server/tutorials/traefik_metrics/index.ts index 6f76be3056110..6e1d8d621e62e 100644 --- a/src/plugins/home/server/tutorials/traefik_metrics/index.ts +++ b/src/plugins/home/server/tutorials/traefik_metrics/index.ts @@ -44,8 +44,8 @@ export function traefikMetricsSpecProvider(context: TutorialContext): TutorialSc }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/uptime_monitors/index.ts b/src/plugins/home/server/tutorials/uptime_monitors/index.ts index 118174d0e5717..9015cb4783163 100644 --- a/src/plugins/home/server/tutorials/uptime_monitors/index.ts +++ b/src/plugins/home/server/tutorials/uptime_monitors/index.ts @@ -55,8 +55,8 @@ export function uptimeMonitorsSpecProvider(context: TutorialContext): TutorialSc completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/uptime_monitors/screenshot.png', onPrem: onPremInstructions([], context), - elasticCloud: cloudInstructions(), - onPremElasticCloud: onPremCloudInstructions(), + elasticCloud: cloudInstructions(context), + onPremElasticCloud: onPremCloudInstructions(context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts index b1dbeb89bdb26..bb288ba72ab02 100644 --- a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts +++ b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts @@ -57,8 +57,8 @@ export function uwsgiMetricsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/uwsgi_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts index 14a574872221a..0070be6622294 100644 --- a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts +++ b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts @@ -54,8 +54,8 @@ export function vSphereMetricsSpecProvider(context: TutorialContext): TutorialSc }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/windows_event_logs/index.ts b/src/plugins/home/server/tutorials/windows_event_logs/index.ts index 008468487ea64..baab0f4c95080 100644 --- a/src/plugins/home/server/tutorials/windows_event_logs/index.ts +++ b/src/plugins/home/server/tutorials/windows_event_logs/index.ts @@ -54,8 +54,8 @@ export function windowsEventLogsSpecProvider(context: TutorialContext): Tutorial }, completionTimeMinutes: 10, onPrem: onPremInstructions(context), - elasticCloud: cloudInstructions(), - onPremElasticCloud: onPremCloudInstructions(), + elasticCloud: cloudInstructions(context), + onPremElasticCloud: onPremCloudInstructions(context), integrationBrowserCategories: ['os_system', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/windows_metrics/index.ts b/src/plugins/home/server/tutorials/windows_metrics/index.ts index 31d9b3f8962ce..ebd5e6864a229 100644 --- a/src/plugins/home/server/tutorials/windows_metrics/index.ts +++ b/src/plugins/home/server/tutorials/windows_metrics/index.ts @@ -54,8 +54,8 @@ export function windowsMetricsSpecProvider(context: TutorialContext): TutorialSc }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['os_system', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/zeek_logs/index.ts b/src/plugins/home/server/tutorials/zeek_logs/index.ts index df86518978c52..3eded8336df74 100644 --- a/src/plugins/home/server/tutorials/zeek_logs/index.ts +++ b/src/plugins/home/server/tutorials/zeek_logs/index.ts @@ -58,8 +58,8 @@ export function zeekLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/zeek_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'monitoring', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts index 8f732969a07f3..4e4206bc1ca29 100644 --- a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts +++ b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts @@ -55,8 +55,8 @@ export function zookeeperMetricsSpecProvider(context: TutorialContext): Tutorial }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore', 'config_management'], }; } diff --git a/src/plugins/home/server/tutorials/zscaler_logs/index.ts b/src/plugins/home/server/tutorials/zscaler_logs/index.ts index 977bbb242c62a..316590c74fd76 100644 --- a/src/plugins/home/server/tutorials/zscaler_logs/index.ts +++ b/src/plugins/home/server/tutorials/zscaler_logs/index.ts @@ -54,8 +54,8 @@ export function zscalerLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.test.ts b/src/plugins/interactive_setup/server/kibana_config_writer.test.ts index 82d02882698d4..d097d7cc4a05d 100644 --- a/src/plugins/interactive_setup/server/kibana_config_writer.test.ts +++ b/src/plugins/interactive_setup/server/kibana_config_writer.test.ts @@ -35,7 +35,8 @@ describe('KibanaConfigWriter', () => { throw new Error('Invalid certificate'); } return { - fingerprint256: 'fingerprint256', + fingerprint256: + 'D4:86:CE:00:AC:71:E4:1D:2B:70:D0:87:A5:55:FA:5D:D1:93:6C:DB:45:80:79:53:7B:A3:AC:13:3E:48:34:D6', }; }; @@ -131,7 +132,7 @@ describe('KibanaConfigWriter', () => { elasticsearch.hosts: [some-host] elasticsearch.serviceAccountToken: some-value elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt] - xpack.fleet.outputs: [{id: fleet-default-output, name: default, is_default: true, is_default_monitoring: true, type: elasticsearch, hosts: [some-host], ca_sha256: fingerprint256}] + xpack.fleet.outputs: [{id: fleet-default-output, name: default, is_default: true, is_default_monitoring: true, type: elasticsearch, hosts: [some-host], ca_trusted_fingerprint: d486ce00ac71e41d2b70d087a555fa5dd1936cdb458079537ba3ac133e4834d6}] ", ], @@ -198,7 +199,7 @@ describe('KibanaConfigWriter', () => { elasticsearch.username: username elasticsearch.password: password elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt] - xpack.fleet.outputs: [{id: fleet-default-output, name: default, is_default: true, is_default_monitoring: true, type: elasticsearch, hosts: [some-host], ca_sha256: fingerprint256}] + xpack.fleet.outputs: [{id: fleet-default-output, name: default, is_default: true, is_default_monitoring: true, type: elasticsearch, hosts: [some-host], ca_trusted_fingerprint: d486ce00ac71e41d2b70d087a555fa5dd1936cdb458079537ba3ac133e4834d6}] ", ], @@ -275,7 +276,7 @@ describe('KibanaConfigWriter', () => { elasticsearch.hosts: [some-host] elasticsearch.serviceAccountToken: some-value elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt] - xpack.fleet.outputs: [{id: fleet-default-output, name: default, is_default: true, is_default_monitoring: true, type: elasticsearch, hosts: [some-host], ca_sha256: fingerprint256}] + xpack.fleet.outputs: [{id: fleet-default-output, name: default, is_default: true, is_default_monitoring: true, type: elasticsearch, hosts: [some-host], ca_trusted_fingerprint: d486ce00ac71e41d2b70d087a555fa5dd1936cdb458079537ba3ac133e4834d6}] ", ], @@ -329,7 +330,7 @@ describe('KibanaConfigWriter', () => { monitoring.ui.container.elasticsearch.enabled: true elasticsearch.serviceAccountToken: some-value elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt] - xpack.fleet.outputs: [{id: fleet-default-output, name: default, is_default: true, is_default_monitoring: true, type: elasticsearch, hosts: [some-host], ca_sha256: fingerprint256}] + xpack.fleet.outputs: [{id: fleet-default-output, name: default, is_default: true, is_default_monitoring: true, type: elasticsearch, hosts: [some-host], ca_trusted_fingerprint: d486ce00ac71e41d2b70d087a555fa5dd1936cdb458079537ba3ac133e4834d6}] ", ], diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.ts b/src/plugins/interactive_setup/server/kibana_config_writer.ts index af177fee33bce..eac1bd0cef175 100644 --- a/src/plugins/interactive_setup/server/kibana_config_writer.ts +++ b/src/plugins/interactive_setup/server/kibana_config_writer.ts @@ -38,7 +38,7 @@ interface FleetOutputConfig { is_default_monitoring: boolean; type: 'elasticsearch'; hosts: string[]; - ca_sha256: string; + ca_trusted_fingerprint: string; } export class KibanaConfigWriter { @@ -187,7 +187,8 @@ export class KibanaConfigWriter { */ private static getFleetDefaultOutputConfig(caCert: string, host: string): FleetOutputConfig[] { const cert = new X509Certificate(caCert); - const certFingerprint = cert.fingerprint256; + // fingerprint256 is a ":" separated uppercase hexadecimal string + const certFingerprint = cert.fingerprint256.split(':').join('').toLowerCase(); return [ { @@ -197,7 +198,7 @@ export class KibanaConfigWriter { is_default_monitoring: true, type: 'elasticsearch', hosts: [host], - ca_sha256: certFingerprint, + ca_trusted_fingerprint: certFingerprint, }, ]; } diff --git a/src/plugins/management/public/components/management_app/management_app.tsx b/src/plugins/management/public/components/management_app/management_app.tsx index 8a520a2629c3b..4d306ffd2e266 100644 --- a/src/plugins/management/public/components/management_app/management_app.tsx +++ b/src/plugins/management/public/components/management_app/management_app.tsx @@ -8,17 +8,18 @@ import './management_app.scss'; import React, { useState, useEffect, useCallback } from 'react'; -import { AppMountParameters, ChromeBreadcrumb, ScopedHistory } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { ManagementSection, MANAGEMENT_BREADCRUMB } from '../../utils'; +import { AppMountParameters, ChromeBreadcrumb, ScopedHistory } from 'kibana/public'; +import { ManagementSection, MANAGEMENT_BREADCRUMB } from '../../utils'; import { ManagementRouter } from './management_router'; import { managementSidebarNav } from '../management_sidebar_nav/management_sidebar_nav'; import { KibanaPageTemplate, KibanaPageTemplateProps, reactRouterNavigate, + KibanaThemeProvider, } from '../../../../kibana_react/public'; import { SectionsServiceStart } from '../../types'; @@ -83,24 +84,26 @@ export const ManagementApp = ({ dependencies, history, theme$ }: ManagementAppPr return ( - - - + + + + + ); }; 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/packages/kbn-es/src/index.js b/src/plugins/presentation_util/public/components/controls/control_types/index.ts similarity index 80% rename from packages/kbn-es/src/index.js rename to src/plugins/presentation_util/public/components/controls/control_types/index.ts index 3b12de68234fa..141e9f9b4d55f 100644 --- a/packages/kbn-es/src/index.js +++ b/src/plugins/presentation_util/public/components/controls/control_types/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -exports.run = require('./cli').run; -exports.Cluster = require('./cluster').Cluster; +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/heatmap/public/vis_type/heatmap.tsx b/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx index 5d466c2f4b3c8..e20c7eb0f8c21 100644 --- a/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx +++ b/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx @@ -98,7 +98,14 @@ export const getHeatmapVisTypeDefinition = ({ }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -106,7 +113,14 @@ export const getHeatmapVisTypeDefinition = ({ title: i18n.translate('visTypeHeatmap.heatmap.groupTitle', { defaultMessage: 'Y-axis' }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -121,7 +135,14 @@ export const getHeatmapVisTypeDefinition = ({ }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/metric/public/metric_vis_type.ts b/src/plugins/vis_types/metric/public/metric_vis_type.ts index d4db2ac9e4671..ffb34248aeccc 100644 --- a/src/plugins/vis_types/metric/public/metric_vis_type.ts +++ b/src/plugins/vis_types/metric/public/metric_vis_type.ts @@ -86,7 +86,14 @@ export const createMetricVisTypeDefinition = (): VisTypeDefinition => }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts b/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts index 15a3675125f61..545456b6dcce0 100644 --- a/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts @@ -87,7 +87,14 @@ export const samplePieVis = { title: 'Split slices', min: 0, max: null, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], editor: false, params: [], }, @@ -98,7 +105,14 @@ export const samplePieVis = { mustBeFirst: true, min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [ { name: 'row', diff --git a/src/plugins/vis_types/pie/public/vis_type/pie.ts b/src/plugins/vis_types/pie/public/vis_type/pie.ts index 0d012ed95b5d9..f10af053bd161 100644 --- a/src/plugins/vis_types/pie/public/vis_type/pie.ts +++ b/src/plugins/vis_types/pie/public/vis_type/pie.ts @@ -80,7 +80,14 @@ export const getPieVisTypeDefinition = ({ }), min: 0, max: Infinity, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -91,7 +98,14 @@ export const getPieVisTypeDefinition = ({ mustBeFirst: true, min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/table/public/table_vis_type.ts b/src/plugins/vis_types/table/public/table_vis_type.ts index a641224e23f52..2f1642e29107a 100644 --- a/src/plugins/vis_types/table/public/table_vis_type.ts +++ b/src/plugins/vis_types/table/public/table_vis_type.ts @@ -62,7 +62,7 @@ export const tableVisTypeDefinition: VisTypeDefinition = { title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { defaultMessage: 'Split rows', }), - aggFilter: ['!filter'], + aggFilter: ['!filter', '!sampler', '!diversified_sampler', '!multi_terms'], }, { group: AggGroupNames.Buckets, @@ -72,7 +72,7 @@ export const tableVisTypeDefinition: VisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!filter'], + aggFilter: ['!filter', '!sampler', '!diversified_sampler', '!multi_terms'], }, ], }, diff --git a/src/plugins/vis_types/vislib/public/gauge.ts b/src/plugins/vis_types/vislib/public/gauge.ts index 51cd7ea7622df..31a44a5d1d73f 100644 --- a/src/plugins/vis_types/vislib/public/gauge.ts +++ b/src/plugins/vis_types/vislib/public/gauge.ts @@ -132,7 +132,14 @@ export const gaugeVisTypeDefinition: VisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/vislib/public/goal.ts b/src/plugins/vis_types/vislib/public/goal.ts index 05ad1f53904d7..26bc598790839 100644 --- a/src/plugins/vis_types/vislib/public/goal.ts +++ b/src/plugins/vis_types/vislib/public/goal.ts @@ -96,7 +96,14 @@ export const goalVisTypeDefinition: VisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, 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/editor/components/options/point_series/point_series.mocks.ts b/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts index 41ab13d54f7c6..401afc5a7473a 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts +++ b/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts @@ -625,7 +625,14 @@ export const getVis = (bucketType: string) => { title: 'X-axis', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [], }, { @@ -634,7 +641,14 @@ export const getVis = (bucketType: string) => { title: 'Split series', min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [], }, { @@ -643,7 +657,14 @@ export const getVis = (bucketType: string) => { title: 'Split chart', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [ { name: 'row', @@ -688,7 +709,14 @@ export const getVis = (bucketType: string) => { title: 'X-axis', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [], }, { @@ -697,7 +725,14 @@ export const getVis = (bucketType: string) => { title: 'Split series', min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [], }, { @@ -706,7 +741,14 @@ export const getVis = (bucketType: string) => { title: 'Split chart', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [ { name: 'row', @@ -722,7 +764,14 @@ export const getVis = (bucketType: string) => { title: 'X-axis', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [], }, { @@ -731,7 +780,14 @@ export const getVis = (bucketType: string) => { title: 'Split series', min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [], }, { @@ -740,7 +796,14 @@ export const getVis = (bucketType: string) => { title: 'Split chart', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [ { name: 'row', diff --git a/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts b/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts index 6077732a9cc6b..766929a2cd654 100644 --- a/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts @@ -149,7 +149,14 @@ export const sampleAreaVis = { title: 'X-axis', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], editor: false, params: [], }, @@ -159,7 +166,14 @@ export const sampleAreaVis = { title: 'Split series', min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], editor: false, params: [], }, @@ -169,7 +183,14 @@ export const sampleAreaVis = { title: 'Split chart', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [ { name: 'row', diff --git a/src/plugins/vis_types/xy/public/utils/get_series_params.test.ts b/src/plugins/vis_types/xy/public/utils/get_series_params.test.ts index 67b8a1c160d40..5c22527d5b9d7 100644 --- a/src/plugins/vis_types/xy/public/utils/get_series_params.test.ts +++ b/src/plugins/vis_types/xy/public/utils/get_series_params.test.ts @@ -45,7 +45,7 @@ describe('getSeriesParams', () => { ); expect(seriesParams).toStrictEqual([ { - circlesRadius: 3, + circlesRadius: 1, data: { id: '1', label: 'Total quantity', diff --git a/src/plugins/vis_types/xy/public/utils/get_series_params.ts b/src/plugins/vis_types/xy/public/utils/get_series_params.ts index 987c8df83b01f..0acd2a0913282 100644 --- a/src/plugins/vis_types/xy/public/utils/get_series_params.ts +++ b/src/plugins/vis_types/xy/public/utils/get_series_params.ts @@ -22,7 +22,7 @@ const makeSerie = ( type: ChartType.Line, drawLinesBetweenPoints: true, showCircles: true, - circlesRadius: 3, + circlesRadius: 1, interpolate: InterpolationMode.Linear, lineWidth: 2, valueAxis: defaultValueAxis, 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/src/plugins/vis_types/xy/public/vis_types/area.ts b/src/plugins/vis_types/xy/public/vis_types/area.ts index 3b8f78db25d36..efeb4142ff0d7 100644 --- a/src/plugins/vis_types/xy/public/vis_types/area.ts +++ b/src/plugins/vis_types/xy/public/vis_types/area.ts @@ -97,7 +97,7 @@ export const areaVisTypeDefinition = { drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true, - circlesRadius: 3, + circlesRadius: 1, interpolate: InterpolationMode.Linear, valueAxis: 'ValueAxis-1', }, @@ -157,7 +157,14 @@ export const areaVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -167,7 +174,14 @@ export const areaVisTypeDefinition = { }), min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -177,7 +191,14 @@ export const areaVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/xy/public/vis_types/histogram.ts b/src/plugins/vis_types/xy/public/vis_types/histogram.ts index 79b3fd72de452..1cd346abec6e7 100644 --- a/src/plugins/vis_types/xy/public/vis_types/histogram.ts +++ b/src/plugins/vis_types/xy/public/vis_types/histogram.ts @@ -101,7 +101,7 @@ export const histogramVisTypeDefinition = { drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true, - circlesRadius: 3, + circlesRadius: 1, }, ], radiusRatio: 0, @@ -160,7 +160,14 @@ export const histogramVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -170,7 +177,14 @@ export const histogramVisTypeDefinition = { }), min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -180,7 +194,14 @@ export const histogramVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts index 5ac833190dd38..4e6056bbdae4f 100644 --- a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts +++ b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts @@ -102,7 +102,7 @@ export const horizontalBarVisTypeDefinition = { drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true, - circlesRadius: 3, + circlesRadius: 1, }, ], addTooltip: true, @@ -159,7 +159,14 @@ export const horizontalBarVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -169,7 +176,14 @@ export const horizontalBarVisTypeDefinition = { }), min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -179,7 +193,14 @@ export const horizontalBarVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/xy/public/vis_types/line.ts b/src/plugins/vis_types/xy/public/vis_types/line.ts index f7467ca53fa0e..affcc64320df6 100644 --- a/src/plugins/vis_types/xy/public/vis_types/line.ts +++ b/src/plugins/vis_types/xy/public/vis_types/line.ts @@ -99,7 +99,7 @@ export const lineVisTypeDefinition = { lineWidth: 2, interpolate: InterpolationMode.Linear, showCircles: true, - circlesRadius: 3, + circlesRadius: 1, }, ], addTooltip: true, @@ -151,7 +151,14 @@ export const lineVisTypeDefinition = { title: i18n.translate('visTypeXy.line.segmentTitle', { defaultMessage: 'X-axis' }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -161,7 +168,14 @@ export const lineVisTypeDefinition = { }), min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -171,7 +185,14 @@ export const lineVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap index 56e2cb1b60f3c..98d37568e4541 100644 --- a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap +++ b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap @@ -6,29 +6,42 @@ exports[`VisualizationNoResults should render according to snapshot 1`] = ` data-test-subj="visNoResult" >
-
-
+ +
+
- No results found + +
+
+ No results found +
+
+
-
+
`; diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/check_for_duplicate_title.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/check_for_duplicate_title.ts new file mode 100644 index 0000000000000..c0820cce45c90 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/check_for_duplicate_title.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { OverlayStart, SavedObjectsClientContract } from 'kibana/public'; +import type { SavedObject } from '../../../../saved_objects/public'; +import { SAVE_DUPLICATE_REJECTED } from './constants'; +import { findObjectByTitle } from './find_object_by_title'; +import { displayDuplicateTitleConfirmModal } from './display_duplicate_title_confirm_modal'; + +/** + * check for an existing SavedObject with the same title in ES + * returns Promise when it's no duplicate, or the modal displaying the warning + * that's there's a duplicate is confirmed, else it returns a rejected Promise + * @param savedObject + * @param isTitleDuplicateConfirmed + * @param onTitleDuplicate + * @param services + */ +export async function checkForDuplicateTitle( + savedObject: Pick< + SavedObject, + 'id' | 'title' | 'getDisplayName' | 'lastSavedTitle' | 'copyOnSave' | 'getEsType' + >, + isTitleDuplicateConfirmed: boolean, + onTitleDuplicate: (() => void) | undefined, + services: { + savedObjectsClient: SavedObjectsClientContract; + overlays: OverlayStart; + } +): Promise { + const { savedObjectsClient, overlays } = services; + // Don't check for duplicates if user has already confirmed save with duplicate title + if (isTitleDuplicateConfirmed) { + return true; + } + + // Don't check if the user isn't updating the title, otherwise that would become very annoying to have + // to confirm the save every time, except when copyOnSave is true, then we do want to check. + if (savedObject.title === savedObject.lastSavedTitle && !savedObject.copyOnSave) { + return true; + } + + const duplicate = await findObjectByTitle( + savedObjectsClient, + savedObject.getEsType(), + savedObject.title + ); + + if (!duplicate || duplicate.id === savedObject.id) { + return true; + } + + if (onTitleDuplicate) { + onTitleDuplicate(); + return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); + } + + // TODO: make onTitleDuplicate a required prop and remove UI components from this class + // Need to leave here until all users pass onTitleDuplicate. + return displayDuplicateTitleConfirmModal(savedObject, overlays); +} diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/confirm_modal_promise.tsx b/src/plugins/visualizations/public/utils/saved_objects_utils/confirm_modal_promise.tsx new file mode 100644 index 0000000000000..3c29fd958465b --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/confirm_modal_promise.tsx @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import type { OverlayStart } from 'kibana/public'; +import { EuiConfirmModal } from '@elastic/eui'; +import { toMountPoint } from '../../../../kibana_react/public'; + +export function confirmModalPromise( + message = '', + title = '', + confirmBtnText = '', + overlays: OverlayStart +): Promise { + return new Promise((resolve, reject) => { + const cancelButtonText = i18n.translate('visualizations.confirmModal.cancelButtonLabel', { + defaultMessage: 'Cancel', + }); + + const modal = overlays.openModal( + toMountPoint( + { + modal.close(); + reject(); + }} + onConfirm={() => { + modal.close(); + resolve(true); + }} + confirmButtonText={confirmBtnText} + cancelButtonText={cancelButtonText} + title={title} + > + {message} + + ) + ); + }); +} diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/constants.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/constants.ts new file mode 100644 index 0000000000000..fcabc0b493f68 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/constants.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +/** An error message to be used when the user rejects a confirm overwrite. */ +export const OVERWRITE_REJECTED = i18n.translate('visualizations.overwriteRejectedDescription', { + defaultMessage: 'Overwrite confirmation was rejected', +}); + +/** An error message to be used when the user rejects a confirm save with duplicate title. */ +export const SAVE_DUPLICATE_REJECTED = i18n.translate( + 'visualizations.saveDuplicateRejectedDescription', + { + defaultMessage: 'Save with duplicate title confirmation was rejected', + } +); diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/display_duplicate_title_confirm_modal.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/display_duplicate_title_confirm_modal.ts new file mode 100644 index 0000000000000..48ada48511812 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/display_duplicate_title_confirm_modal.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { OverlayStart } from 'kibana/public'; +import type { SavedObject } from '../../../../saved_objects/public'; +import { SAVE_DUPLICATE_REJECTED } from './constants'; +import { confirmModalPromise } from './confirm_modal_promise'; + +export function displayDuplicateTitleConfirmModal( + savedObject: Pick, + overlays: OverlayStart +): Promise { + const confirmMessage = i18n.translate( + 'visualizations.confirmModal.saveDuplicateConfirmationMessage', + { + defaultMessage: `A {name} with the title '{title}' already exists. Would you like to save anyway?`, + values: { title: savedObject.title, name: savedObject.getDisplayName() }, + } + ); + + const confirmButtonText = i18n.translate('visualizations.confirmModal.saveDuplicateButtonLabel', { + defaultMessage: 'Save {name}', + values: { name: savedObject.getDisplayName() }, + }); + try { + return confirmModalPromise(confirmMessage, '', confirmButtonText, overlays); + } catch { + return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); + } +} diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.test.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.test.ts new file mode 100644 index 0000000000000..d61fe1c13eee4 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { findObjectByTitle } from './find_object_by_title'; +import { + SimpleSavedObject, + SavedObjectsClientContract, + SavedObject, +} from '../../../../../core/public'; + +describe('findObjectByTitle', () => { + const savedObjectsClient: SavedObjectsClientContract = {} as SavedObjectsClientContract; + + beforeEach(() => { + savedObjectsClient.find = jest.fn(); + }); + + it('returns undefined if title is not provided', async () => { + const match = await findObjectByTitle(savedObjectsClient, 'index-pattern', ''); + expect(match).toBeUndefined(); + }); + + it('matches any case', async () => { + const indexPattern = new SimpleSavedObject(savedObjectsClient, { + attributes: { title: 'foo' }, + } as SavedObject); + savedObjectsClient.find = jest.fn().mockImplementation(() => + Promise.resolve({ + savedObjects: [indexPattern], + }) + ); + const match = await findObjectByTitle(savedObjectsClient, 'index-pattern', 'FOO'); + expect(match).toEqual(indexPattern); + }); +}); diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts new file mode 100644 index 0000000000000..10289ac0f2f53 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + SavedObjectsClientContract, + SimpleSavedObject, + SavedObjectAttributes, +} from 'kibana/public'; + +/** Returns an object matching a given title */ +export async function findObjectByTitle( + savedObjectsClient: SavedObjectsClientContract, + type: string, + title: string +): Promise | void> { + if (!title) { + return; + } + + // Elastic search will return the most relevant results first, which means exact matches should come + // first, and so we shouldn't need to request everything. Using 10 just to be on the safe side. + const response = await savedObjectsClient.find({ + type, + perPage: 10, + search: `"${title}"`, + searchFields: ['title'], + fields: ['title'], + }); + return response.savedObjects.find( + (obj) => obj.get('title').toLowerCase() === title.toLowerCase() + ); +} diff --git a/packages/kbn-es/src/errors.js b/src/plugins/visualizations/public/utils/saved_objects_utils/index.ts similarity index 63% rename from packages/kbn-es/src/errors.js rename to src/plugins/visualizations/public/utils/saved_objects_utils/index.ts index 87490168bf5ee..e993ddd96a7d9 100644 --- a/packages/kbn-es/src/errors.js +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/index.ts @@ -6,12 +6,5 @@ * Side Public License, v 1. */ -exports.createCliError = function (message) { - const error = new Error(message); - error.isCliError = true; - return error; -}; - -exports.isCliError = function (error) { - return error && error.isCliError; -}; +export { saveWithConfirmation } from './save_with_confirmation'; +export { checkForDuplicateTitle } from './check_for_duplicate_title'; diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.test.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.test.ts new file mode 100644 index 0000000000000..6d2c8f6bbe089 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { SavedObjectAttributes, SavedObjectsCreateOptions, OverlayStart } from 'kibana/public'; +import type { SavedObjectsClientContract } from 'kibana/public'; +import { saveWithConfirmation } from './save_with_confirmation'; +import * as deps from './confirm_modal_promise'; +import { OVERWRITE_REJECTED } from './constants'; + +describe('saveWithConfirmation', () => { + const savedObjectsClient: SavedObjectsClientContract = {} as SavedObjectsClientContract; + const overlays: OverlayStart = {} as OverlayStart; + const source: SavedObjectAttributes = {} as SavedObjectAttributes; + const options: SavedObjectsCreateOptions = {} as SavedObjectsCreateOptions; + const savedObject = { + getEsType: () => 'test type', + title: 'test title', + displayName: 'test display name', + }; + + beforeEach(() => { + savedObjectsClient.create = jest.fn(); + jest.spyOn(deps, 'confirmModalPromise').mockReturnValue(Promise.resolve({} as any)); + }); + + test('should call create of savedObjectsClient', async () => { + await saveWithConfirmation(source, savedObject, options, { savedObjectsClient, overlays }); + expect(savedObjectsClient.create).toHaveBeenCalledWith( + savedObject.getEsType(), + source, + options + ); + }); + + test('should call confirmModalPromise when such record exists', async () => { + savedObjectsClient.create = jest + .fn() + .mockImplementation((type, src, opt) => + opt && opt.overwrite ? Promise.resolve({} as any) : Promise.reject({ res: { status: 409 } }) + ); + + await saveWithConfirmation(source, savedObject, options, { savedObjectsClient, overlays }); + expect(deps.confirmModalPromise).toHaveBeenCalledWith( + expect.any(String), + expect.any(String), + expect.any(String), + overlays + ); + }); + + test('should call create of savedObjectsClient when overwriting confirmed', async () => { + savedObjectsClient.create = jest + .fn() + .mockImplementation((type, src, opt) => + opt && opt.overwrite ? Promise.resolve({} as any) : Promise.reject({ res: { status: 409 } }) + ); + + await saveWithConfirmation(source, savedObject, options, { savedObjectsClient, overlays }); + expect(savedObjectsClient.create).toHaveBeenLastCalledWith(savedObject.getEsType(), source, { + overwrite: true, + ...options, + }); + }); + + test('should reject when overwriting denied', async () => { + savedObjectsClient.create = jest.fn().mockReturnValue(Promise.reject({ res: { status: 409 } })); + jest.spyOn(deps, 'confirmModalPromise').mockReturnValue(Promise.reject()); + + expect.assertions(1); + await expect( + saveWithConfirmation(source, savedObject, options, { + savedObjectsClient, + overlays, + }) + ).rejects.toThrow(OVERWRITE_REJECTED); + }); +}); diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts new file mode 100644 index 0000000000000..de9ba38343548 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import type { + SavedObjectAttributes, + SavedObjectsCreateOptions, + OverlayStart, + SavedObjectsClientContract, +} from 'kibana/public'; +import { OVERWRITE_REJECTED } from './constants'; +import { confirmModalPromise } from './confirm_modal_promise'; + +/** + * Attempts to create the current object using the serialized source. If an object already + * exists, a warning message requests an overwrite confirmation. + * @param source - serialized version of this object what will be indexed into elasticsearch. + * @param savedObject - a simple object that contains properties title and displayName, and getEsType method + * @param options - options to pass to the saved object create method + * @param services - provides Kibana services savedObjectsClient and overlays + * @returns {Promise} - A promise that is resolved with the objects id if the object is + * successfully indexed. If the overwrite confirmation was rejected, an error is thrown with + * a confirmRejected = true parameter so that case can be handled differently than + * a create or index error. + * @resolved {SavedObject} + */ +export async function saveWithConfirmation( + source: SavedObjectAttributes, + savedObject: { + getEsType(): string; + title: string; + displayName: string; + }, + options: SavedObjectsCreateOptions, + services: { savedObjectsClient: SavedObjectsClientContract; overlays: OverlayStart } +) { + const { savedObjectsClient, overlays } = services; + try { + return await savedObjectsClient.create(savedObject.getEsType(), source, options); + } catch (err) { + // record exists, confirm overwriting + if (get(err, 'res.status') === 409) { + const confirmMessage = i18n.translate( + 'visualizations.confirmModal.overwriteConfirmationMessage', + { + defaultMessage: 'Are you sure you want to overwrite {title}?', + values: { title: savedObject.title }, + } + ); + + const title = i18n.translate('visualizations.confirmModal.overwriteTitle', { + defaultMessage: 'Overwrite {name}?', + values: { name: savedObject.displayName }, + }); + const confirmButtonText = i18n.translate('visualizations.confirmModal.overwriteButtonLabel', { + defaultMessage: 'Overwrite', + }); + + return confirmModalPromise(confirmMessage, title, confirmButtonText, overlays) + .then(() => + savedObjectsClient.create(savedObject.getEsType(), source, { + overwrite: true, + ...options, + }) + ) + .catch(() => Promise.reject(new Error(OVERWRITE_REJECTED))); + } + return await Promise.reject(err); + } +} diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts index 5c8c0594d3563..fe2453fbb78a4 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts @@ -56,10 +56,11 @@ const mockCheckForDuplicateTitle = jest.fn(() => { } }); const mockSaveWithConfirmation = jest.fn(() => ({ id: 'test-after-confirm' })); -jest.mock('../../../../plugins/saved_objects/public', () => ({ +jest.mock('./saved_objects_utils/check_for_duplicate_title', () => ({ checkForDuplicateTitle: jest.fn(() => mockCheckForDuplicateTitle()), +})); +jest.mock('./saved_objects_utils/save_with_confirmation', () => ({ saveWithConfirmation: jest.fn(() => mockSaveWithConfirmation()), - isErrorNonFatal: jest.fn(() => true), })); describe('saved_visualize_utils', () => { @@ -263,15 +264,19 @@ describe('saved_visualize_utils', () => { describe('isTitleDuplicateConfirmed', () => { it('as false we should not save vis with duplicated title', async () => { isTitleDuplicateConfirmed = false; - const savedVisId = await saveVisualization( - vis, - { isTitleDuplicateConfirmed }, - { savedObjectsClient, overlays } - ); + try { + const savedVisId = await saveVisualization( + vis, + { isTitleDuplicateConfirmed }, + { savedObjectsClient, overlays } + ); + expect(savedVisId).toBe(''); + } catch { + // ignore + } expect(savedObjectsClient.create).not.toHaveBeenCalled(); expect(mockSaveWithConfirmation).not.toHaveBeenCalled(); expect(mockCheckForDuplicateTitle).toHaveBeenCalled(); - expect(savedVisId).toBe(''); expect(vis.id).toBeUndefined(); }); diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts index a28ee9486c4d2..f221fa6a208b8 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts @@ -22,11 +22,7 @@ import { parseSearchSourceJSON, DataPublicPluginStart, } from '../../../../plugins/data/public'; -import { - checkForDuplicateTitle, - saveWithConfirmation, - isErrorNonFatal, -} from '../../../../plugins/saved_objects/public'; +import { saveWithConfirmation, checkForDuplicateTitle } from './saved_objects_utils'; import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; import type { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/public'; import { VisualizationsAppExtension } from '../vis_types/vis_type_alias_registry'; @@ -41,6 +37,7 @@ import type { TypesStart, BaseVisType } from '../vis_types'; // @ts-ignore import { updateOldState } from '../legacy/vis_update_state'; import { injectReferences, extractReferences } from './saved_visualization_references'; +import { OVERWRITE_REJECTED, SAVE_DUPLICATE_REJECTED } from './saved_objects_utils/constants'; export const SAVED_VIS_TYPE = 'visualization'; @@ -395,7 +392,7 @@ export async function saveVisualization( return savedObject.id; } catch (err: any) { savedObject.id = originalId; - if (isErrorNonFatal(err)) { + if (err && [OVERWRITE_REJECTED, SAVE_DUPLICATE_REJECTED].includes(err.message)) { return ''; } return Promise.reject(err); diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts index 1f6fbfeb47e59..06e06a4fefa0c 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts @@ -1674,7 +1674,8 @@ describe('migration visualization', () => { type = 'area', categoryAxes?: object[], valueAxes?: object[], - hasPalette = false + hasPalette = false, + hasCirclesRadius = false ) => ({ attributes: { title: 'My Vis', @@ -1694,6 +1695,21 @@ describe('migration visualization', () => { labels: {}, }, ], + seriesParams: [ + { + show: true, + type, + mode: 'stacked', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + ...(hasCirclesRadius && { + circlesRadius: 3, + }), + }, + ], ...(hasPalette && { palette: { type: 'palette', @@ -1732,6 +1748,20 @@ describe('migration visualization', () => { expect(palette.name).toEqual('default'); }); + it("should decorate existing docs with the circlesRadius attribute if it doesn't exist", () => { + const migratedTestDoc = migrate(getTestDoc()); + const [result] = JSON.parse(migratedTestDoc.attributes.visState).params.seriesParams; + + expect(result.circlesRadius).toEqual(1); + }); + + it('should not decorate existing docs with the circlesRadius attribute if it exists', () => { + const migratedTestDoc = migrate(getTestDoc('area', undefined, undefined, true, true)); + const [result] = JSON.parse(migratedTestDoc.attributes.visState).params.seriesParams; + + expect(result.circlesRadius).toEqual(3); + }); + describe('labels.filter', () => { it('should keep existing categoryAxes labels.filter value', () => { const migratedTestDoc = migrate(getTestDoc('area', [{ labels: { filter: false } }])); diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts index b598d34943e6c..4c8771a2f6924 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts @@ -867,6 +867,20 @@ const decorateAxes = ( }, })); +/** + * Defaults circlesRadius to 1 if it is not configured + */ +const addCirclesRadius = (axes: T[]): T[] => + axes.map((axis) => { + const hasCircleRadiusAttribute = Number.isFinite(axis?.circlesRadius); + return { + ...axis, + ...(!hasCircleRadiusAttribute && { + circlesRadius: 1, + }), + }; + }); + // Inlined from vis_type_xy const CHART_TYPE_AREA = 'area'; const CHART_TYPE_LINE = 'line'; @@ -913,10 +927,12 @@ const migrateVislibAreaLineBarTypes: SavedObjectMigrationFn = (doc) => valueAxes: visState.params.valueAxes && decorateAxes(visState.params.valueAxes, isHorizontalBar), + seriesParams: + visState.params.seriesParams && addCirclesRadius(visState.params.seriesParams), isVislibVis: true, detailedTooltip: true, ...(isLineOrArea && { - fittingFunction: 'zero', + fittingFunction: 'linear', }), }, }), diff --git a/test/common/config.js b/test/common/config.js index b9ab24450ac82..1a60932581847 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -56,6 +56,10 @@ export default function () { ...(!!process.env.CODE_COVERAGE ? [`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'coverage')}`] : []), + '--logging.appenders.deprecation.type=console', + '--logging.appenders.deprecation.layout.type=json', + '--logging.loggers[0].name=elasticsearch.deprecation', + '--logging.loggers[0].appenders[0]=deprecation', ], }, services, diff --git a/test/functional/apps/context/_context_navigation.ts b/test/functional/apps/context/_context_navigation.ts index 9b8d33208dfb1..c7337b91bf0c9 100644 --- a/test/functional/apps/context/_context_navigation.ts +++ b/test/functional/apps/context/_context_navigation.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; const TEST_FILTER_COLUMN_NAMES = [ @@ -22,6 +23,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const docTable = getService('docTable'); const PageObjects = getPageObjects(['common', 'context', 'discover', 'timePicker']); const kibanaServer = getService('kibanaServer'); + const filterBar = getService('filterBar'); + const find = getService('find'); describe('discover - context - back navigation', function contextSize() { before(async function () { @@ -56,5 +59,29 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return initialHitCount === hitCount; }); }); + + it('should go back via breadcrumbs with preserved state', async function () { + await retry.waitFor( + 'user navigating to context and returning to discover via breadcrumbs', + async () => { + await docTable.clickRowToggle({ rowIndex: 0 }); + const rowActions = await docTable.getRowActions({ rowIndex: 0 }); + await rowActions[0].click(); + await PageObjects.context.waitUntilContextLoadingHasFinished(); + + await find.clickByCssSelector(`[data-test-subj="breadcrumb first"]`); + await PageObjects.discover.waitForDocTableLoadingComplete(); + + for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { + expect(await filterBar.hasFilter(columnName, value)).to.eql(true); + } + expect(await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes()).to.eql({ + start: 'Sep 18, 2015 @ 06:31:44.000', + end: 'Sep 23, 2015 @ 18:31:44.000', + }); + return true; + } + ); + }); }); } 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/apps/dashboard/url_field_formatter.ts b/test/functional/apps/dashboard/url_field_formatter.ts index 254d71294d8c7..16cdb62768219 100644 --- a/test/functional/apps/dashboard/url_field_formatter.ts +++ b/test/functional/apps/dashboard/url_field_formatter.ts @@ -56,7 +56,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await common.navigateToApp('dashboard'); await dashboard.loadSavedDashboard('dashboard with table'); await dashboard.waitForRenderComplete(); - const fieldLink = await visChart.getFieldLinkInVisTable(`${fieldName}: Descending`, 1); + const fieldLink = await visChart.getFieldLinkInVisTable(`${fieldName}: Descending`); await clickFieldAndCheckUrl(fieldLink); }); diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts index 91c2d5914732d..4a4e06e28c321 100644 --- a/test/functional/apps/discover/_data_grid_field_data.ts +++ b/test/functional/apps/discover/_data_grid_field_data.ts @@ -71,7 +71,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSearchingHasFinished(); await retry.waitFor('first cell contains expected timestamp', async () => { - const cell = await dataGrid.getCellElement(1, 3); + const cell = await dataGrid.getCellElement(0, 2); const text = await cell.getVisibleText(); return text === expectedTimeStamp; }); diff --git a/test/functional/apps/visualize/_data_table.ts b/test/functional/apps/visualize/_data_table.ts index 14181c084a77f..77973b8fb9b67 100644 --- a/test/functional/apps/visualize/_data_table.ts +++ b/test/functional/apps/visualize/_data_table.ts @@ -268,7 +268,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should apply correct filter', async () => { - await PageObjects.visChart.filterOnTableCell(1, 3); + await PageObjects.visChart.filterOnTableCell(0, 2); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); const data = await PageObjects.visChart.getTableVisContent(); expect(data).to.be.eql([ diff --git a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts index ef664bf4b3054..51ceef947bfac 100644 --- a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts +++ b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts @@ -70,7 +70,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { // hover and click on cell to filter - await PageObjects.visChart.filterOnTableCell(1, 2); + await PageObjects.visChart.filterOnTableCell(0, 1); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); diff --git a/test/functional/apps/visualize/_embedding_chart.ts b/test/functional/apps/visualize/_embedding_chart.ts index 93ab2987dc4a8..9531eafc33bed 100644 --- a/test/functional/apps/visualize/_embedding_chart.ts +++ b/test/functional/apps/visualize/_embedding_chart.ts @@ -83,7 +83,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should allow to change timerange from the visualization in embedded mode', async () => { await retry.try(async () => { - await PageObjects.visChart.filterOnTableCell(1, 7); + await PageObjects.visChart.filterOnTableCell(0, 6); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); 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/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index d9f183ddd5332..dc36197034691 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -349,10 +349,12 @@ export class VisualizeChartPageObject extends FtrService { return await this.testSubjects.getVisibleText('dataGridHeader'); } - public async getFieldLinkInVisTable(fieldName: string, rowIndex: number = 1) { - const headers = await this.dataGrid.getHeaders(); - const fieldColumnIndex = headers.indexOf(fieldName); - const cell = await this.dataGrid.getCellElement(rowIndex, fieldColumnIndex + 1); + public async getFieldLinkInVisTable( + fieldName: string, + rowIndex: number = 0, + colIndex: number = 0 + ) { + const cell = await this.dataGrid.getCellElement(rowIndex, colIndex); return await cell.findByTagName('a'); } diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index f54e7b65a46e2..d49ef5fa0990a 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -81,18 +81,12 @@ export class DataGridService extends FtrService { /** * Returns a grid cell element by row & column indexes. - * The row offset equals 1 since the first row of data grid is the header row. - * @param rowIndex data row index starting from 1 (1 means 1st row) - * @param columnIndex column index starting from 1 (1 means 1st column) + * @param rowIndex data row index starting from 0 (0 means 1st row) + * @param columnIndex column index starting from 0 (0 means 1st column) */ - public async getCellElement(rowIndex: number, columnIndex: number) { - const table = await this.find.byCssSelector('.euiDataGrid'); - const $ = await table.parseDomContent(); - const columnNumber = $('.euiDataGridHeaderCell__content').length; + public async getCellElement(rowIndex: number = 0, columnIndex: number = 0) { return await this.find.byCssSelector( - `[data-test-subj="dataGridWrapper"] [data-test-subj="dataGridRowCell"]:nth-of-type(${ - columnNumber * (rowIndex - 1) + columnIndex + 1 - })` + `[data-test-subj="dataGridWrapper"] [data-test-subj="dataGridRowCell"][data-gridcell-id="${rowIndex},${columnIndex}"]` ); } diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs_sampler.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_sampler.ts new file mode 100644 index 0000000000000..d2411b2416067 --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_sampler.ts @@ -0,0 +1,121 @@ +/* + * Copyright 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 { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function ({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + + describe('esaggs_sampler', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + const timeRange = { + from: '2015-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + + describe('aggSampler', () => { + it('can execute aggSampler', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggSampler id="0" enabled=true schema="bucket"} + aggs={aggAvg id="1" enabled=true schema="metric" field="bytes"} + `; + const result = await expectExpression('sampler', expression).getResponse(); + + expect(result.columns.length).to.be(2); + const samplerColumn = result.columns[0]; + expect(samplerColumn.name).to.be('sampler'); + expect(samplerColumn.meta.sourceParams.params).to.eql({}); + + expect(result.rows.length).to.be(1); + expect(Object.keys(result.rows[0]).length).to.be(1); + const resultFromSample = result.rows[0]['col-1-1']; // check that sampler bucket doesn't produce columns + expect(typeof resultFromSample).to.be('number'); + expect(resultFromSample).to.greaterThan(0); // can't check exact metric using sample + }); + + it('can execute aggSampler with custom shard_size', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggSampler id="0" enabled=true schema="bucket" shard_size=20} + aggs={aggAvg id="1" enabled=true schema="metric" field="bytes"} + `; + const result = await expectExpression('sampler', expression).getResponse(); + + expect(result.columns.length).to.be(2); + const samplerColumn = result.columns[0]; + expect(samplerColumn.name).to.be('sampler'); + expect(samplerColumn.meta.sourceParams.params).to.eql({ shard_size: 20 }); + + expect(result.rows.length).to.be(1); + expect(Object.keys(result.rows[0]).length).to.be(1); // check that sampler bucket doesn't produce columns + const resultFromSample = result.rows[0]['col-1-1']; + expect(typeof resultFromSample).to.be('number'); + expect(resultFromSample).to.greaterThan(0); // can't check exact metric using sample + }); + }); + + describe('aggDiversifiedSampler', () => { + it('can execute aggDiversifiedSampler', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggDiversifiedSampler id="0" enabled=true schema="bucket" field="extension.raw"} + aggs={aggAvg id="1" enabled=true schema="metric" field="bytes"} + `; + const result = await expectExpression('sampler', expression).getResponse(); + + expect(result.columns.length).to.be(2); + const samplerColumn = result.columns[0]; + expect(samplerColumn.name).to.be('diversified_sampler'); + expect(samplerColumn.meta.sourceParams.params).to.eql({ field: 'extension.raw' }); + + expect(result.rows.length).to.be(1); + expect(Object.keys(result.rows[0]).length).to.be(1); + const resultFromSample = result.rows[0]['col-1-1']; // check that sampler bucket doesn't produce columns + expect(typeof resultFromSample).to.be('number'); + expect(resultFromSample).to.greaterThan(0); // can't check exact metric using sample + }); + + it('can execute aggSampler with custom shard_size and max_docs_per_value', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggDiversifiedSampler id="0" enabled=true schema="bucket" field="extension.raw" shard_size=20 max_docs_per_value=3} + aggs={aggAvg id="1" enabled=true schema="metric" field="bytes"} + `; + const result = await expectExpression('sampler', expression).getResponse(); + + expect(result.columns.length).to.be(2); + const samplerColumn = result.columns[0]; + expect(samplerColumn.name).to.be('diversified_sampler'); + expect(samplerColumn.meta.sourceParams.params).to.eql({ + field: 'extension.raw', + max_docs_per_value: 3, + shard_size: 20, + }); + + expect(result.rows.length).to.be(1); + expect(Object.keys(result.rows[0]).length).to.be(1); // check that sampler bucket doesn't produce columns + const resultFromSample = result.rows[0]['col-1-1']; + expect(typeof resultFromSample).to.be('number'); + expect(resultFromSample).to.greaterThan(0); // can't check exact metric using sample + }); + }); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts index 32f59fcf3df9c..fe2ccce23d94a 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -44,5 +44,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./esaggs')); loadTestFile(require.resolve('./esaggs_timeshift')); loadTestFile(require.resolve('./esaggs_multiterms')); + loadTestFile(require.resolve('./esaggs_sampler')); }); } 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/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index d370a278e0a5c..df650b11abfc0 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -299,7 +299,6 @@ describe('Task Runner', () => { expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent.mock.calls[0][0]).toMatchInlineSnapshot(` Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -450,7 +449,6 @@ describe('Task Runner', () => { const eventLogger = customTaskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-start', category: ['alerts'], @@ -582,7 +580,6 @@ describe('Task Runner', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(5, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', category: ['alerts'], kind: 'alert', outcome: 'success' }, kibana: { alerting: { @@ -671,7 +668,6 @@ describe('Task Runner', () => { expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-start', category: ['alerts'], @@ -767,7 +763,6 @@ describe('Task Runner', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(4, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', category: ['alerts'], @@ -931,7 +926,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -1001,7 +995,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -1272,7 +1265,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -1418,7 +1410,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -1569,7 +1560,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -1755,7 +1745,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -2139,7 +2128,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -2246,7 +2234,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -2481,7 +2468,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -2515,7 +2501,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, @@ -2590,7 +2575,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -2624,7 +2608,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, @@ -2708,7 +2691,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -2742,7 +2724,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, @@ -2826,7 +2807,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -2860,7 +2840,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, @@ -2943,7 +2922,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -2977,7 +2955,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, @@ -3238,7 +3215,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -3416,7 +3392,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -3525,7 +3500,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -3631,7 +3605,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -3732,7 +3705,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -3834,7 +3806,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -3930,7 +3901,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -4036,7 +4006,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -4134,7 +4103,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -4234,7 +4202,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -4383,7 +4350,6 @@ describe('Task Runner', () => { expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent.mock.calls[0][0]).toMatchInlineSnapshot(` Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -4463,7 +4429,6 @@ describe('Task Runner', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); expect(eventLogger.logEvent.mock.calls[0][0]).toStrictEqual({ - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-start', kind: 'alert', @@ -4484,7 +4449,6 @@ describe('Task Runner', () => { message: 'alert execution start: "1"', }); expect(eventLogger.logEvent.mock.calls[1][0]).toStrictEqual({ - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', kind: 'alert', diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 0cf5202787392..3cd1a2d1217dc 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -602,7 +602,6 @@ export class TaskRunner< const scheduleDelay = runDate.getTime() - this.taskInstance.runAt.getTime(); const event = createAlertEventLogRecordObject({ - timestamp: runDateString, ruleId: alertId, ruleType: this.alertType as UntypedNormalizedAlertType, action: EVENT_LOG_ACTIONS.execute, @@ -747,7 +746,6 @@ export class TaskRunner< const eventLogger = this.context.eventLogger; const event: IEvent = { - '@timestamp': new Date().toISOString(), event: { action: EVENT_LOG_ACTIONS.executeTimeout, kind: 'alert', diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index c82cc0a7f21e8..eb3e22f348ed7 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -196,7 +196,6 @@ describe('Task Runner Cancel', () => { expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-start', category: ['alerts'], @@ -225,7 +224,6 @@ describe('Task Runner Cancel', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-timeout', category: ['alerts'], @@ -250,7 +248,6 @@ describe('Task Runner Cancel', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', category: ['alerts'], @@ -424,7 +421,6 @@ describe('Task Runner Cancel', () => { expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-start', category: ['alerts'], @@ -453,7 +449,6 @@ describe('Task Runner Cancel', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-timeout', category: ['alerts'], @@ -479,7 +474,6 @@ describe('Task Runner Cancel', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', category: ['alerts'], @@ -539,7 +533,6 @@ describe('Task Runner Cancel', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(6); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-start', category: ['alerts'], @@ -569,7 +562,6 @@ describe('Task Runner Cancel', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-timeout', category: ['alerts'], @@ -689,7 +681,6 @@ describe('Task Runner Cancel', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(6, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', category: ['alerts'], kind: 'alert', outcome: 'success' }, kibana: { alerting: { diff --git a/x-pack/plugins/apm/common/agent_key_types.ts b/x-pack/plugins/apm/common/agent_key_types.ts new file mode 100644 index 0000000000000..986e67d35698e --- /dev/null +++ b/x-pack/plugins/apm/common/agent_key_types.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 interface CreateApiKeyResponse { + api_key: string; + expiration?: number; + id: string; + name: 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/correlations/field_stats_types.ts b/x-pack/plugins/apm/common/correlations/field_stats_types.ts index 50dc7919fbd00..41f7e3c3c6649 100644 --- a/x-pack/plugins/apm/common/correlations/field_stats_types.ts +++ b/x-pack/plugins/apm/common/correlations/field_stats_types.ts @@ -8,9 +8,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { CorrelationsParams } from './types'; -export interface FieldStatsCommonRequestParams extends CorrelationsParams { - samplerShardSize: number; -} +export type FieldStatsCommonRequestParams = CorrelationsParams; export interface Field { fieldName: string; @@ -55,3 +53,5 @@ export type FieldStats = | NumericFieldStats | KeywordFieldStats | BooleanFieldStats; + +export type FieldValueFieldStats = TopValuesStats; 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/common/fleet.ts b/x-pack/plugins/apm/common/fleet.ts index 00a958952d2de..bd8c6cf2653c2 100644 --- a/x-pack/plugins/apm/common/fleet.ts +++ b/x-pack/plugins/apm/common/fleet.ts @@ -8,7 +8,7 @@ import semverParse from 'semver/functions/parse'; export const POLICY_ELASTIC_AGENT_ON_CLOUD = 'policy-elastic-agent-on-cloud'; -export const SUPPORTED_APM_PACKAGE_VERSION = '7.16.0'; +export const SUPPORTED_APM_PACKAGE_VERSION = '8.0.0-dev4'; // TODO update to just '8.0.0' once published export function isPrereleaseVersion(version: string) { return semverParse(version)?.prerelease?.length ?? 0 > 0; 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/agent_keys/agent_keys_table.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx index 4a05f38d8e505..f49264242e63f 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx @@ -18,10 +18,10 @@ import { ConfirmDeleteModal } from './confirm_delete_modal'; interface Props { agentKeys: ApiKey[]; - refetchAgentKeys: () => void; + onKeyDelete: () => void; } -export function AgentKeysTable({ agentKeys, refetchAgentKeys }: Props) { +export function AgentKeysTable({ agentKeys, onKeyDelete }: Props) { const [agentKeyToBeDeleted, setAgentKeyToBeDeleted] = useState(); const columns: Array> = [ @@ -159,7 +159,7 @@ export function AgentKeysTable({ agentKeys, refetchAgentKeys }: Props) { agentKey={agentKeyToBeDeleted} onConfirm={() => { setAgentKeyToBeDeleted(undefined); - refetchAgentKeys(); + onKeyDelete(); }} /> )} diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/create_agent_key.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/create_agent_key.tsx new file mode 100644 index 0000000000000..5803e5a2a75a8 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/create_agent_key.tsx @@ -0,0 +1,244 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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, { useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButton, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, + EuiButtonEmpty, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiFieldText, + EuiText, + EuiFormFieldset, + EuiCheckbox, + htmlIdGenerator, +} from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ApmPluginStartDeps } from '../../../../plugin'; +import { CreateApiKeyResponse } from '../../../../../common/agent_key_types'; + +interface Props { + onCancel: () => void; + onSuccess: (agentKey: CreateApiKeyResponse) => void; + onError: (keyName: string) => void; +} + +export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) { + const { + services: { security }, + } = useKibana(); + + const [username, setUsername] = useState(''); + + const [formTouched, setFormTouched] = useState(false); + const [keyName, setKeyName] = useState(''); + const [agentConfigChecked, setAgentConfigChecked] = useState(true); + const [eventWriteChecked, setEventWriteChecked] = useState(true); + const [sourcemapChecked, setSourcemapChecked] = useState(true); + + const isInputInvalid = isEmpty(keyName); + const isFormInvalid = formTouched && isInputInvalid; + + const formError = i18n.translate( + 'xpack.apm.settings.agentKeys.createKeyFlyout.name.placeholder', + { defaultMessage: 'Enter a name' } + ); + + useEffect(() => { + const getCurrentUser = async () => { + try { + const authenticatedUser = await security?.authc.getCurrentUser(); + setUsername(authenticatedUser?.username || ''); + } catch { + setUsername(''); + } + }; + getCurrentUser(); + }, [security?.authc]); + + const createAgentKeyTitle = i18n.translate( + 'xpack.apm.settings.agentKeys.createKeyFlyout.createAgentKey', + { defaultMessage: 'Create agent key' } + ); + + const createAgentKey = async () => { + setFormTouched(true); + if (isInputInvalid) { + return; + } + + try { + const { agentKey } = await callApmApi({ + endpoint: 'POST /apm/agent_keys', + signal: null, + params: { + body: { + name: keyName, + sourcemap: sourcemapChecked, + event: eventWriteChecked, + agentConfig: agentConfigChecked, + }, + }, + }); + + onSuccess(agentKey); + } catch (error) { + onError(keyName); + } + }; + + return ( + + + +

{createAgentKeyTitle}

+
+
+ + + + {username && ( + + {username} + + )} + + setKeyName(e.target.value)} + isInvalid={isFormInvalid} + onBlur={() => setFormTouched(true)} + /> + + + + + setAgentConfigChecked((state) => !state)} + /> + + + + setEventWriteChecked((state) => !state)} + /> + + + + setSourcemapChecked((state) => !state)} + /> + + + + + + + + + + + {i18n.translate( + 'xpack.apm.settings.agentKeys.createKeyFlyout.cancelButton', + { + defaultMessage: 'Cancel', + } + )} + + + + + {createAgentKeyTitle} + + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/create_agent_key/agent_key_callout.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/create_agent_key/agent_key_callout.tsx new file mode 100644 index 0000000000000..db313e35a0229 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/create_agent_key/agent_key_callout.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiSpacer, + EuiCallOut, + EuiButtonIcon, + EuiCopy, + EuiFormControlLayout, +} from '@elastic/eui'; + +interface Props { + name: string; + token: string; +} + +export function AgentKeyCallOut({ name, token }: Props) { + return ( + <> + +

+ {i18n.translate( + 'xpack.apm.settings.agentKeys.copyAgentKeyField.message', + { + defaultMessage: + 'Copy this key now. You will not be able to view it again.', + } + )} +

+ + {(copy) => ( + + )} + + } + > + + +
+ + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/index.tsx index 23acc2e98dd73..8fb4ede96a819 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { Fragment } from 'react'; +import React, { Fragment, useState } from 'react'; import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; import { @@ -21,6 +21,11 @@ import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { PermissionDenied } from './prompts/permission_denied'; import { ApiKeysNotEnabled } from './prompts/api_keys_not_enabled'; import { AgentKeysTable } from './agent_keys_table'; +import { CreateAgentKeyFlyout } from './create_agent_key'; +import { AgentKeyCallOut } from './create_agent_key/agent_key_callout'; +import { CreateApiKeyResponse } from '../../../../../common/agent_key_types'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { ApiKey } from '../../../../../../security/common/model'; const INITIAL_DATA = { areApiKeysEnabled: false, @@ -28,33 +33,12 @@ const INITIAL_DATA = { }; export function AgentKeys() { - return ( - - - {i18n.translate('xpack.apm.settings.agentKeys.descriptionText', { - defaultMessage: - 'View and delete agent keys. An agent key sends requests on behalf of a user.', - })} - - - - - -

- {i18n.translate('xpack.apm.settings.agentKeys.title', { - defaultMessage: 'Agent keys', - })} -

-
-
-
- - -
- ); -} + const { toasts } = useApmPluginContext().core.notifications; + + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [createdAgentKey, setCreatedAgentKey] = + useState(); -function AgentKeysContent() { const { data: { areApiKeysEnabled, canManage } = INITIAL_DATA, status: privilegesStatus, @@ -85,16 +69,112 @@ function AgentKeysContent() { ); const agentKeys = data?.agentKeys; - const isLoading = - privilegesStatus === FETCH_STATUS.LOADING || - status === FETCH_STATUS.LOADING; - const requestFailed = - privilegesStatus === FETCH_STATUS.FAILURE || - status === FETCH_STATUS.FAILURE; + return ( + + + {i18n.translate('xpack.apm.settings.agentKeys.descriptionText', { + defaultMessage: + 'View and delete agent keys. An agent key sends requests on behalf of a user.', + })} + + + + + +

+ {i18n.translate('xpack.apm.settings.agentKeys.title', { + defaultMessage: 'Agent keys', + })} +

+
+
+ {areApiKeysEnabled && canManage && !isEmpty(agentKeys) && ( + + setIsFlyoutVisible(true)} + fill={true} + iconType="plusInCircle" + > + {i18n.translate( + 'xpack.apm.settings.agentKeys.createAgentKeyButton', + { + defaultMessage: 'Create agent key', + } + )} + + + )} +
+ + {createdAgentKey && ( + + )} + {isFlyoutVisible && ( + { + setIsFlyoutVisible(false); + }} + onSuccess={(agentKey: CreateApiKeyResponse) => { + setCreatedAgentKey(agentKey); + setIsFlyoutVisible(false); + refetchAgentKeys(); + }} + onError={(keyName: string) => { + toasts.addDanger( + i18n.translate('xpack.apm.settings.agentKeys.crate.failed', { + defaultMessage: 'Error creating agent key "{keyName}"', + values: { keyName }, + }) + ); + setIsFlyoutVisible(false); + }} + /> + )} + { + setCreatedAgentKey(undefined); + refetchAgentKeys(); + }} + onCreateAgentClick={() => setIsFlyoutVisible(true)} + /> +
+ ); +} +function AgentKeysContent({ + loading, + requestFailed, + canManage, + areApiKeysEnabled, + agentKeys, + onKeyDelete, + onCreateAgentClick, +}: { + loading: boolean; + requestFailed: boolean; + canManage: boolean; + areApiKeysEnabled: boolean; + agentKeys?: ApiKey[]; + onKeyDelete: () => void; + onCreateAgentClick: () => void; +}) { if (!agentKeys) { - if (isLoading) { + if (loading) { return ( } @@ -147,7 +227,7 @@ function AgentKeysContent() { title={

{i18n.translate('xpack.apm.settings.agentKeys.emptyPromptTitle', { - defaultMessage: 'Create your first agent key', + defaultMessage: 'Create your first key', })}

} @@ -155,12 +235,16 @@ function AgentKeysContent() {

{i18n.translate('xpack.apm.settings.agentKeys.emptyPromptBody', { defaultMessage: - 'Create agent keys to authorize requests to the APM Server.', + 'Create keys to authorize agent requests to the APM Server.', })}

} actions={ - + {i18n.translate( 'xpack.apm.settings.agentKeys.createAgentKeyButton', { @@ -175,10 +259,7 @@ function AgentKeysContent() { if (agentKeys && !isEmpty(agentKeys)) { return ( - + ); } 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/correlations/context_popover/context_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx index f1d0d194749c5..d7043ea669a03 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx @@ -11,14 +11,11 @@ import { EuiFlexItem, EuiPopover, EuiPopoverTitle, - EuiSpacer, - EuiText, EuiTitle, EuiToolTip, } from '@elastic/eui'; -import React, { Fragment, useState } from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { FieldStats } from '../../../../../common/correlations/field_stats_types'; import { OnAddFilter, TopValues } from './top_values'; import { useTheme } from '../../../../hooks/use_theme'; @@ -97,27 +94,11 @@ export function CorrelationsContextPopover({ {infoIsOpen ? ( - <> - - {topValueStats.topValuesSampleSize !== undefined && ( - - - - - - - )} - + ) : null} ); diff --git a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx index 05b4f6d56fa45..fbf33899a2de2 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx @@ -12,11 +12,21 @@ import { EuiProgress, EuiSpacer, EuiToolTip, + EuiText, + EuiHorizontalRule, + EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FieldStats } from '../../../../../common/correlations/field_stats_types'; +import numeral from '@elastic/numeral'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + FieldStats, + TopValueBucket, +} from '../../../../../common/correlations/field_stats_types'; import { asPercent } from '../../../../../common/utils/formatters'; import { useTheme } from '../../../../hooks/use_theme'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useFetchParams } from '../use_fetch_params'; export type OnAddFilter = ({ fieldName, @@ -28,23 +38,179 @@ export type OnAddFilter = ({ include: boolean; }) => void; -interface Props { +interface TopValueProps { + progressBarMax: number; + barColor: string; + value: TopValueBucket; + isHighlighted: boolean; + fieldName: string; + onAddFilter?: OnAddFilter; + valueText?: string; + reverseLabel?: boolean; +} +export function TopValue({ + progressBarMax, + barColor, + value, + isHighlighted, + fieldName, + onAddFilter, + valueText, + reverseLabel = false, +}: TopValueProps) { + const theme = useTheme(); + return ( + + + + {value.key} + + } + className="eui-textTruncate" + aria-label={value.key.toString()} + valueText={valueText} + labelProps={ + isHighlighted + ? { + style: { fontWeight: 'bold' }, + } + : undefined + } + /> + + {fieldName !== undefined && + value.key !== undefined && + onAddFilter !== undefined ? ( + <> + { + onAddFilter({ + fieldName, + fieldValue: + typeof value.key === 'number' + ? value.key.toString() + : value.key, + include: true, + }); + }} + aria-label={i18n.translate( + 'xpack.apm.correlations.fieldContextPopover.addFilterAriaLabel', + { + defaultMessage: 'Filter for {fieldName}: "{value}"', + values: { fieldName, value: value.key }, + } + )} + data-test-subj={`apmFieldContextTopValuesAddFilterButton-${value.key}-${value.key}`} + style={{ + minHeight: 'auto', + width: theme.eui.euiSizeL, + paddingRight: 2, + paddingLeft: 2, + paddingTop: 0, + paddingBottom: 0, + }} + /> + { + onAddFilter({ + fieldName, + fieldValue: + typeof value.key === 'number' + ? value.key.toString() + : value.key, + include: false, + }); + }} + aria-label={i18n.translate( + 'xpack.apm.correlations.fieldContextPopover.removeFilterAriaLabel', + { + defaultMessage: 'Filter out {fieldName}: "{value}"', + values: { fieldName, value: value.key }, + } + )} + data-test-subj={`apmFieldContextTopValuesExcludeFilterButton-${value.key}-${value.key}`} + style={{ + minHeight: 'auto', + width: theme.eui.euiSizeL, + paddingTop: 0, + paddingBottom: 0, + paddingRight: 2, + paddingLeft: 2, + }} + /> + + ) : null} + + ); +} + +interface TopValuesProps { topValueStats: FieldStats; compressed?: boolean; onAddFilter?: OnAddFilter; fieldValue?: string | number; } -export function TopValues({ topValueStats, onAddFilter, fieldValue }: Props) { +export function TopValues({ + topValueStats, + onAddFilter, + fieldValue, +}: TopValuesProps) { const { topValues, topValuesSampleSize, count, fieldName } = topValueStats; const theme = useTheme(); - if (!Array.isArray(topValues) || topValues.length === 0) return null; + const idxToHighlight = Array.isArray(topValues) + ? topValues.findIndex((value) => value.key === fieldValue) + : null; + + const params = useFetchParams(); + const { data: fieldValueStats, status } = useFetcher( + (callApmApi) => { + if ( + idxToHighlight === -1 && + fieldName !== undefined && + fieldValue !== undefined + ) { + return callApmApi({ + endpoint: 'GET /internal/apm/correlations/field_value_stats', + params: { + query: { + ...params, + fieldName, + fieldValue, + }, + }, + }); + } + }, + [params, fieldName, fieldValue, idxToHighlight] + ); + if ( + !Array.isArray(topValues) || + topValues?.length === 0 || + fieldValue === undefined + ) + return null; const sampledSize = typeof topValuesSampleSize === 'string' ? parseInt(topValuesSampleSize, 10) : topValuesSampleSize; + const progressBarMax = sampledSize ?? count; return (
- - - - {value.key} - - } - className="eui-textTruncate" - aria-label={value.key.toString()} - valueText={valueText} - labelProps={ - isHighlighted - ? { - style: { fontWeight: 'bold' }, - } - : undefined - } - /> - - {fieldName !== undefined && - value.key !== undefined && - onAddFilter !== undefined ? ( - <> - { - onAddFilter({ - fieldName, - fieldValue: - typeof value.key === 'number' - ? value.key.toString() - : value.key, - include: true, - }); - }} - aria-label={i18n.translate( - 'xpack.apm.correlations.fieldContextPopover.addFilterAriaLabel', - { - defaultMessage: 'Filter for {fieldName}: "{value}"', - values: { fieldName, value: value.key }, - } - )} - data-test-subj={`apmFieldContextTopValuesAddFilterButton-${value.key}-${value.key}`} - style={{ - minHeight: 'auto', - width: theme.eui.euiSizeL, - paddingRight: 2, - paddingLeft: 2, - paddingTop: 0, - paddingBottom: 0, - }} - /> - { - onAddFilter({ - fieldName, - fieldValue: - typeof value.key === 'number' - ? value.key.toString() - : value.key, - include: false, - }); - }} - aria-label={i18n.translate( - 'xpack.apm.correlations.fieldContextPopover.removeFilterAriaLabel', - { - defaultMessage: 'Filter out {fieldName}: "{value}"', - values: { fieldName, value: value.key }, - } - )} - data-test-subj={`apmFieldContextTopValuesExcludeFilterButton-${value.key}-${value.key}`} - style={{ - minHeight: 'auto', - width: theme.eui.euiSizeL, - paddingTop: 0, - paddingBottom: 0, - paddingRight: 2, - paddingLeft: 2, - }} - /> - - ) : null} - + ); })} + + {idxToHighlight === -1 && ( + <> + + + + + + {status === FETCH_STATUS.SUCCESS && + Array.isArray(fieldValueStats?.topValues) ? ( + fieldValueStats?.topValues.map((value) => { + const valueText = + progressBarMax !== undefined + ? asPercent(value.doc_count, progressBarMax) + : undefined; + + return ( + + ); + }) + ) : ( + + + + )} + + )} + + {topValueStats.topValuesSampleSize !== undefined && ( + <> + + + {i18n.translate( + 'xpack.apm.correlations.fieldContextPopover.calculatedFromSampleDescription', + { + defaultMessage: + 'Calculated from sample of {sampleSize} documents', + values: { sampleSize: topValueStats.topValuesSampleSize }, + } + )} + + + )}
); } 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..163082cf044cd 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 @@ -19,14 +19,14 @@ import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detecti import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; -import { useApmParams } from '../../../hooks/use_apm_params'; +import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; 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: '', @@ -46,9 +46,7 @@ function useServicesFetcher() { const { query: { rangeFrom, rangeTo, environment, kuery }, - } = - // @ts-ignore 4.3.5 upgrade - Type instantiation is excessively deep and possibly infinite. - useApmParams('/services/{serviceName}', '/services'); + } = useAnyOfApmParams('/services/{serviceName}', '/services'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); @@ -159,26 +157,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 +189,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/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index ea65c837a4177..fe91b14e64e8a 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -66,9 +66,11 @@ export function getServiceColumns({ showTransactionTypeColumn, comparisonData, breakpoints, + showHealthStatusColumn, }: { query: TypeOf['query']; showTransactionTypeColumn: boolean; + showHealthStatusColumn: boolean; breakpoints: Breakpoints; comparisonData?: ServicesDetailedStatisticsAPIResponse; }): Array> { @@ -76,21 +78,25 @@ export function getServiceColumns({ const showWhenSmallOrGreaterThanLarge = isSmall || !isLarge; const showWhenSmallOrGreaterThanXL = isSmall || !isXl; return [ - { - field: 'healthStatus', - name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { - defaultMessage: 'Health', - }), - width: `${unit * 6}px`, - sortable: true, - render: (_, { healthStatus }) => { - return ( - - ); - }, - }, + ...(showHealthStatusColumn + ? [ + { + field: 'healthStatus', + name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { + defaultMessage: 'Health', + }), + width: `${unit * 6}px`, + sortable: true, + render: (_, { healthStatus }) => { + return ( + + ); + }, + } as ITableColumn, + ] + : []), { field: 'serviceName', name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', { @@ -248,13 +254,17 @@ export function ServiceList({ showTransactionTypeColumn, comparisonData, breakpoints, + showHealthStatusColumn: displayHealthStatus, }), - [query, showTransactionTypeColumn, comparisonData, breakpoints] + [ + query, + showTransactionTypeColumn, + comparisonData, + breakpoints, + displayHealthStatus, + ] ); - const columns = displayHealthStatus - ? serviceColumns - : serviceColumns.filter((column) => column.field !== 'healthStatus'); const initialSortField = displayHealthStatus ? 'healthStatus' : 'transactionsPerMinute'; @@ -300,7 +310,7 @@ export function ServiceList({ { it('renders empty state', async () => { @@ -29,34 +55,10 @@ describe('ServiceList', () => { }); describe('responsive columns', () => { - const query = { - rangeFrom: 'now-15m', - rangeTo: 'now', - environment: ENVIRONMENT_ALL.value, - kuery: '', - }; - - const service: any = { - serviceName: 'opbeans-python', - agentName: 'python', - transactionsPerMinute: { - value: 86.93333333333334, - timeseries: [], - }, - errorsPerMinute: { - value: 12.6, - timeseries: [], - }, - avgResponseTime: { - value: 91535.42944785276, - timeseries: [], - }, - environments: ['test'], - transactionType: 'request', - }; describe('when small', () => { it('shows environment, transaction type and sparklines', () => { const renderedColumns = getServiceColumns({ + showHealthStatusColumn: true, query, showTransactionTypeColumn: true, breakpoints: { @@ -91,6 +93,7 @@ describe('ServiceList', () => { describe('when Large', () => { it('hides environment, transaction type and sparklines', () => { const renderedColumns = getServiceColumns({ + showHealthStatusColumn: true, query, showTransactionTypeColumn: true, breakpoints: { @@ -114,6 +117,7 @@ describe('ServiceList', () => { describe('when XL', () => { it('hides transaction type', () => { const renderedColumns = getServiceColumns({ + showHealthStatusColumn: true, query, showTransactionTypeColumn: true, breakpoints: { @@ -147,6 +151,7 @@ describe('ServiceList', () => { describe('when XXL', () => { it('hides transaction type', () => { const renderedColumns = getServiceColumns({ + showHealthStatusColumn: true, query, showTransactionTypeColumn: true, breakpoints: { @@ -181,20 +186,34 @@ describe('ServiceList', () => { }); describe('without ML data', () => { - it('sorts by throughput', async () => { - render(); - - expect(await screen.findByTitle('Throughput')).toBeInTheDocument(); + it('hides healthStatus column', () => { + const renderedColumns = getServiceColumns({ + showHealthStatusColumn: false, + query, + showTransactionTypeColumn: true, + breakpoints: { + isSmall: false, + isLarge: false, + isXl: false, + } as Breakpoints, + }).map((c) => c.field); + expect(renderedColumns.includes('healthStatus')).toBeFalsy(); }); }); describe('with ML data', () => { - it('renders the health column', async () => { - render(); - - expect( - await screen.findByRole('button', { name: /Health/ }) - ).toBeInTheDocument(); + it('shows healthStatus column', () => { + const renderedColumns = getServiceColumns({ + showHealthStatusColumn: true, + query, + showTransactionTypeColumn: true, + breakpoints: { + isSmall: false, + isLarge: false, + isXl: false, + } as Breakpoints, + }).map((c) => c.field); + expect(renderedColumns.includes('healthStatus')).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts b/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts index b0cc134778d21..74a49d06d761b 100644 --- a/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts @@ -7,26 +7,53 @@ import { getInfrastructureKQLFilter } from './'; describe('service logs', () => { + const serviceName = 'opbeans-node'; + describe('getInfrastructureKQLFilter', () => { - it('filter by container id', () => { + it('filter by service name', () => { + expect( + getInfrastructureKQLFilter( + { + serviceInfrastructure: { + containerIds: [], + hostNames: [], + }, + }, + serviceName + ) + ).toEqual('service.name: "opbeans-node"'); + }); + + it('filter by container id as fallback', () => { expect( - getInfrastructureKQLFilter({ - serviceInfrastructure: { - containerIds: ['foo', 'bar'], - hostNames: ['baz', `quz`], + getInfrastructureKQLFilter( + { + serviceInfrastructure: { + containerIds: ['foo', 'bar'], + hostNames: ['baz', `quz`], + }, }, - }) - ).toEqual('container.id: "foo" or container.id: "bar"'); + serviceName + ) + ).toEqual( + 'service.name: "opbeans-node" or (not service.name and (container.id: "foo" or container.id: "bar"))' + ); }); - it('filter by host names', () => { + + it('filter by host names as fallback', () => { expect( - getInfrastructureKQLFilter({ - serviceInfrastructure: { - containerIds: [], - hostNames: ['baz', `quz`], + getInfrastructureKQLFilter( + { + serviceInfrastructure: { + containerIds: [], + hostNames: ['baz', `quz`], + }, }, - }) - ).toEqual('host.name: "baz" or host.name: "quz"'); + serviceName + ) + ).toEqual( + 'service.name: "opbeans-node" or (not service.name and (host.name: "baz" or host.name: "quz"))' + ); }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx index bb32919196f84..9b4d8e7d52a28 100644 --- a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx @@ -18,6 +18,7 @@ import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { CONTAINER_ID, HOSTNAME, + SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useTimeRange } from '../../../hooks/use_time_range'; @@ -86,20 +87,27 @@ export function ServiceLogs() { height={'60vh'} startTimestamp={moment(start).valueOf()} endTimestamp={moment(end).valueOf()} - query={getInfrastructureKQLFilter(data)} + query={getInfrastructureKQLFilter(data, serviceName)} /> ); } export const getInfrastructureKQLFilter = ( - data?: APIReturnType<'GET /internal/apm/services/{serviceName}/infrastructure'> + data: + | APIReturnType<'GET /internal/apm/services/{serviceName}/infrastructure'> + | undefined, + serviceName: string ) => { const containerIds = data?.serviceInfrastructure?.containerIds ?? []; const hostNames = data?.serviceInfrastructure?.hostNames ?? []; - const kqlFilter = containerIds.length + const infraAttributes = containerIds.length ? containerIds.map((id) => `${CONTAINER_ID}: "${id}"`) : hostNames.map((id) => `${HOSTNAME}: "${id}"`); - return kqlFilter.join(' or '); + const infraAttributesJoined = infraAttributes.join(' or '); + + return infraAttributes.length + ? `${SERVICE_NAME}: "${serviceName}" or (not ${SERVICE_NAME} and (${infraAttributesJoined}))` + : `${SERVICE_NAME}: "${serviceName}"`; }; diff --git a/x-pack/plugins/apm/public/components/app/service_map/Controls.tsx b/x-pack/plugins/apm/public/components/app/service_map/Controls.tsx index 4605952a6f396..a48fb77b45585 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Controls.tsx @@ -16,7 +16,7 @@ import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_ import { APMQueryParams } from '../../shared/Links/url_helpers'; import { CytoscapeContext } from './Cytoscape'; import { getAnimationOptions, getNodeHeight } from './cytoscape_options'; -import { useApmParams } from '../../../hooks/use_apm_params'; +import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; const ControlsContainer = euiStyled('div')` left: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; @@ -107,7 +107,7 @@ export function Controls() { const { query: { kuery }, - } = useApmParams('/service-map', '/services/{serviceName}/service-map'); + } = useAnyOfApmParams('/service-map', '/services/{serviceName}/service-map'); const [zoom, setZoom] = useState((cy && cy.zoom()) || 1); const duration = parseInt(theme.eui.euiAnimSpeedFast, 10); diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx index a862ff872f61a..8fa93e22a90fe 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { useUiTracker } from '../../../../../../observability/public'; import { ContentsProps } from '.'; import { NodeStats } from '../../../../../common/service_map'; -import { useApmParams } from '../../../../hooks/use_apm_params'; +import { useAnyOfApmParams } from '../../../../hooks/use_apm_params'; import { useApmRouter } from '../../../../hooks/use_apm_router'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { ApmRoutes } from '../../../routing/apm_route_config'; @@ -25,8 +25,7 @@ export function BackendContents({ start, end, }: ContentsProps) { - // @ts-ignore 4.3.5 upgrade - Type instantiation is excessively deep and possibly infinite. - const { query } = useApmParams( + const { query } = useAnyOfApmParams( '/service-map', '/services/{serviceName}/service-map' ); diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx index 8f66658785b97..a82fa3121bb3b 100644 --- a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx @@ -12,19 +12,29 @@ import { EuiSpacer, EuiText, EuiCodeBlock, + EuiTabbedContent, + EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { CreateAgentInstructions } from './agent_instructions_mappings'; +import React, { ComponentType } from 'react'; +import styled from 'styled-components'; +import { + AgentRuntimeAttachmentProps, + CreateAgentInstructions, +} from './agent_instructions_mappings'; import { Markdown, useKibana, } from '../../../../../../../src/plugins/kibana_react/public'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { AgentIcon } from '../../shared/agent_icon'; -import { NewPackagePolicy } from '../apm_policy_form/typings'; +import type { + NewPackagePolicy, + PackagePolicy, + PackagePolicyEditExtensionComponentProps, +} from '../apm_policy_form/typings'; import { getCommands } from '../../../tutorial/config_agent/commands/get_commands'; -import { replaceTemplateStrings } from './replace_template_strings'; +import { renderMustache } from './render_mustache'; function AccordionButtonContent({ agentName, @@ -97,96 +107,175 @@ function TutorialConfigAgent({ } interface Props { + policy: PackagePolicy; newPolicy: NewPackagePolicy; + onChange: PackagePolicyEditExtensionComponentProps['onChange']; agentName: AgentName; title: string; variantId: string; createAgentInstructions: CreateAgentInstructions; + AgentRuntimeAttachment?: ComponentType; } +const StyledEuiAccordion = styled(EuiAccordion)` + // This is an alternative fix suggested by the EUI team to fix drag elements inside EuiAccordion + // This Issue tracks the fix on the Eui side https://github.com/elastic/eui/issues/3548#issuecomment-639041283 + .euiAccordion__childWrapper { + transform: none; + } +`; + export function AgentInstructionsAccordion({ + policy, newPolicy, + onChange, agentName, title, createAgentInstructions, variantId, + AgentRuntimeAttachment, }: Props) { const docLinks = useKibana().services.docLinks; const vars = newPolicy?.inputs?.[0]?.vars; const apmServerUrl = vars?.url.value; const secretToken = vars?.secret_token.value; const steps = createAgentInstructions(apmServerUrl, secretToken); + const stepsElements = steps.map( + ( + { title: stepTitle, textPre, textPost, customComponentName, commands }, + index + ) => { + const commandBlock = commands + ? renderMustache({ + text: commands, + docLinks, + }) + : ''; + + return ( +
+ +

{stepTitle}

+
+ + + {textPre && ( + + )} + {commandBlock && ( + <> + + + {commandBlock} + + + )} + {customComponentName === 'TutorialConfigAgent' && ( + + )} + {customComponentName === 'TutorialConfigAgentRumScript' && ( + + )} + {textPost && ( + <> + + + + )} + + +
+ ); + } + ); + + const manualInstrumentationContent = ( + <> + + {stepsElements} + + ); + return ( - } > - - {steps.map( - ( - { - title: stepTitle, - textPre, - textPost, - customComponentName, - commands, - }, - index - ) => { - const commandBlock = replaceTemplateStrings( - Array.isArray(commands) ? commands.join('\n') : commands || '', - docLinks - ); - return ( -
- -

{stepTitle}

-
- - - {textPre && ( - - )} - {commandBlock && ( - <> - - - {commandBlock} - - - )} - {customComponentName === 'TutorialConfigAgent' && ( - - )} - {customComponentName === 'TutorialConfigAgentRumScript' && ( - - )} - {textPost && ( + {AgentRuntimeAttachment ? ( + <> + + + + {i18n.translate( + 'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.autoAttachment', + { defaultMessage: 'Auto-Attachment' } + )} + + + + + + ), + content: ( <> - - )} - - -
- ); - } + ), + }, + ]} + /> + + ) : ( + manualInstrumentationContent )} -
+ ); } diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_mappings.ts b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_mappings.ts index 8bfdafe61d44e..5e992094ac64c 100644 --- a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_mappings.ts +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_mappings.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ComponentType } from 'react'; import { createDotNetAgentInstructions, createDjangoAgentInstructions, @@ -18,6 +19,18 @@ import { createRackAgentInstructions, } from '../../../../common/tutorial/instructions/apm_agent_instructions'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { JavaRuntimeAttachment } from './runtime_attachment/supported_agents/java_runtime_attachment'; +import { + NewPackagePolicy, + PackagePolicy, + PackagePolicyEditExtensionComponentProps, +} from '../apm_policy_form/typings'; + +export interface AgentRuntimeAttachmentProps { + policy: PackagePolicy; + newPolicy: NewPackagePolicy; + onChange: PackagePolicyEditExtensionComponentProps['onChange']; +} export type CreateAgentInstructions = ( apmServerUrl?: string, @@ -35,12 +48,14 @@ export const ApmAgentInstructionsMappings: Array<{ title: string; variantId: string; createAgentInstructions: CreateAgentInstructions; + AgentRuntimeAttachment?: ComponentType; }> = [ { agentName: 'java', title: 'Java', variantId: 'java', createAgentInstructions: createJavaAgentInstructions, + AgentRuntimeAttachment: JavaRuntimeAttachment, }, { agentName: 'rum-js', diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/index.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/index.tsx index d6a43a1e1268a..09b638fb184df 100644 --- a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/index.tsx +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/index.tsx @@ -21,19 +21,28 @@ interface Props { onChange: PackagePolicyEditExtensionComponentProps['onChange']; } -export function ApmAgents({ newPolicy }: Props) { +export function ApmAgents({ policy, newPolicy, onChange }: Props) { return (
{ApmAgentInstructionsMappings.map( - ({ agentName, title, createAgentInstructions, variantId }) => ( + ({ + agentName, + title, + createAgentInstructions, + variantId, + AgentRuntimeAttachment, + }) => ( diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/replace_template_strings.ts b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/render_mustache.ts similarity index 65% rename from x-pack/plugins/apm/public/components/fleet_integration/apm_agents/replace_template_strings.ts rename to x-pack/plugins/apm/public/components/fleet_integration/apm_agents/render_mustache.ts index d36d76d466308..ebf5fea7f2b85 100644 --- a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/replace_template_strings.ts +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/render_mustache.ts @@ -10,12 +10,17 @@ import Mustache from 'mustache'; const TEMPLATE_TAGS = ['{', '}']; -export function replaceTemplateStrings( - text: string, - docLinks?: CoreStart['docLinks'] -) { - Mustache.parse(text, TEMPLATE_TAGS); - return Mustache.render(text, { +export function renderMustache({ + text, + docLinks, +}: { + text: string | string[]; + docLinks?: CoreStart['docLinks']; +}) { + const template = Array.isArray(text) ? text.join('\n') : text; + + Mustache.parse(template, TEMPLATE_TAGS); + return Mustache.render(template, { config: { docs: { base_url: docLinks?.ELASTIC_WEBSITE_URL, diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/default_discovery_rule.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/default_discovery_rule.tsx new file mode 100644 index 0000000000000..848582bb3feb6 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/default_discovery_rule.tsx @@ -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 { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiBadge, +} from '@elastic/eui'; +import React from 'react'; + +export function DefaultDiscoveryRule() { + return ( + + + + Exclude + + + Everything else + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/discovery_rule.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/discovery_rule.tsx new file mode 100644 index 0000000000000..f7b1b3db3a4c4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/discovery_rule.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiBadge, + EuiPanel, + DraggableProvidedDragHandleProps, + EuiButtonIcon, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { Operation } from '.'; + +interface Props { + id: string; + order: number; + operation: string; + type: string; + probe: string; + providedDragHandleProps?: DraggableProvidedDragHandleProps; + onDelete: (discoveryItemId: string) => void; + onEdit: (discoveryItemId: string) => void; + operationTypes: Operation[]; +} + +export function DiscoveryRule({ + id, + order, + operation, + type, + probe, + providedDragHandleProps, + onDelete, + onEdit, + operationTypes, +}: Props) { + const operationTypesLabels = useMemo(() => { + return operationTypes.reduce<{ + [operationValue: string]: { + label: string; + types: { [typeValue: string]: string }; + }; + }>((acc, current) => { + return { + ...acc, + [current.operation.value]: { + label: current.operation.label, + types: current.types.reduce((memo, { value, label }) => { + return { ...memo, [value]: label }; + }, {}), + }, + }; + }, {}); + }, [operationTypes]); + return ( + + + +
+ +
+
+ + + + {order} + + + + {operationTypesLabels[operation].label} + + + + + + +

{operationTypesLabels[operation].types[type]}

+
+
+ + {probe} + +
+
+ + + + { + onEdit(id); + }} + /> + + + { + onDelete(id); + }} + /> + + + +
+
+
+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/edit_discovery_rule.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/edit_discovery_rule.tsx new file mode 100644 index 0000000000000..5059bbabfce91 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/edit_discovery_rule.tsx @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiButton, + EuiButtonEmpty, + EuiFormFieldset, + EuiSelect, + EuiFieldText, + EuiFormRow, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { + Operation, + DISCOVERY_RULE_TYPE_ALL, + STAGED_DISCOVERY_RULE_ID, +} from '.'; + +interface Props { + id: string; + onChangeOperation: (discoveryItemId: string) => void; + operation: string; + onChangeType: (discoveryItemId: string) => void; + type: string; + onChangeProbe: (discoveryItemId: string) => void; + probe: string; + onCancel: () => void; + onSubmit: () => void; + operationTypes: Operation[]; +} + +export function EditDiscoveryRule({ + id, + onChangeOperation, + operation, + onChangeType, + type, + onChangeProbe, + probe, + onCancel, + onSubmit, + operationTypes, +}: Props) { + return ( + + + + + ({ + text: item.operation.label, + value: item.operation.value, + }))} + value={operation} + onChange={(e) => { + onChangeOperation(e.target.value); + }} + /> + + + + + + + + + definedOperation.value === operation + ) + ?.types.map((item) => ({ + inputDisplay: item.label, + value: item.value, + dropdownDisplay: ( + <> + {item.label} + +

{item.description}

+
+ + ), + })) ?? [] + } + valueOfSelected={type} + onChange={onChangeType} + /> +
+
+
+
+ {type !== DISCOVERY_RULE_TYPE_ALL && ( + + + + + onChangeProbe(e.target.value)} + /> + + + + + )} + + + Cancel + + + + {id === STAGED_DISCOVERY_RULE_ID + ? i18n.translate( + 'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.editRule.add', + { defaultMessage: 'Add' } + ) + : i18n.translate( + 'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.editRule.save', + { defaultMessage: 'Save' } + )} + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/index.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/index.tsx new file mode 100644 index 0000000000000..8f2a1d3d1dea1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/index.tsx @@ -0,0 +1,327 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { + htmlIdGenerator, + euiDragDropReorder, + DropResult, + EuiComboBoxOptionOption, +} from '@elastic/eui'; +import React, { useState, useCallback, ReactNode } from 'react'; +import { RuntimeAttachment as RuntimeAttachmentStateless } from './runtime_attachment'; + +export const STAGED_DISCOVERY_RULE_ID = 'STAGED_DISCOVERY_RULE_ID'; +export const DISCOVERY_RULE_TYPE_ALL = 'all'; + +export interface IDiscoveryRule { + operation: string; + type: string; + probe: string; +} + +export type IDiscoveryRuleList = Array<{ + id: string; + discoveryRule: IDiscoveryRule; +}>; + +export interface RuntimeAttachmentSettings { + enabled: boolean; + discoveryRules: IDiscoveryRule[]; + version: string | null; +} + +interface Props { + onChange?: (runtimeAttachmentSettings: RuntimeAttachmentSettings) => void; + toggleDescription: ReactNode; + discoveryRulesDescription: ReactNode; + showUnsavedWarning?: boolean; + initialIsEnabled?: boolean; + initialDiscoveryRules?: IDiscoveryRule[]; + operationTypes: Operation[]; + selectedVersion: string; + versions: string[]; +} + +interface Option { + value: string; + label: string; + description?: string; +} + +export interface Operation { + operation: Option; + types: Option[]; +} + +const versionRegex = new RegExp(/^\d+\.\d+\.\d+$/); +function validateVersion(version: string) { + return versionRegex.test(version); +} + +export function RuntimeAttachment(props: Props) { + const { initialDiscoveryRules = [], onChange = () => {} } = props; + const [isEnabled, setIsEnabled] = useState(Boolean(props.initialIsEnabled)); + const [discoveryRuleList, setDiscoveryRuleList] = + useState( + initialDiscoveryRules.map((discoveryRule) => ({ + id: generateId(), + discoveryRule, + })) + ); + const [editDiscoveryRuleId, setEditDiscoveryRuleId] = useState( + null + ); + const [version, setVersion] = useState(props.selectedVersion); + const [versions, setVersions] = useState(props.versions); + const [isValidVersion, setIsValidVersion] = useState( + validateVersion(version) + ); + + const onToggleEnable = useCallback(() => { + const nextIsEnabled = !isEnabled; + setIsEnabled(nextIsEnabled); + onChange({ + enabled: nextIsEnabled, + discoveryRules: nextIsEnabled + ? discoveryRuleList.map(({ discoveryRule }) => discoveryRule) + : [], + version: nextIsEnabled ? version : null, + }); + }, [isEnabled, onChange, discoveryRuleList, version]); + + const onDelete = useCallback( + (discoveryRuleId: string) => { + const filteredDiscoveryRuleList = discoveryRuleList.filter( + ({ id }) => id !== discoveryRuleId + ); + setDiscoveryRuleList(filteredDiscoveryRuleList); + onChange({ + enabled: isEnabled, + discoveryRules: filteredDiscoveryRuleList.map( + ({ discoveryRule }) => discoveryRule + ), + version, + }); + }, + [isEnabled, discoveryRuleList, onChange, version] + ); + + const onEdit = useCallback( + (discoveryRuleId: string) => { + const editingDiscoveryRule = discoveryRuleList.find( + ({ id }) => id === discoveryRuleId + ); + if (editingDiscoveryRule) { + const { + discoveryRule: { operation, type, probe }, + } = editingDiscoveryRule; + setStagedOperationText(operation); + setStagedTypeText(type); + setStagedProbeText(probe); + setEditDiscoveryRuleId(discoveryRuleId); + } + }, + [discoveryRuleList] + ); + + const [stagedOperationText, setStagedOperationText] = useState(''); + const [stagedTypeText, setStagedTypeText] = useState(''); + const [stagedProbeText, setStagedProbeText] = useState(''); + + const onChangeOperation = useCallback( + (operationText: string) => { + setStagedOperationText(operationText); + const selectedOperationTypes = props.operationTypes.find( + ({ operation }) => operationText === operation.value + ); + const selectedTypeAvailable = selectedOperationTypes?.types.some( + ({ value }) => stagedTypeText === value + ); + if (!selectedTypeAvailable) { + setStagedTypeText(selectedOperationTypes?.types[0].value ?? ''); + } + }, + [props.operationTypes, stagedTypeText] + ); + + const onChangeType = useCallback((operationText: string) => { + setStagedTypeText(operationText); + if (operationText === DISCOVERY_RULE_TYPE_ALL) { + setStagedProbeText(''); + } + }, []); + + const onChangeProbe = useCallback((operationText: string) => { + setStagedProbeText(operationText); + }, []); + + const onCancel = useCallback(() => { + if (editDiscoveryRuleId === STAGED_DISCOVERY_RULE_ID) { + onDelete(STAGED_DISCOVERY_RULE_ID); + } + setEditDiscoveryRuleId(null); + }, [editDiscoveryRuleId, onDelete]); + + const onSubmit = useCallback(() => { + const editDiscoveryRuleIndex = discoveryRuleList.findIndex( + ({ id }) => id === editDiscoveryRuleId + ); + const editDiscoveryRule = discoveryRuleList[editDiscoveryRuleIndex]; + const nextDiscoveryRuleList = [ + ...discoveryRuleList.slice(0, editDiscoveryRuleIndex), + { + id: + editDiscoveryRule.id === STAGED_DISCOVERY_RULE_ID + ? generateId() + : editDiscoveryRule.id, + discoveryRule: { + operation: stagedOperationText, + type: stagedTypeText, + probe: stagedProbeText, + }, + }, + ...discoveryRuleList.slice(editDiscoveryRuleIndex + 1), + ]; + setDiscoveryRuleList(nextDiscoveryRuleList); + setEditDiscoveryRuleId(null); + onChange({ + enabled: isEnabled, + discoveryRules: nextDiscoveryRuleList.map( + ({ discoveryRule }) => discoveryRule + ), + version, + }); + }, [ + isEnabled, + editDiscoveryRuleId, + stagedOperationText, + stagedTypeText, + stagedProbeText, + discoveryRuleList, + onChange, + version, + ]); + + const onAddRule = useCallback(() => { + const firstOperationType = props.operationTypes[0]; + const operationText = firstOperationType.operation.value; + const typeText = firstOperationType.types[0].value; + const valueText = ''; + setStagedOperationText(operationText); + setStagedTypeText(typeText); + setStagedProbeText(valueText); + const nextDiscoveryRuleList = [ + { + id: STAGED_DISCOVERY_RULE_ID, + discoveryRule: { + operation: operationText, + type: typeText, + probe: valueText, + }, + }, + ...discoveryRuleList, + ]; + setDiscoveryRuleList(nextDiscoveryRuleList); + setEditDiscoveryRuleId(STAGED_DISCOVERY_RULE_ID); + }, [discoveryRuleList, props.operationTypes]); + + const onDragEnd = useCallback( + ({ source, destination }: DropResult) => { + if (source && destination) { + const nextDiscoveryRuleList = euiDragDropReorder( + discoveryRuleList, + source.index, + destination.index + ); + setDiscoveryRuleList(nextDiscoveryRuleList); + onChange({ + enabled: isEnabled, + discoveryRules: nextDiscoveryRuleList.map( + ({ discoveryRule }) => discoveryRule + ), + version, + }); + } + }, + [isEnabled, discoveryRuleList, onChange, version] + ); + + function onChangeVersion(nextVersion?: string) { + if (!nextVersion) { + return; + } + setVersion(nextVersion); + onChange({ + enabled: isEnabled, + discoveryRules: isEnabled + ? discoveryRuleList.map(({ discoveryRule }) => discoveryRule) + : [], + version: nextVersion, + }); + } + + function onCreateNewVersion( + newVersion: string, + flattenedOptions: Array> + ) { + const normalizedNewVersion = newVersion.trim().toLowerCase(); + const isNextVersionValid = validateVersion(normalizedNewVersion); + setIsValidVersion(isNextVersionValid); + if (!normalizedNewVersion || !isNextVersionValid) { + return; + } + + // Create the option if it doesn't exist. + if ( + flattenedOptions.findIndex( + (option) => option.label.trim().toLowerCase() === normalizedNewVersion + ) === -1 + ) { + setVersions([...versions, newVersion]); + } + + onChangeVersion(newVersion); + } + + return ( + { + const nextVersion: string | undefined = selectedVersions[0]?.label; + const isNextVersionValid = validateVersion(nextVersion); + setIsValidVersion(isNextVersionValid); + onChangeVersion(nextVersion); + }} + onCreateNewVersion={onCreateNewVersion} + isValidVersion={isValidVersion} + /> + ); +} + +const generateId = htmlIdGenerator(); diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/runtime_attachment.stories.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/runtime_attachment.stories.tsx new file mode 100644 index 0000000000000..12f6705284ff9 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/runtime_attachment.stories.tsx @@ -0,0 +1,484 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Meta, Story } from '@storybook/react'; +import React, { useState } from 'react'; +import { RuntimeAttachment } from '.'; +import { JavaRuntimeAttachment } from './supported_agents/java_runtime_attachment'; + +const stories: Meta<{}> = { + title: 'fleet/Runtime agent attachment', + component: RuntimeAttachment, + decorators: [ + (StoryComponent) => { + return ( +
+ +
+ ); + }, + ], +}; +export default stories; + +const excludeOptions = [ + { value: 'main', label: 'main class / jar name' }, + { value: 'vmarg', label: 'vmarg' }, + { value: 'user', label: 'user' }, +]; +const includeOptions = [{ value: 'all', label: 'All' }, ...excludeOptions]; + +const versions = ['1.27.1', '1.27.0', '1.26.0', '1.25.0']; + +export const RuntimeAttachmentExample: Story = () => { + const [runtimeAttachmentSettings, setRuntimeAttachmentSettings] = useState( + {} + ); + return ( + <> + { + setRuntimeAttachmentSettings(settings); + }} + toggleDescription="Attach the Java agent to running and starting Java applications." + discoveryRulesDescription="For every running JVM, the discovery rules are evaluated in the order they are provided. The first matching rule determines the outcome. Learn more in the docs" + showUnsavedWarning={true} + initialIsEnabled={true} + initialDiscoveryRules={[ + { + operation: 'include', + type: 'main', + probe: 'java-opbeans-10010', + }, + { + operation: 'exclude', + type: 'vmarg', + probe: '10948653898867', + }, + ]} + versions={versions} + selectedVersion={versions[0]} + /> +
+
{JSON.stringify(runtimeAttachmentSettings, null, 4)}
+ + ); +}; + +export const JavaRuntimeAttachmentExample: Story = () => { + return ( + {}} + /> + ); +}; + +const policy = { + id: 'cc380ec5-d84e-40e1-885a-d706edbdc968', + version: 'WzM0MzA2LDJd', + name: 'apm-1', + description: '', + namespace: 'default', + policy_id: 'policy-elastic-agent-on-cloud', + enabled: true, + output_id: '', + inputs: [ + { + type: 'apm', + policy_template: 'apmserver', + enabled: true, + streams: [], + vars: { + host: { + value: 'localhost:8200', + type: 'text', + }, + url: { + value: 'http://localhost:8200', + type: 'text', + }, + secret_token: { + type: 'text', + }, + api_key_enabled: { + value: false, + type: 'bool', + }, + enable_rum: { + value: true, + type: 'bool', + }, + anonymous_enabled: { + value: true, + type: 'bool', + }, + anonymous_allow_agent: { + value: ['rum-js', 'js-base', 'iOS/swift'], + type: 'text', + }, + anonymous_allow_service: { + value: [], + type: 'text', + }, + anonymous_rate_limit_event_limit: { + value: 10, + type: 'integer', + }, + anonymous_rate_limit_ip_limit: { + value: 10000, + type: 'integer', + }, + default_service_environment: { + type: 'text', + }, + rum_allow_origins: { + value: ['"*"'], + type: 'text', + }, + rum_allow_headers: { + value: [], + type: 'text', + }, + rum_response_headers: { + type: 'yaml', + }, + rum_library_pattern: { + value: '"node_modules|bower_components|~"', + type: 'text', + }, + rum_exclude_from_grouping: { + value: '"^/webpack"', + type: 'text', + }, + api_key_limit: { + value: 100, + type: 'integer', + }, + max_event_bytes: { + value: 307200, + type: 'integer', + }, + capture_personal_data: { + value: true, + type: 'bool', + }, + max_header_bytes: { + value: 1048576, + type: 'integer', + }, + idle_timeout: { + value: '45s', + type: 'text', + }, + read_timeout: { + value: '3600s', + type: 'text', + }, + shutdown_timeout: { + value: '30s', + type: 'text', + }, + write_timeout: { + value: '30s', + type: 'text', + }, + max_connections: { + value: 0, + type: 'integer', + }, + response_headers: { + type: 'yaml', + }, + expvar_enabled: { + value: false, + type: 'bool', + }, + tls_enabled: { + value: false, + type: 'bool', + }, + tls_certificate: { + type: 'text', + }, + tls_key: { + type: 'text', + }, + tls_supported_protocols: { + value: ['TLSv1.0', 'TLSv1.1', 'TLSv1.2'], + type: 'text', + }, + tls_cipher_suites: { + value: [], + type: 'text', + }, + tls_curve_types: { + value: [], + type: 'text', + }, + tail_sampling_policies: { + type: 'yaml', + }, + tail_sampling_interval: { + type: 'text', + }, + }, + config: { + 'apm-server': { + value: { + rum: { + source_mapping: { + metadata: [], + }, + }, + agent_config: [], + }, + }, + }, + compiled_input: { + 'apm-server': { + auth: { + anonymous: { + allow_agent: ['rum-js', 'js-base', 'iOS/swift'], + allow_service: null, + enabled: true, + rate_limit: { + event_limit: 10, + ip_limit: 10000, + }, + }, + api_key: { + enabled: false, + limit: 100, + }, + secret_token: null, + }, + capture_personal_data: true, + idle_timeout: '45s', + default_service_environment: null, + 'expvar.enabled': false, + host: 'localhost:8200', + max_connections: 0, + max_event_size: 307200, + max_header_size: 1048576, + read_timeout: '3600s', + response_headers: null, + rum: { + allow_headers: null, + allow_origins: ['*'], + enabled: true, + exclude_from_grouping: '^/webpack', + library_pattern: 'node_modules|bower_components|~', + response_headers: null, + }, + shutdown_timeout: '30s', + write_timeout: '30s', + }, + }, + }, + ], + package: { + name: 'apm', + title: 'Elastic APM', + version: '7.16.0', + }, + elasticsearch: { + privileges: { + cluster: ['cluster:monitor/main'], + }, + }, + revision: 1, + created_at: '2021-11-18T02:14:55.758Z', + created_by: 'admin', + updated_at: '2021-11-18T02:14:55.758Z', + updated_by: 'admin', +}; + +const newPolicy = { + version: 'WzM0MzA2LDJd', + name: 'apm-1', + description: '', + namespace: 'default', + policy_id: 'policy-elastic-agent-on-cloud', + enabled: true, + output_id: '', + package: { + name: 'apm', + title: 'Elastic APM', + version: '8.0.0-dev2', + }, + elasticsearch: { + privileges: { + cluster: ['cluster:monitor/main'], + }, + }, + inputs: [ + { + type: 'apm', + policy_template: 'apmserver', + enabled: true, + vars: { + host: { + value: 'localhost:8200', + type: 'text', + }, + url: { + value: 'http://localhost:8200', + type: 'text', + }, + secret_token: { + type: 'text', + }, + api_key_enabled: { + value: false, + type: 'bool', + }, + enable_rum: { + value: true, + type: 'bool', + }, + anonymous_enabled: { + value: true, + type: 'bool', + }, + anonymous_allow_agent: { + value: ['rum-js', 'js-base', 'iOS/swift'], + type: 'text', + }, + anonymous_allow_service: { + value: [], + type: 'text', + }, + anonymous_rate_limit_event_limit: { + value: 10, + type: 'integer', + }, + anonymous_rate_limit_ip_limit: { + value: 10000, + type: 'integer', + }, + default_service_environment: { + type: 'text', + }, + rum_allow_origins: { + value: ['"*"'], + type: 'text', + }, + rum_allow_headers: { + value: [], + type: 'text', + }, + rum_response_headers: { + type: 'yaml', + }, + rum_library_pattern: { + value: '"node_modules|bower_components|~"', + type: 'text', + }, + rum_exclude_from_grouping: { + value: '"^/webpack"', + type: 'text', + }, + api_key_limit: { + value: 100, + type: 'integer', + }, + max_event_bytes: { + value: 307200, + type: 'integer', + }, + capture_personal_data: { + value: true, + type: 'bool', + }, + max_header_bytes: { + value: 1048576, + type: 'integer', + }, + idle_timeout: { + value: '45s', + type: 'text', + }, + read_timeout: { + value: '3600s', + type: 'text', + }, + shutdown_timeout: { + value: '30s', + type: 'text', + }, + write_timeout: { + value: '30s', + type: 'text', + }, + max_connections: { + value: 0, + type: 'integer', + }, + response_headers: { + type: 'yaml', + }, + expvar_enabled: { + value: false, + type: 'bool', + }, + tls_enabled: { + value: false, + type: 'bool', + }, + tls_certificate: { + type: 'text', + }, + tls_key: { + type: 'text', + }, + tls_supported_protocols: { + value: ['TLSv1.0', 'TLSv1.1', 'TLSv1.2'], + type: 'text', + }, + tls_cipher_suites: { + value: [], + type: 'text', + }, + tls_curve_types: { + value: [], + type: 'text', + }, + tail_sampling_policies: { + type: 'yaml', + }, + tail_sampling_interval: { + type: 'text', + }, + }, + config: { + 'apm-server': { + value: { + rum: { + source_mapping: { + metadata: [], + }, + }, + agent_config: [], + }, + }, + }, + streams: [], + }, + ], +}; diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/runtime_attachment.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/runtime_attachment.tsx new file mode 100644 index 0000000000000..3592eb4f04745 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/runtime_attachment.tsx @@ -0,0 +1,235 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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, + EuiSpacer, + EuiSwitch, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiDragDropContext, + EuiDroppable, + EuiDraggable, + EuiIcon, + DropResult, + EuiComboBox, + EuiComboBoxProps, + EuiFormRow, +} from '@elastic/eui'; +import React, { ReactNode } from 'react'; +import { i18n } from '@kbn/i18n'; +import { DiscoveryRule } from './discovery_rule'; +import { DefaultDiscoveryRule } from './default_discovery_rule'; +import { EditDiscoveryRule } from './edit_discovery_rule'; +import { IDiscoveryRuleList, Operation } from '.'; + +interface Props { + isEnabled: boolean; + onToggleEnable: () => void; + discoveryRuleList: IDiscoveryRuleList; + setDiscoveryRuleList: (discoveryRuleItems: IDiscoveryRuleList) => void; + onDelete: (discoveryItemId: string) => void; + editDiscoveryRuleId: null | string; + onEdit: (discoveryItemId: string) => void; + onChangeOperation: (operationText: string) => void; + stagedOperationText: string; + onChangeType: (typeText: string) => void; + stagedTypeText: string; + onChangeProbe: (probeText: string) => void; + stagedProbeText: string; + onCancel: () => void; + onSubmit: () => void; + onAddRule: () => void; + operationTypes: Operation[]; + toggleDescription: ReactNode; + discoveryRulesDescription: ReactNode; + showUnsavedWarning?: boolean; + onDragEnd: (dropResult: DropResult) => void; + selectedVersion: string; + versions: string[]; + onChangeVersion: EuiComboBoxProps['onChange']; + onCreateNewVersion: EuiComboBoxProps['onCreateOption']; + isValidVersion: boolean; +} + +export function RuntimeAttachment({ + isEnabled, + onToggleEnable, + discoveryRuleList, + setDiscoveryRuleList, + onDelete, + editDiscoveryRuleId, + onEdit, + onChangeOperation, + stagedOperationText, + onChangeType, + stagedTypeText, + onChangeProbe, + stagedProbeText, + onCancel, + onSubmit, + onAddRule, + operationTypes, + toggleDescription, + discoveryRulesDescription, + showUnsavedWarning, + onDragEnd, + selectedVersion, + versions, + onChangeVersion, + onCreateNewVersion, + isValidVersion, +}: Props) { + return ( +
+ {showUnsavedWarning && ( + <> + + + + )} + + + + + +

{toggleDescription}

+
+
+ {isEnabled && versions && ( + + + ({ label: _version }))} + onChange={onChangeVersion} + onCreateOption={onCreateNewVersion} + singleSelection + isClearable={false} + /> + + + )} +
+ {isEnabled && ( + <> + + +

+ {i18n.translate( + 'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.discoveryRules', + { defaultMessage: 'Discovery rules' } + )} +

+
+ + + + + + + +

{discoveryRulesDescription}

+
+
+ + + {i18n.translate( + 'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.addRule', + { defaultMessage: 'Add rule' } + )} + + +
+ + + + {discoveryRuleList.map(({ discoveryRule, id }, idx) => ( + + {(provided) => + id === editDiscoveryRuleId ? ( + + ) : ( + + ) + } + + ))} + + + + + )} + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/supported_agents/java_runtime_attachment.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/supported_agents/java_runtime_attachment.tsx new file mode 100644 index 0000000000000..2284315d4a6ba --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/supported_agents/java_runtime_attachment.tsx @@ -0,0 +1,276 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import yaml from 'js-yaml'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useCallback, useState, useMemo } from 'react'; +import { + RuntimeAttachment, + RuntimeAttachmentSettings, + IDiscoveryRule, +} from '..'; +import type { + NewPackagePolicy, + PackagePolicy, + PackagePolicyEditExtensionComponentProps, +} from '../../../apm_policy_form/typings'; + +interface Props { + policy: PackagePolicy; + newPolicy: NewPackagePolicy; + onChange: PackagePolicyEditExtensionComponentProps['onChange']; +} + +const excludeOptions = [ + { + value: 'main', + label: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.main', + { defaultMessage: 'main' } + ), + description: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.mainDescription', + { + defaultMessage: + 'A regular expression of fully qualified main class names or paths to JARs of applications the java agent should be attached to. Performs a partial match so that foo matches /bin/foo.jar.', + } + ), + }, + { + value: 'vmarg', + label: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.vmarg', + { defaultMessage: 'vmarg' } + ), + description: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.vmargDescription', + { + defaultMessage: + 'A regular expression matched against the arguments passed to the JVM, such as system properties. Performs a partial match so that attach=true matches the system property -Dattach=true.', + } + ), + }, + { + value: 'user', + label: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.user', + { defaultMessage: 'user' } + ), + description: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.userDescription', + { + defaultMessage: + 'A username that is matched against the operating system user that runs the JVM.', + } + ), + }, +]; +const includeOptions = [ + { + value: 'all', + label: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.include.options.all', + { defaultMessage: 'All' } + ), + description: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.include.options.allDescription', + { defaultMessage: 'Includes all JVMs for attachment.' } + ), + }, + ...excludeOptions, +]; + +const versions = [ + '1.27.1', + '1.27.0', + '1.26.0', + '1.25.0', + '1.24.0', + '1.23.0', + '1.22.0', + '1.21.0', + '1.20.0', + '1.19.0', + '1.18.1', + '1.18.0', + '1.18.0.RC1', + '1.17.0', + '1.16.0', + '1.15.0', + '1.14.0', + '1.13.0', + '1.12.0', + '1.11.0', + '1.10.0', + '1.9.0', + '1.8.0', + '1.7.0', + '1.6.1', + '1.6.0', + '1.5.0', + '1.4.0', + '1.3.0', + '1.2.0', +]; + +function getApmVars(newPolicy: NewPackagePolicy) { + return newPolicy.inputs.find(({ type }) => type === 'apm')?.vars; +} + +export function JavaRuntimeAttachment({ newPolicy, onChange }: Props) { + const [isDirty, setIsDirty] = useState(false); + const onChangePolicy = useCallback( + (runtimeAttachmentSettings: RuntimeAttachmentSettings) => { + const apmInputIdx = newPolicy.inputs.findIndex( + ({ type }) => type === 'apm' + ); + onChange({ + isValid: true, + updatedPolicy: { + ...newPolicy, + inputs: [ + ...newPolicy.inputs.slice(0, apmInputIdx), + { + ...newPolicy.inputs[apmInputIdx], + vars: { + ...newPolicy.inputs[apmInputIdx].vars, + java_attacher_enabled: { + value: runtimeAttachmentSettings.enabled, + type: 'bool', + }, + java_attacher_discovery_rules: { + type: 'yaml', + value: encodeDiscoveryRulesYaml( + runtimeAttachmentSettings.discoveryRules + ), + }, + java_attacher_agent_version: { + type: 'text', + value: runtimeAttachmentSettings.version, + }, + }, + }, + ...newPolicy.inputs.slice(apmInputIdx + 1), + ], + }, + }); + setIsDirty(true); + }, + [newPolicy, onChange] + ); + + const apmVars = useMemo(() => getApmVars(newPolicy), [newPolicy]); + + return ( + + {i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.discoveryRulesDescription.docLink', + { defaultMessage: 'docs' } + )} + + ), + }} + /> + } + showUnsavedWarning={isDirty} + initialIsEnabled={apmVars?.java_attacher_enabled?.value} + initialDiscoveryRules={decodeDiscoveryRulesYaml( + apmVars?.java_attacher_discovery_rules?.value ?? '[]\n', + [initialDiscoveryRule] + )} + selectedVersion={ + apmVars?.java_attacher_agent_version?.value || versions[0] + } + versions={versions} + /> + ); +} + +const initialDiscoveryRule = { + operation: 'include', + type: 'vmarg', + probe: 'elastic.apm.attach=true', +}; + +type DiscoveryRulesParsedYaml = Array<{ [operationType: string]: string }>; + +function decodeDiscoveryRulesYaml( + discoveryRulesYaml: string, + defaultDiscoveryRules: IDiscoveryRule[] = [] +): IDiscoveryRule[] { + try { + const parsedYaml: DiscoveryRulesParsedYaml = + yaml.load(discoveryRulesYaml) ?? []; + + if (parsedYaml.length === 0) { + return defaultDiscoveryRules; + } + + // transform into array of discovery rules + return parsedYaml.map((discoveryRuleMap) => { + const [operationType, probe] = Object.entries(discoveryRuleMap)[0]; + return { + operation: operationType.split('-')[0], + type: operationType.split('-')[1], + probe, + }; + }); + } catch (error) { + return defaultDiscoveryRules; + } +} + +function encodeDiscoveryRulesYaml(discoveryRules: IDiscoveryRule[]): string { + // transform into list of key,value objects for expected yaml result + const mappedDiscoveryRules: DiscoveryRulesParsedYaml = discoveryRules.map( + ({ operation, type, probe }) => ({ + [`${operation}-${type}`]: probe, + }) + ); + return yaml.dump(mappedDiscoveryRules); +} diff --git a/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx index ec8366dfb36b4..229f34f7857ad 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx @@ -18,11 +18,11 @@ import { getLegacyApmHref } from '../../shared/Links/apm/APMLink'; type Tab = NonNullable[0] & { key: | 'agent-configurations' + | 'agent-keys' | 'anomaly-detection' | 'apm-indices' | 'customize-ui' - | 'schema' - | 'agent-keys'; + | 'schema'; hidden?: boolean; }; @@ -76,6 +76,17 @@ function getTabs({ search, }), }, + { + key: 'agent-keys', + label: i18n.translate('xpack.apm.settings.agentKeys', { + defaultMessage: 'Agent Keys', + }), + href: getLegacyApmHref({ + basePath, + path: `/settings/agent-keys`, + search, + }), + }, { key: 'anomaly-detection', label: i18n.translate('xpack.apm.settings.anomalyDetection', { @@ -117,17 +128,6 @@ function getTabs({ }), href: getLegacyApmHref({ basePath, path: `/settings/schema`, search }), }, - { - key: 'agent-keys', - label: i18n.translate('xpack.apm.settings.agentKeys', { - defaultMessage: 'Agent Keys', - }), - href: getLegacyApmHref({ - basePath, - path: `/settings/agent-keys`, - search, - }), - }, ]; return tabs 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/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx index aa6d69c03d8f6..2cb4e0964686f 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -15,7 +15,7 @@ import { useUiTracker } from '../../../../../observability/public'; import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { useApmParams } from '../../../hooks/use_apm_params'; +import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; import { useBreakpoints } from '../../../hooks/use_breakpoints'; import { useTimeRange } from '../../../hooks/use_time_range'; import * as urlHelpers from '../../shared/Links/url_helpers'; @@ -121,8 +121,7 @@ export function TimeComparison() { const { isSmall } = useBreakpoints(); const { query: { rangeFrom, rangeTo }, - // @ts-expect-error Type instantiation is excessively deep and possibly infinite. - } = useApmParams('/services', '/backends/*', '/services/{serviceName}'); + } = useAnyOfApmParams('/services', '/backends/*', '/services/{serviceName}'); const { exactStart, exactEnd } = useTimeRange({ rangeFrom, 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_apm_params.ts b/x-pack/plugins/apm/public/hooks/use_apm_params.ts index 12b79ec7c90ae..b4c17c1b329ae 100644 --- a/x-pack/plugins/apm/public/hooks/use_apm_params.ts +++ b/x-pack/plugins/apm/public/hooks/use_apm_params.ts @@ -4,42 +4,29 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { ValuesType } from 'utility-types'; import { TypeOf, PathsOf, useParams } from '@kbn/typed-react-router-config'; import { ApmRoutes } from '../components/routing/apm_route_config'; -export function useApmParams>( +// these three different functions exist purely to speed up completions from +// TypeScript. One overloaded function is expensive because of the size of the +// union type that is created. + +export function useMaybeApmParams>( path: TPath, optional: true -): TypeOf | undefined; +): TypeOf | undefined { + return useParams(path, optional); +} export function useApmParams>( path: TPath -): TypeOf; - -export function useApmParams< - TPath1 extends PathsOf, - TPath2 extends PathsOf ->( - path1: TPath1, - path2: TPath2 -): TypeOf | TypeOf; - -export function useApmParams< - TPath1 extends PathsOf, - TPath2 extends PathsOf, - TPath3 extends PathsOf ->( - path1: TPath1, - path2: TPath2, - path3: TPath3 -): - | TypeOf - | TypeOf - | TypeOf; +): TypeOf { + return useParams(path)!; +} -export function useApmParams( - ...args: any[] -): TypeOf> | undefined { - return useParams(...args); +export function useAnyOfApmParams>>( + ...paths: TPaths +): TypeOf> { + return useParams(...paths)!; } diff --git a/x-pack/plugins/apm/public/hooks/use_apm_router.ts b/x-pack/plugins/apm/public/hooks/use_apm_router.ts index d10b6da857802..dea66d7b2e1c8 100644 --- a/x-pack/plugins/apm/public/hooks/use_apm_router.ts +++ b/x-pack/plugins/apm/public/hooks/use_apm_router.ts @@ -14,6 +14,8 @@ export function useApmRouter() { const { core } = useApmPluginContext(); const link = (...args: [any]) => { + // @ts-expect-error router.link() expects never type, because + // no routes are specified. that's okay. return core.http.basePath.prepend('/app/apm' + router.link(...args)); }; 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/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 3a439df245609..d62cca4e07d45 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -54,6 +54,8 @@ import { getLazyApmAgentsTabExtension } from './components/fleet_integration/laz import { getLazyAPMPolicyCreateExtension } from './components/fleet_integration/lazy_apm_policy_create_extension'; import { getLazyAPMPolicyEditExtension } from './components/fleet_integration/lazy_apm_policy_edit_extension'; import { featureCatalogueEntry } from './featureCatalogueEntry'; +import type { SecurityPluginStart } from '../../security/public'; + export type ApmPluginSetup = ReturnType; export type ApmPluginStart = void; @@ -81,6 +83,7 @@ export interface ApmPluginStartDeps { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; observability: ObservabilityPublicStart; fleet?: FleetStart; + security?: SecurityPluginStart; } const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', { diff --git a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts index 11deff82de572..6b00c5cdd9a2b 100644 --- a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts +++ b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { kibanaPackageJson } from '@kbn/dev-utils'; +import { kibanaPackageJson } from '@kbn/utils'; import { GetDeprecationsContext } from '../../../../../src/core/server'; import { CloudSetup } from '../../../cloud/server'; 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/index.ts b/x-pack/plugins/apm/server/index.ts index 416a873bac0a9..958bfb672083a 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -17,6 +17,7 @@ import { APMPlugin } from './plugin'; // All options should be documented in the APM configuration settings: https://github.com/elastic/kibana/blob/main/docs/settings/apm-settings.asciidoc // and be included on cloud allow list unless there are specific reasons not to const configSchema = schema.object({ + autoCreateApmDataView: schema.boolean({ defaultValue: true }), serviceMapEnabled: schema.boolean({ defaultValue: true }), serviceMapFingerprintBucketSize: schema.number({ defaultValue: 100 }), serviceMapTraceIdBucketSize: schema.number({ defaultValue: 65 }), @@ -25,7 +26,6 @@ const configSchema = schema.object({ }), serviceMapTraceIdGlobalBucketSize: schema.number({ defaultValue: 6 }), serviceMapMaxTracesPerRequest: schema.number({ defaultValue: 50 }), - autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), ui: schema.object({ enabled: schema.boolean({ defaultValue: true }), transactionGroupBucketSize: schema.number({ defaultValue: 1000 }), @@ -59,7 +59,15 @@ const configSchema = schema.object({ // plugin config export const config: PluginConfigDescriptor = { - deprecations: ({ renameFromRoot, deprecateFromRoot, unusedFromRoot }) => [ + deprecations: ({ + rename, + renameFromRoot, + deprecateFromRoot, + unusedFromRoot, + }) => [ + rename('autocreateApmIndexPattern', 'autoCreateApmDataView', { + level: 'warning', + }), renameFromRoot( 'apm_oss.transactionIndices', 'xpack.apm.indices.transaction', 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/agent_keys/create_agent_key.ts b/x-pack/plugins/apm/server/routes/agent_keys/create_agent_key.ts new file mode 100644 index 0000000000000..02207dad32efb --- /dev/null +++ b/x-pack/plugins/apm/server/routes/agent_keys/create_agent_key.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { ApmPluginRequestHandlerContext } from '../typings'; +import { CreateApiKeyResponse } from '../../../common/agent_key_types'; + +const enum PrivilegeType { + SOURCEMAP = 'sourcemap:write', + EVENT = 'event:write', + AGENT_CONFIG = 'config_agent:read', +} + +interface SecurityHasPrivilegesResponse { + application: { + apm: { + '-': { + [PrivilegeType.SOURCEMAP]: boolean; + [PrivilegeType.EVENT]: boolean; + [PrivilegeType.AGENT_CONFIG]: boolean; + }; + }; + }; + has_all_requested: boolean; + username: string; +} + +export async function createAgentKey({ + context, + requestBody, +}: { + context: ApmPluginRequestHandlerContext; + requestBody: { + name: string; + sourcemap?: boolean; + event?: boolean; + agentConfig?: boolean; + }; +}) { + // Elasticsearch will allow a user without the right apm privileges to create API keys, but the keys won't validate + // check first whether the user has the right privileges, and bail out early if not + const { + body: { application, username, has_all_requested: hasRequiredPrivileges }, + } = await context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges( + { + body: { + application: [ + { + application: 'apm', + privileges: [ + PrivilegeType.SOURCEMAP, + PrivilegeType.EVENT, + PrivilegeType.AGENT_CONFIG, + ], + resources: ['-'], + }, + ], + }, + } + ); + + if (!hasRequiredPrivileges) { + const missingPrivileges = Object.entries(application.apm['-']) + .filter((x) => !x[1]) + .map((x) => x[0]) + .join(', '); + const error = `${username} is missing the following requested privilege(s): ${missingPrivileges}.\ + You might try with the superuser, or add the APM application privileges to the role of the authenticated user, eg.: + PUT /_security/role/my_role { + ... + "applications": [{ + "application": "apm", + "privileges": ["sourcemap:write", "event:write", "config_agent:read"], + "resources": ["*"] + }], + ... + }`; + throw Boom.internal(error); + } + + const { name = 'apm-key', sourcemap, event, agentConfig } = requestBody; + + const privileges: PrivilegeType[] = []; + if (!sourcemap && !event && !agentConfig) { + privileges.push( + PrivilegeType.SOURCEMAP, + PrivilegeType.EVENT, + PrivilegeType.AGENT_CONFIG + ); + } + + if (sourcemap) { + privileges.push(PrivilegeType.SOURCEMAP); + } + + if (event) { + privileges.push(PrivilegeType.EVENT); + } + + if (agentConfig) { + privileges.push(PrivilegeType.AGENT_CONFIG); + } + + const body = { + name, + metadata: { + application: 'apm', + }, + role_descriptors: { + apm: { + cluster: [], + index: [], + applications: [ + { + application: 'apm', + privileges, + resources: ['*'], + }, + ], + }, + }, + }; + + const { body: agentKey } = + await context.core.elasticsearch.client.asCurrentUser.security.createApiKey( + { + body, + } + ); + + return { + agentKey, + }; +} diff --git a/x-pack/plugins/apm/server/routes/agent_keys/route.ts b/x-pack/plugins/apm/server/routes/agent_keys/route.ts index e5f40205b2912..44bbb22e703b5 100644 --- a/x-pack/plugins/apm/server/routes/agent_keys/route.ts +++ b/x-pack/plugins/apm/server/routes/agent_keys/route.ts @@ -8,11 +8,13 @@ import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; import * as t from 'io-ts'; +import { toBooleanRt } from '@kbn/io-ts-utils/to_boolean_rt'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; import { getAgentKeys } from './get_agent_keys'; import { getAgentKeysPrivileges } from './get_agent_keys_privileges'; import { invalidateAgentKey } from './invalidate_agent_key'; +import { createAgentKey } from './create_agent_key'; const agentKeysRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/agent_keys', @@ -74,10 +76,40 @@ const invalidateAgentKeyRoute = createApmServerRoute({ }, }); +const createAgentKeyRoute = createApmServerRoute({ + endpoint: 'POST /apm/agent_keys', + options: { tags: ['access:apm', 'access:apm_write'] }, + params: t.type({ + body: t.intersection([ + t.partial({ + sourcemap: toBooleanRt, + event: toBooleanRt, + agentConfig: toBooleanRt, + }), + t.type({ + name: t.string, + }), + ]), + }), + handler: async (resources) => { + const { context, params } = resources; + + const { body: requestBody } = params; + + const agentKey = await createAgentKey({ + context, + requestBody, + }); + + return agentKey; + }, +}); + export const agentKeysRouteRepository = createApmServerRouteRepository() .add(agentKeysRoute) .add(agentKeysPrivilegesRoute) - .add(invalidateAgentKeyRoute); + .add(invalidateAgentKeyRoute) + .add(createAgentKeyRoute); const SECURITY_REQUIRED_MESSAGE = i18n.translate( 'xpack.apm.api.apiKeys.securityRequired', 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/correlations/queries/field_stats/get_boolean_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts index c936e626a5599..a41e3370c1063 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts @@ -7,8 +7,6 @@ import { ElasticsearchClient } from 'kibana/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import { buildSamplerAggregation } from '../../utils/field_stats_utils'; import { FieldValuePair } from '../../../../../common/correlations/types'; import { FieldStatsCommonRequestParams, @@ -25,7 +23,7 @@ export const getBooleanFieldStatsRequest = ( ): estypes.SearchRequest => { const query = getQueryWithParams({ params, termFilters }); - const { index, samplerShardSize } = params; + const { index } = params; const size = 0; const aggs: Aggs = { @@ -42,14 +40,13 @@ export const getBooleanFieldStatsRequest = ( const searchBody = { query, - aggs: { - sample: buildSamplerAggregation(aggs, samplerShardSize), - }, + aggs, }; return { index, size, + track_total_hits: false, body: searchBody, }; }; @@ -67,19 +64,17 @@ export const fetchBooleanFieldStats = async ( ); const { body } = await esClient.search(request); const aggregations = body.aggregations as { - sample: { - sampled_value_count: estypes.AggregationsFiltersBucketItemKeys; - sampled_values: estypes.AggregationsTermsAggregate; - }; + sampled_value_count: estypes.AggregationsFiltersBucketItemKeys; + sampled_values: estypes.AggregationsTermsAggregate; }; const stats: BooleanFieldStats = { fieldName: field.fieldName, - count: aggregations?.sample.sampled_value_count.doc_count ?? 0, + count: aggregations?.sampled_value_count.doc_count ?? 0, }; const valueBuckets: TopValueBucket[] = - aggregations?.sample.sampled_values?.buckets ?? []; + aggregations?.sampled_values?.buckets ?? []; valueBuckets.forEach((bucket) => { stats[`${bucket.key.toString()}Count`] = bucket.doc_count; }); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts index 2775d755c9907..30bebc4c24774 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts @@ -20,7 +20,6 @@ const params = { includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', - samplerShardSize: 5000, }; export const getExpectedQuery = (aggs: any) => { @@ -46,6 +45,7 @@ export const getExpectedQuery = (aggs: any) => { }, index: 'apm-*', size: 0, + track_total_hits: false, }; }; @@ -55,28 +55,16 @@ describe('field_stats', () => { const req = getNumericFieldStatsRequest(params, 'url.path'); const expectedAggs = { - sample: { - aggs: { - sampled_field_stats: { - aggs: { actual_stats: { stats: { field: 'url.path' } } }, - filter: { exists: { field: 'url.path' } }, - }, - sampled_percentiles: { - percentiles: { - field: 'url.path', - keyed: false, - percents: [50], - }, - }, - sampled_top: { - terms: { - field: 'url.path', - order: { _count: 'desc' }, - size: 10, - }, - }, + sampled_field_stats: { + aggs: { actual_stats: { stats: { field: 'url.path' } } }, + filter: { exists: { field: 'url.path' } }, + }, + sampled_top: { + terms: { + field: 'url.path', + order: { _count: 'desc' }, + size: 10, }, - sampler: { shard_size: 5000 }, }, }; expect(req).toEqual(getExpectedQuery(expectedAggs)); @@ -87,13 +75,8 @@ describe('field_stats', () => { const req = getKeywordFieldStatsRequest(params, 'url.path'); const expectedAggs = { - sample: { - sampler: { shard_size: 5000 }, - aggs: { - sampled_top: { - terms: { field: 'url.path', size: 10, order: { _count: 'desc' } }, - }, - }, + sampled_top: { + terms: { field: 'url.path', size: 10 }, }, }; expect(req).toEqual(getExpectedQuery(expectedAggs)); @@ -104,15 +87,10 @@ describe('field_stats', () => { const req = getBooleanFieldStatsRequest(params, 'url.path'); const expectedAggs = { - sample: { - sampler: { shard_size: 5000 }, - aggs: { - sampled_value_count: { - filter: { exists: { field: 'url.path' } }, - }, - sampled_values: { terms: { field: 'url.path', size: 2 } }, - }, + sampled_value_count: { + filter: { exists: { field: 'url.path' } }, }, + sampled_values: { terms: { field: 'url.path', size: 2 } }, }; expect(req).toEqual(getExpectedQuery(expectedAggs)); }); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts new file mode 100644 index 0000000000000..0fa508eff508c --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from 'kibana/server'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { FieldValuePair } from '../../../../../common/correlations/types'; +import { + FieldStatsCommonRequestParams, + FieldValueFieldStats, + Aggs, + TopValueBucket, +} from '../../../../../common/correlations/field_stats_types'; +import { getQueryWithParams } from '../get_query_with_params'; + +export const getFieldValueFieldStatsRequest = ( + params: FieldStatsCommonRequestParams, + field?: FieldValuePair +): estypes.SearchRequest => { + const query = getQueryWithParams({ params }); + + const { index } = params; + + const size = 0; + const aggs: Aggs = { + filtered_count: { + filter: { + term: { + [`${field?.fieldName}`]: field?.fieldValue, + }, + }, + }, + }; + + const searchBody = { + query, + aggs, + }; + + return { + index, + size, + track_total_hits: false, + body: searchBody, + }; +}; + +export const fetchFieldValueFieldStats = async ( + esClient: ElasticsearchClient, + params: FieldStatsCommonRequestParams, + field: FieldValuePair +): Promise => { + const request = getFieldValueFieldStatsRequest(params, field); + + const { body } = await esClient.search(request); + const aggregations = body.aggregations as { + filtered_count: estypes.AggregationsFiltersBucketItemKeys; + }; + const topValues: TopValueBucket[] = [ + { + key: field.fieldValue, + doc_count: aggregations.filtered_count.doc_count, + }, + ]; + + const stats = { + fieldName: field.fieldName, + topValues, + topValuesSampleSize: aggregations.filtered_count.doc_count ?? 0, + }; + + return stats; +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts index 8b41f7662679c..513252ee65e11 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts @@ -8,10 +8,7 @@ import { ElasticsearchClient } from 'kibana/server'; import { chunk } from 'lodash'; import { ES_FIELD_TYPES } from '@kbn/field-types'; -import { - FieldValuePair, - CorrelationsParams, -} from '../../../../../common/correlations/types'; +import { FieldValuePair } from '../../../../../common/correlations/types'; import { FieldStats, FieldStatsCommonRequestParams, @@ -23,7 +20,7 @@ import { fetchBooleanFieldStats } from './get_boolean_field_stats'; export const fetchFieldsStats = async ( esClient: ElasticsearchClient, - params: CorrelationsParams, + fieldStatsParams: FieldStatsCommonRequestParams, fieldsToSample: string[], termFilters?: FieldValuePair[] ): Promise<{ stats: FieldStats[]; errors: any[] }> => { @@ -33,14 +30,10 @@ export const fetchFieldsStats = async ( if (fieldsToSample.length === 0) return { stats, errors }; const respMapping = await esClient.fieldCaps({ - ...getRequestBase(params), + ...getRequestBase(fieldStatsParams), fields: fieldsToSample, }); - const fieldStatsParams: FieldStatsCommonRequestParams = { - ...params, - samplerShardSize: 5000, - }; const fieldStatsPromises = Object.entries(respMapping.body.fields) .map(([key, value], idx) => { const field: FieldValuePair = { fieldName: key, fieldValue: '' }; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts index c64bbc6678779..16ba4f24f5e93 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts @@ -14,7 +14,6 @@ import { Aggs, TopValueBucket, } from '../../../../../common/correlations/field_stats_types'; -import { buildSamplerAggregation } from '../../utils/field_stats_utils'; import { getQueryWithParams } from '../get_query_with_params'; export const getKeywordFieldStatsRequest = ( @@ -24,7 +23,7 @@ export const getKeywordFieldStatsRequest = ( ): estypes.SearchRequest => { const query = getQueryWithParams({ params, termFilters }); - const { index, samplerShardSize } = params; + const { index } = params; const size = 0; const aggs: Aggs = { @@ -32,23 +31,19 @@ export const getKeywordFieldStatsRequest = ( terms: { field: fieldName, size: 10, - order: { - _count: 'desc', - }, }, }, }; const searchBody = { query, - aggs: { - sample: buildSamplerAggregation(aggs, samplerShardSize), - }, + aggs, }; return { index, size, + track_total_hits: false, body: searchBody, }; }; @@ -66,19 +61,16 @@ export const fetchKeywordFieldStats = async ( ); const { body } = await esClient.search(request); const aggregations = body.aggregations as { - sample: { - sampled_top: estypes.AggregationsTermsAggregate; - }; + sampled_top: estypes.AggregationsTermsAggregate; }; - const topValues: TopValueBucket[] = - aggregations?.sample.sampled_top?.buckets ?? []; + const topValues: TopValueBucket[] = aggregations?.sampled_top?.buckets ?? []; const stats = { fieldName: field.fieldName, topValues, topValuesSampleSize: topValues.reduce( (acc, curr) => acc + curr.doc_count, - aggregations.sample.sampled_top?.sum_other_doc_count ?? 0 + aggregations.sampled_top?.sum_other_doc_count ?? 0 ), }; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts index 21e6559fdda25..197ed66c4fe70 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { find, get } from 'lodash'; +import { get } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { NumericFieldStats, @@ -16,10 +16,6 @@ import { } from '../../../../../common/correlations/field_stats_types'; import { FieldValuePair } from '../../../../../common/correlations/types'; import { getQueryWithParams } from '../get_query_with_params'; -import { buildSamplerAggregation } from '../../utils/field_stats_utils'; - -// Only need 50th percentile for the median -const PERCENTILES = [50]; export const getNumericFieldStatsRequest = ( params: FieldStatsCommonRequestParams, @@ -29,9 +25,8 @@ export const getNumericFieldStatsRequest = ( const query = getQueryWithParams({ params, termFilters }); const size = 0; - const { index, samplerShardSize } = params; + const { index } = params; - const percents = PERCENTILES; const aggs: Aggs = { sampled_field_stats: { filter: { exists: { field: fieldName } }, @@ -41,13 +36,6 @@ export const getNumericFieldStatsRequest = ( }, }, }, - sampled_percentiles: { - percentiles: { - field: fieldName, - percents, - keyed: false, - }, - }, sampled_top: { terms: { field: fieldName, @@ -61,14 +49,13 @@ export const getNumericFieldStatsRequest = ( const searchBody = { query, - aggs: { - sample: buildSamplerAggregation(aggs, samplerShardSize), - }, + aggs, }; return { index, size, + track_total_hits: false, body: searchBody, }; }; @@ -87,19 +74,15 @@ export const fetchNumericFieldStats = async ( const { body } = await esClient.search(request); const aggregations = body.aggregations as { - sample: { - sampled_top: estypes.AggregationsTermsAggregate; - sampled_percentiles: estypes.AggregationsHdrPercentilesAggregate; - sampled_field_stats: { - doc_count: number; - actual_stats: estypes.AggregationsStatsAggregate; - }; + sampled_top: estypes.AggregationsTermsAggregate; + sampled_field_stats: { + doc_count: number; + actual_stats: estypes.AggregationsStatsAggregate; }; }; - const docCount = aggregations?.sample.sampled_field_stats?.doc_count ?? 0; - const fieldStatsResp = - aggregations?.sample.sampled_field_stats?.actual_stats ?? {}; - const topValues = aggregations?.sample.sampled_top?.buckets ?? []; + const docCount = aggregations?.sampled_field_stats?.doc_count ?? 0; + const fieldStatsResp = aggregations?.sampled_field_stats?.actual_stats ?? {}; + const topValues = aggregations?.sampled_top?.buckets ?? []; const stats: NumericFieldStats = { fieldName: field.fieldName, @@ -110,20 +93,9 @@ export const fetchNumericFieldStats = async ( topValues, topValuesSampleSize: topValues.reduce( (acc: number, curr: TopValueBucket) => acc + curr.doc_count, - aggregations.sample.sampled_top?.sum_other_doc_count ?? 0 + aggregations.sampled_top?.sum_other_doc_count ?? 0 ), }; - if (stats.count !== undefined && stats.count > 0) { - const percentiles = aggregations?.sample.sampled_percentiles.values ?? []; - const medianPercentile: { value: number; key: number } | undefined = find( - percentiles, - { - key: 50, - } - ); - stats.median = medianPercentile !== undefined ? medianPercentile!.value : 0; - } - return stats; }; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/index.ts b/x-pack/plugins/apm/server/routes/correlations/queries/index.ts index 548127eb7647d..d2a86a20bd5c6 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/index.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/index.ts @@ -16,3 +16,4 @@ export { fetchTransactionDurationCorrelation } from './query_correlation'; export { fetchTransactionDurationCorrelationWithHistogram } from './query_correlation_with_histogram'; export { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps'; export { fetchTransactionDurationRanges } from './query_ranges'; +export { fetchFieldValueFieldStats } from './field_stats/get_field_value_stats'; diff --git a/x-pack/plugins/apm/server/routes/correlations/route.ts b/x-pack/plugins/apm/server/routes/correlations/route.ts index b02a6fbc6b7a6..377fedf9d1813 100644 --- a/x-pack/plugins/apm/server/routes/correlations/route.ts +++ b/x-pack/plugins/apm/server/routes/correlations/route.ts @@ -19,6 +19,7 @@ import { fetchSignificantCorrelations, fetchTransactionDurationFieldCandidates, fetchTransactionDurationFieldValuePairs, + fetchFieldValueFieldStats, } from './queries'; import { fetchFieldsStats } from './queries/field_stats/get_fields_stats'; @@ -77,12 +78,12 @@ const fieldStatsRoute = createApmServerRoute({ transactionName: t.string, transactionType: t.string, }), - environmentRt, - kueryRt, - rangeRt, t.type({ fieldsToSample: t.array(t.string), }), + environmentRt, + kueryRt, + rangeRt, ]), }), options: { tags: ['access:apm'] }, @@ -112,6 +113,51 @@ const fieldStatsRoute = createApmServerRoute({ }, }); +const fieldValueStatsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/correlations/field_value_stats', + params: t.type({ + query: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + fieldName: t.string, + fieldValue: t.union([t.string, t.number]), + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + + const { indices } = await setupRequest(resources); + const esClient = resources.context.core.elasticsearch.client.asCurrentUser; + + const { fieldName, fieldValue, ...params } = resources.params.query; + + return withApmSpan( + 'get_correlations_field_value_stats', + async () => + await fetchFieldValueFieldStats( + esClient, + { + ...params, + index: indices.transaction, + }, + { fieldName, fieldValue } + ) + ); + }, +}); + const fieldValuePairsRoute = createApmServerRoute({ endpoint: 'POST /internal/apm/correlations/field_value_pairs', params: t.type({ @@ -252,5 +298,6 @@ export const correlationsRouteRepository = createApmServerRouteRepository() .add(pValuesRoute) .add(fieldCandidatesRoute) .add(fieldStatsRoute) + .add(fieldValueStatsRoute) .add(fieldValuePairsRoute) .add(significantCorrelationsRoute); diff --git a/x-pack/plugins/apm/server/routes/correlations/utils/field_stats_utils.ts b/x-pack/plugins/apm/server/routes/correlations/utils/field_stats_utils.ts index 7f98f771c50e2..a60622583781b 100644 --- a/x-pack/plugins/apm/server/routes/correlations/utils/field_stats_utils.ts +++ b/x-pack/plugins/apm/server/routes/correlations/utils/field_stats_utils.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - /* * Contains utility functions for building and processing queries. */ @@ -38,22 +36,3 @@ export function buildBaseFilterCriteria( return filterCriteria; } - -// Wraps the supplied aggregations in a sampler aggregation. -// A supplied samplerShardSize (the shard_size parameter of the sampler aggregation) -// of less than 1 indicates no sampling, and the aggs are returned as-is. -export function buildSamplerAggregation( - aggs: any, - samplerShardSize: number -): estypes.AggregationsAggregationContainer { - if (samplerShardSize < 1) { - return aggs; - } - - return { - sampler: { - shard_size: samplerShardSize, - }, - aggs, - }; -} diff --git a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.test.ts b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.test.ts index 7d345b5e3bec1..cdea5cd43f02f 100644 --- a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.test.ts +++ b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.test.ts @@ -36,7 +36,7 @@ describe('createStaticDataView', () => { const savedObjectsClient = getMockSavedObjectsClient('apm-*'); await createStaticDataView({ setup, - config: { autocreateApmIndexPattern: false } as APMConfig, + config: { autoCreateApmDataView: false } as APMConfig, savedObjectsClient, spaceId: 'default', }); @@ -53,7 +53,7 @@ describe('createStaticDataView', () => { await createStaticDataView({ setup, - config: { autocreateApmIndexPattern: true } as APMConfig, + config: { autoCreateApmDataView: true } as APMConfig, savedObjectsClient, spaceId: 'default', }); @@ -70,7 +70,7 @@ describe('createStaticDataView', () => { await createStaticDataView({ setup, - config: { autocreateApmIndexPattern: true } as APMConfig, + config: { autoCreateApmDataView: true } as APMConfig, savedObjectsClient, spaceId: 'default', }); @@ -90,7 +90,7 @@ describe('createStaticDataView', () => { await createStaticDataView({ setup, - config: { autocreateApmIndexPattern: true } as APMConfig, + config: { autoCreateApmDataView: true } as APMConfig, savedObjectsClient, spaceId: 'default', }); @@ -117,7 +117,7 @@ describe('createStaticDataView', () => { await createStaticDataView({ setup, - config: { autocreateApmIndexPattern: true } as APMConfig, + config: { autoCreateApmDataView: true } as APMConfig, savedObjectsClient, spaceId: 'default', }); diff --git a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts index 20b3d3117dd9f..665f9ca3e96eb 100644 --- a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts +++ b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts @@ -31,8 +31,8 @@ export async function createStaticDataView({ spaceId?: string; }): Promise { return withApmSpan('create_static_data_view', async () => { - // don't autocreate APM data view if it's been disabled via the config - if (!config.autocreateApmIndexPattern) { + // don't auto-create APM data view if it's been disabled via the config + if (!config.autoCreateApmDataView) { return false; } diff --git a/x-pack/plugins/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts b/x-pack/plugins/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts index d32e751a6ca99..e460991029915 100644 --- a/x-pack/plugins/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts +++ b/x-pack/plugins/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts @@ -110,7 +110,6 @@ export async function getErrorGroupMainStatistics({ ); return ( - // @ts-ignore 4.3.5 upgrade - Expression produces a union type that is too complex to represent. ts(2590) response.aggregations?.error_groups.buckets.map((bucket) => ({ groupId: bucket.key as string, name: getErrorName(bucket.sample.hits.hits[0]._source), 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..a5fcececad1cc 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 @@ -120,7 +120,6 @@ export async function getServiceAnomalies({ const relevantBuckets = uniqBy( sortBy( // make sure we only return data for jobs that are available in this space - // @ts-ignore 4.3.5 upgrade typedAnomalyResponse.aggregations?.services.buckets.filter((bucket) => jobIds.includes(bucket.key.jobId as string) ) ?? [], @@ -162,17 +161,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 +181,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/services/get_service_instances/get_service_instances_transaction_statistics.ts b/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts index a9f5615acb1c0..ec081916f455d 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts @@ -168,7 +168,6 @@ export async function getServiceInstancesTransactionStatistics< const { timeseries } = serviceNodeBucket; return { serviceNodeName, - // @ts-ignore 4.3.5 upgrade - Expression produces a union type that is too complex to represent. errorRate: timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key, y: dateBucket.failures.doc_count / dateBucket.doc_count, 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/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot index b8013818ca58f..d47ecf71b2293 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot @@ -109,21 +109,34 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` className="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow canvasAssetManager__emptyPanel" >
-
-

- Import your assets to get started -

+
+ +
+
+
+

+ Import your assets to get started +

+
+
+
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot b/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot index 6d782713d8fc1..8f00060a1dd1c 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot @@ -16,49 +16,61 @@ exports[`Storyshots Home/Components/Empty Prompt Empty Prompt 1`] = ` className="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusNone euiPanel--subdued euiPanel--noShadow euiPanel--noBorder" >
-
-

- Add your first workpad -

-
+ className="euiEmptyPrompt__icon" + > + +
-

- Create a new workpad, start from a template, or import a workpad JSON file by dropping it here. -

-

- New to Canvas? - - +

Add your first workpad - - . -

+

+ +
+
+

+ Create a new workpad, start from a template, or import a workpad JSON file by dropping it here. +

+

+ New to Canvas? + + + Add your first workpad + + . +

+
+ +
-
+
diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot index f019f9dc8f23d..23202a7a1fb88 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot @@ -86,36 +86,49 @@ exports[`Storyshots components/SavedElementsModal no custom elements 1`] = ` className="euiSpacer euiSpacer--l" />
-
-

- Add new elements -

-
+ className="euiEmptyPrompt__icon" + > + +
-

- Group and save workpad elements to create new elements -

+
+

+ Add new elements +

+ +
+
+

+ Group and save workpad elements to create new elements +

+
+ +
- +
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/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index b8074436d350b..42e96ab4471fe 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -188,7 +188,6 @@ module.exports = { { test: [ require.resolve('@elastic/eui/es/components/drag_and_drop'), - require.resolve('@elastic/eui/packages/react-datepicker'), require.resolve('highlight.js'), ], use: require.resolve('null-loader'), diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx index e9b6d71eee274..1b05145d561f5 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -36,14 +36,6 @@ Date.now = jest.fn(() => testTime.getTime()); // Mock telemetry service jest.mock('../public/lib/ui_metric', () => ({ trackCanvasUiMetric: () => {} })); -// Mock react-datepicker dep used by eui to avoid rendering the entire large component -jest.mock('@elastic/eui/packages/react-datepicker', () => { - return { - __esModule: true, - default: 'ReactDatePicker', - }; -}); - // Mock React Portal for components that use modals, tooltips, etc // @ts-expect-error Portal mocks are notoriously difficult to type ReactDOM.createPortal = jest.fn((element) => element); diff --git a/x-pack/plugins/cases/common/api/cases/alerts.ts b/x-pack/plugins/cases/common/api/cases/alerts.ts index 1a1abb4cbb66a..3647b1acb3a40 100644 --- a/x-pack/plugins/cases/common/api/cases/alerts.ts +++ b/x-pack/plugins/cases/common/api/cases/alerts.ts @@ -14,5 +14,4 @@ const AlertRt = rt.type({ }); export const AlertResponseRt = rt.array(AlertRt); - export type AlertResponse = rt.TypeOf; 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/common/utils/markdown_plugins/lens/serializer.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts index e561b2f8cfb8a..69d01b0051e18 100644 --- a/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { Plugin } from 'unified'; import type { TimeRange } from 'src/plugins/data/common'; import { LENS_ID } from './constants'; @@ -13,8 +14,13 @@ export interface LensSerializerProps { timeRange: TimeRange; } -export const LensSerializer = ({ timeRange, attributes }: LensSerializerProps) => +const serializeLens = ({ timeRange, attributes }: LensSerializerProps) => `!{${LENS_ID}${JSON.stringify({ timeRange, attributes, })}}`; + +export const LensSerializer: Plugin = function () { + const Compiler = this.Compiler; + Compiler.prototype.visitors.lens = serializeLens; +}; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts index 0a95c9466b1ff..b9448f93d95c3 100644 --- a/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts @@ -5,8 +5,14 @@ * 2.0. */ +import { Plugin } from 'unified'; export interface TimelineSerializerProps { match: string; } -export const TimelineSerializer = ({ match }: TimelineSerializerProps) => match; +const serializeTimeline = ({ match }: TimelineSerializerProps) => match; + +export const TimelineSerializer: Plugin = function () { + const Compiler = this.Compiler; + Compiler.prototype.visitors.timeline = serializeTimeline; +}; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/utils.test.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/utils.test.ts index baee979856511..516aff2300759 100644 --- a/x-pack/plugins/cases/common/utils/markdown_plugins/utils.test.ts +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/utils.test.ts @@ -18,5 +18,93 @@ describe('markdown utils', () => { const parsed = parseCommentString('hello\n'); expect(stringifyMarkdownComment(parsed)).toEqual('hello\n'); }); + + // This check ensures the version of remark-stringify supports tables. From version 9+ this is not supported by default. + it('parses and stringifies github formatted markdown correctly', () => { + const parsed = parseCommentString(`| Tables | Are | Cool | + |----------|:-------------:|------:| + | col 1 is | left-aligned | $1600 | + | col 2 is | centered | $12 | + | col 3 is | right-aligned | $1 |`); + + expect(stringifyMarkdownComment(parsed)).toMatchInlineSnapshot(` + "| Tables | Are | Cool | + | -------- | :-----------: | ----: | + | col 1 is | left-aligned | $1600 | + | col 2 is | centered | $12 | + | col 3 is | right-aligned | $1 | + " + `); + }); + + it('parses a timeline url', () => { + const timelineUrl = + '[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))'; + + const parsedNodes = parseCommentString(timelineUrl); + + expect(parsedNodes).toMatchInlineSnapshot(` + Object { + "children": Array [ + Object { + "match": "[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))", + "position": Position { + "end": Object { + "column": 138, + "line": 1, + "offset": 137, + }, + "indent": Array [], + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "timeline", + }, + ], + "position": Object { + "end": Object { + "column": 138, + "line": 1, + "offset": 137, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "root", + } + `); + }); + + it('stringifies a timeline url', () => { + const timelineUrl = + '[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))'; + + const parsedNodes = parseCommentString(timelineUrl); + + expect(stringifyMarkdownComment(parsedNodes)).toEqual(`${timelineUrl}\n`); + }); + + it('parses a lens visualization', () => { + const lensVisualization = + '!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"TEst22","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}}}}}},"visualization":{"layerId":"layer1","accessor":"col2"},"query":{"language":"kuery","query":""},"filters":[]},"references":[{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-layer1"}]}}}'; + + const parsedNodes = parseCommentString(lensVisualization); + expect(parsedNodes.children[0].type).toEqual('lens'); + }); + + it('stringifies a lens visualization', () => { + const lensVisualization = + '!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"TEst22","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}}}}}},"visualization":{"layerId":"layer1","accessor":"col2"},"query":{"language":"kuery","query":""},"filters":[]},"references":[{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-layer1"}]}}}'; + + const parsedNodes = parseCommentString(lensVisualization); + + expect(stringifyMarkdownComment(parsedNodes)).toEqual(`${lensVisualization}\n`); + }); }); }); diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/utils.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/utils.ts index b6b061fcb41d9..e9bda7ae469e2 100644 --- a/x-pack/plugins/cases/common/utils/markdown_plugins/utils.ts +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/utils.ts @@ -45,20 +45,13 @@ export const parseCommentString = (comment: string) => { export const stringifyMarkdownComment = (comment: MarkdownNode) => unified() .use([ - [ - remarkStringify, - { - allowDangerousHtml: true, - handlers: { - /* - because we're using rison in the timeline url we need - to make sure that markdown parser doesn't modify the url - */ - timeline: TimelineSerializer, - lens: LensSerializer, - }, - }, - ], + [remarkStringify], + /* + because we're using rison in the timeline url we need + to make sure that markdown parser doesn't modify the url + */ + LensSerializer, + TimelineSerializer, ]) .stringify(comment); 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/__snapshots__/audit_logger.test.ts.snap b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap index ed0fa1eb0f3ed..8933c70c8eaf0 100644 --- a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap +++ b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap @@ -924,6 +924,90 @@ Object { } `; +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAttachmentMetrics" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_get_metrics", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to access cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAttachmentMetrics" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_get_metrics", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAttachmentMetrics" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_get_metrics", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User has accessed cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAttachmentMetrics" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_get_metrics", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a comments as any owners", +} +`; + exports[`audit_logger log function event structure creates the correct audit event for operation: "getCase" with an error and entity 1`] = ` Object { "error": Object { 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..3596423860bf3 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'; @@ -83,11 +83,47 @@ export function isWriteOperation(operation: OperationDetails): boolean { return Object.values(WriteOperations).includes(operation.name as WriteOperations); } -/** - * Definition of all APIs within the cases backend. - */ -export const Operations: Record = { - // case operations +const CaseOperations = { + [ReadOperations.GetCase]: { + ecsType: EVENT_TYPES.access, + name: ACCESS_CASE_OPERATION, + action: 'case_get', + verbs: accessVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, + [ReadOperations.ResolveCase]: { + ecsType: EVENT_TYPES.access, + name: ACCESS_CASE_OPERATION, + action: 'case_resolve', + verbs: accessVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, + [ReadOperations.FindCases]: { + ecsType: EVENT_TYPES.access, + name: ACCESS_CASE_OPERATION, + action: 'case_find', + verbs: accessVerbs, + docType: 'cases', + savedObjectType: CASE_SAVED_OBJECT, + }, + [ReadOperations.GetCaseIDsByAlertID]: { + ecsType: EVENT_TYPES.access, + name: ACCESS_CASE_OPERATION, + action: 'case_ids_by_alert_id_get', + verbs: accessVerbs, + docType: 'cases', + savedObjectType: CASE_COMMENT_SAVED_OBJECT, + }, + [ReadOperations.GetCaseMetrics]: { + ecsType: EVENT_TYPES.access, + name: ACCESS_CASE_OPERATION, + action: 'case_get_metrics', + verbs: accessVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, [WriteOperations.CreateCase]: { ecsType: EVENT_TYPES.creation, name: WriteOperations.CreateCase, @@ -120,6 +156,17 @@ export const Operations: Record = { + ...CaseOperations, + ...ConfigurationOperations, + ...AttachmentOperations, + [ReadOperations.GetTags]: { ecsType: EVENT_TYPES.access, - name: ACCESS_COMMENT_OPERATION, - action: 'case_comment_get_all', + name: ReadOperations.GetTags, + action: 'case_tags_get', verbs: accessVerbs, - docType: 'comments', - savedObjectType: CASE_COMMENT_SAVED_OBJECT, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, }, - [ReadOperations.FindComments]: { + [ReadOperations.GetReporters]: { ecsType: EVENT_TYPES.access, - name: ACCESS_COMMENT_OPERATION, - action: 'case_comment_find', + name: ReadOperations.GetReporters, + action: 'case_reporters_get', verbs: accessVerbs, - docType: 'comments', - savedObjectType: CASE_COMMENT_SAVED_OBJECT, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, }, - // stats operations [ReadOperations.GetCaseStatuses]: { ecsType: EVENT_TYPES.access, name: ACCESS_CASE_OPERATION, @@ -274,7 +291,6 @@ export const Operations: Record => { - const { unsecuredSavedObjectsClient, authorization, attachmentService } = clientArgs; - - // This will perform an authorization check to ensure the user has access to the parent case - const theCase = await casesClient.cases.get({ - id: caseId, - includeComments: false, - includeSubCaseComments: false, - }); - - const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } = - await authorization.getAuthorizationFilter(Operations.getAlertsAttachedToCase); - - const alerts = await attachmentService.getAllAlertsAttachToCase({ - unsecuredSavedObjectsClient, - caseId: theCase.id, - filter: authorizationFilter, - }); - - ensureSavedObjectsAreAuthorized( - alerts.map((alert) => ({ - owner: alert.attributes.owner, - id: alert.id, - })) - ); - - return normalizeAlertResponse(alerts); + const { unsecuredSavedObjectsClient, authorization, attachmentService, logger } = clientArgs; + + try { + // This will perform an authorization check to ensure the user has access to the parent case + const theCase = await casesClient.cases.get({ + id: caseId, + includeComments: false, + includeSubCaseComments: false, + }); + + const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } = + await authorization.getAuthorizationFilter(Operations.getAlertsAttachedToCase); + + const alerts = await attachmentService.getAllAlertsAttachToCase({ + unsecuredSavedObjectsClient, + caseId: theCase.id, + filter: authorizationFilter, + }); + + ensureSavedObjectsAreAuthorized( + alerts.map((alert) => ({ + owner: alert.attributes.owner, + id: alert.id, + })) + ); + + return normalizeAlertResponse(alerts); + } catch (error) { + throw createCaseError({ + message: `Failed to get alerts attached to case id: ${caseId}: ${error}`, + error, + logger, + }); + } }; /** 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..118761acb3680 100644 --- a/x-pack/plugins/cases/server/client/metrics/alerts_count.ts +++ b/x-pack/plugins/cases/server/client/metrics/alerts_count.ts @@ -5,19 +5,57 @@ * 2.0. */ -import { CaseMetricsResponse } from '../../../common'; +import { CaseMetricsResponse } from '../../../common/api'; +import { Operations } from '../../authorization'; +import { createCaseError } from '../../common/error'; +import { CasesClient } from '../client'; +import { CasesClientArgs } from '../types'; import { MetricsHandler } from './types'; export class AlertsCount implements MetricsHandler { + constructor( + private readonly caseId: string, + private readonly casesClient: CasesClient, + private readonly clientArgs: CasesClientArgs + ) {} + public getFeatures(): Set { return new Set(['alertsCount']); } public async compute(): Promise { - return { - alerts: { - count: 0, - }, - }; + const { unsecuredSavedObjectsClient, authorization, attachmentService, logger } = + this.clientArgs; + + try { + // This will perform an authorization check to ensure the user has access to the parent case + const theCase = await this.casesClient.cases.get({ + id: this.caseId, + includeComments: false, + includeSubCaseComments: false, + }); + + const { filter: authorizationFilter } = await authorization.getAuthorizationFilter( + Operations.getAttachmentMetrics + ); + + const alertsCount = await attachmentService.countAlertsAttachedToCase({ + unsecuredSavedObjectsClient, + caseId: theCase.id, + filter: authorizationFilter, + }); + + return { + alerts: { + count: alertsCount ?? 0, + }, + }; + } catch (error) { + throw createCaseError({ + message: `Failed to count alerts attached case id: ${this.caseId}: ${error}`, + error, + logger, + }); + } } } 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..b192e681df109 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,12 +6,12 @@ */ 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'; import { loggingSystemMock, savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import { createCaseServiceMock } from '../../services/mocks'; +import { createAttachmentServiceMock, createCaseServiceMock } from '../../services/mocks'; import { SavedObject } from 'kibana/server'; describe('getMetrics', () => { @@ -28,7 +28,16 @@ describe('getMetrics', () => { } as unknown as CaseResponse; }); + const attachmentService = createAttachmentServiceMock(); + attachmentService.countAlertsAttachedToCase.mockImplementation(async () => { + return 5; + }); + const authorization = createAuthorizationMock(); + authorization.getAuthorizationFilter.mockImplementation(async () => { + return { filter: undefined, ensureSavedObjectsAreAuthorized: () => {} }; + }); + const soClient = savedObjectsClientMock.create(); const caseService = createCaseServiceMock(); caseService.getCase.mockImplementation(async () => { @@ -47,6 +56,7 @@ describe('getMetrics', () => { unsecuredSavedObjectsClient: soClient, caseService, logger, + attachmentService, } as unknown as CasesClientArgs; beforeEach(() => { @@ -100,7 +110,7 @@ describe('getMetrics', () => { clientArgs ); - expect(metrics.alerts?.count).toBeDefined(); + expect(metrics.alerts?.count).toEqual(5); expect(metrics.alerts?.hosts).toBeDefined(); }); 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..0cc089a1c0882 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 @@ -5,10 +5,11 @@ * 2.0. */ import { merge } from 'lodash'; +import Boom from '@hapi/boom'; -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'; @@ -33,23 +34,33 @@ export const getCaseMetrics = async ( casesClient: CasesClient, clientArgs: CasesClientArgs ): Promise => { - const handlers = buildHandlers(params, casesClient, clientArgs); - await checkAuthorization(params, clientArgs); - checkAndThrowIfInvalidFeatures(params, handlers, clientArgs); + const { logger } = clientArgs; - const computedMetrics = await Promise.all( - params.features.map(async (feature) => { - const handler = handlers.get(feature); + try { + const handlers = buildHandlers(params, casesClient, clientArgs); + await checkAuthorization(params, clientArgs); + checkAndThrowIfInvalidFeatures(params, handlers); - return handler?.compute(); - }) - ); + const computedMetrics = await Promise.all( + params.features.map(async (feature) => { + const handler = handlers.get(feature); - const mergedResults = computedMetrics.reduce((acc, metric) => { - return merge(acc, metric); - }, {}); + return handler?.compute(); + }) + ); - return CaseMetricsResponseRt.encode(mergedResults ?? {}); + const mergedResults = computedMetrics.reduce((acc, metric) => { + return merge(acc, metric); + }, {}); + + return CaseMetricsResponseRt.encode(mergedResults ?? {}); + } catch (error) { + throw createCaseError({ + logger, + message: `Failed to retrieve metrics within client for case id: ${params.caseId}: ${error}`, + error, + }); + } }; const buildHandlers = ( @@ -59,7 +70,7 @@ const buildHandlers = ( ): Map => { const handlers = [ new Lifespan(params.caseId, casesClient), - new AlertsCount(), + new AlertsCount(params.caseId, casesClient, clientArgs), new AlertDetails(), new Connectors(), ]; @@ -75,18 +86,16 @@ const buildHandlers = ( const checkAndThrowIfInvalidFeatures = ( params: CaseMetricsParams, - handlers: Map, - clientArgs: CasesClientArgs + handlers: Map ) => { const invalidFeatures = params.features.filter((feature) => !handlers.has(feature)); if (invalidFeatures.length > 0) { const invalidFeaturesAsString = invalidFeatures.join(', '); const validFeaturesAsString = [...handlers.keys()].join(', '); - throw createCaseError({ - logger: clientArgs.logger, - message: `invalid features: [${invalidFeaturesAsString}], please only provide valid features: [${validFeaturesAsString}]`, - }); + throw Boom.badRequest( + `invalid features: [${invalidFeaturesAsString}], please only provide valid features: [${validFeaturesAsString}]` + ); } }; 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.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/comments.test.ts index 9e8ff1a334686..385c1c5945a11 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/comments.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/comments.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { createCommentsMigrations, stringifyCommentWithoutTrailingNewline } from './comments'; +import { + createCommentsMigrations, + mergeMigrationFunctionMaps, + migrateByValueLensVisualizations, + stringifyCommentWithoutTrailingNewline, +} from './comments'; import { getLensVisualizations, parseCommentString, @@ -14,84 +19,98 @@ import { import { savedObjectsServiceMock } from '../../../../../../src/core/server/mocks'; import { lensEmbeddableFactory } from '../../../../lens/server/embeddable/lens_embeddable_factory'; import { LensDocShape715 } from '../../../../lens/server'; -import { SavedObjectReference } from 'kibana/server'; +import { + SavedObjectReference, + SavedObjectsMigrationLogger, + SavedObjectUnsanitizedDoc, +} from 'kibana/server'; +import { + MigrateFunction, + MigrateFunctionsObject, +} from '../../../../../../src/plugins/kibana_utils/common'; +import { SerializableRecord } from '@kbn/utility-types'; -const migrations = createCommentsMigrations({ - lensEmbeddableFactory, -}); +describe('comments migrations', () => { + const migrations = createCommentsMigrations({ + lensEmbeddableFactory, + }); -const contextMock = savedObjectsServiceMock.createMigrationContext(); -describe('index migrations', () => { - describe('lens embeddable migrations for by value panels', () => { - describe('7.14.0 remove time zone from Lens visualization date histogram', () => { - const lensVisualizationToMigrate = { - title: 'MyRenamedOps', - description: '', - visualizationType: 'lnsXY', - state: { - datasourceStates: { - indexpattern: { - layers: { - '2': { - columns: { - '3': { - label: '@timestamp', - dataType: 'date', - operationType: 'date_histogram', - sourceField: '@timestamp', - isBucketed: true, - scale: 'interval', - params: { interval: 'auto', timeZone: 'Europe/Berlin' }, - }, - '4': { - label: '@timestamp', - dataType: 'date', - operationType: 'date_histogram', - sourceField: '@timestamp', - isBucketed: true, - scale: 'interval', - params: { interval: 'auto' }, - }, - '5': { - label: '@timestamp', - dataType: 'date', - operationType: 'my_unexpected_operation', - isBucketed: true, - scale: 'interval', - params: { timeZone: 'do not delete' }, - }, - }, - columnOrder: ['3', '4', '5'], - incompleteColumns: {}, + const contextMock = savedObjectsServiceMock.createMigrationContext(); + + const lensVisualizationToMigrate = { + title: 'MyRenamedOps', + description: '', + visualizationType: 'lnsXY', + state: { + datasourceStates: { + indexpattern: { + layers: { + '2': { + columns: { + '3': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto', timeZone: 'Europe/Berlin' }, + }, + '4': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '5': { + label: '@timestamp', + dataType: 'date', + operationType: 'my_unexpected_operation', + isBucketed: true, + scale: 'interval', + params: { timeZone: 'do not delete' }, }, }, + columnOrder: ['3', '4', '5'], + incompleteColumns: {}, }, }, - visualization: { - title: 'Empty XY chart', - legend: { isVisible: true, position: 'right' }, - valueLabels: 'hide', - preferredSeriesType: 'bar_stacked', - layers: [ - { - layerId: '5ab74ddc-93ca-44e2-9857-ecf85c86b53e', - accessors: [ - '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', - 'e5efca70-edb5-4d6d-a30a-79384066987e', - '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', - ], - position: 'top', - seriesType: 'bar_stacked', - showGridlines: false, - xAccessor: '2e57a41e-5a52-42d3-877f-bd211d903ef8', - }, + }, + }, + visualization: { + title: 'Empty XY chart', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: '5ab74ddc-93ca-44e2-9857-ecf85c86b53e', + accessors: [ + '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', + 'e5efca70-edb5-4d6d-a30a-79384066987e', + '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', ], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + xAccessor: '2e57a41e-5a52-42d3-877f-bd211d903ef8', }, - query: { query: '', language: 'kuery' }, - filters: [], - }, - }; + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('lens embeddable migrations for by value panels', () => { + describe('7.14.0 remove time zone from Lens visualization date histogram', () => { const expectedLensVisualizationMigrated = { title: 'MyRenamedOps', description: '', @@ -241,43 +260,140 @@ describe('index migrations', () => { expect((columns[2] as { params: {} }).params).toEqual({ timeZone: 'do not delete' }); }); }); + }); - describe('stringifyCommentWithoutTrailingNewline', () => { - it('removes the newline added by the markdown library when the comment did not originally have one', () => { - const originalComment = 'awesome'; - const parsedString = parseCommentString(originalComment); + describe('handles errors', () => { + interface CommentSerializable extends SerializableRecord { + comment?: string; + } - expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual( - 'awesome' - ); - }); + const migrationFunction: MigrateFunction = ( + comment + ) => { + throw new Error('an error'); + }; - it('leaves the newline if it was in the original comment', () => { - const originalComment = 'awesome\n'; - const parsedString = parseCommentString(originalComment); + const comment = `!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"editMode\":false,\"attributes\":${JSON.stringify( + lensVisualizationToMigrate + )}}}\n\n`; - expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual( - 'awesome\n' - ); - }); + const caseComment = { + type: 'cases-comments', + id: '1cefd0d0-e86d-11eb-bae5-3d065cd16a32', + attributes: { + comment, + }, + references: [], + }; - it('does not remove newlines that are not at the end of the comment', () => { - const originalComment = 'awesome\ncomment'; - const parsedString = parseCommentString(originalComment); + it('logs an error when it fails to parse invalid json', () => { + const commentMigrationFunction = migrateByValueLensVisualizations(migrationFunction, '1.0.0'); - expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual( - 'awesome\ncomment' - ); + const result = commentMigrationFunction(caseComment, contextMock); + // the comment should remain unchanged when there is an error + expect(result.attributes.comment).toEqual(comment); + + const log = contextMock.log as jest.Mocked; + expect(log.error.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Failed to migrate comment with doc id: 1cefd0d0-e86d-11eb-bae5-3d065cd16a32 version: 8.0.0 error: an error", + Object { + "migrations": Object { + "comment": Object { + "id": "1cefd0d0-e86d-11eb-bae5-3d065cd16a32", + }, + }, + }, + ] + `); + }); + + describe('mergeMigrationFunctionMaps', () => { + it('logs an error when the passed migration functions fails', () => { + const migrationObj1 = { + '1.0.0': migrateByValueLensVisualizations(migrationFunction, '1.0.0'), + } as unknown as MigrateFunctionsObject; + + const migrationObj2 = { + '2.0.0': (doc: SavedObjectUnsanitizedDoc<{ comment?: string }>) => { + return doc; + }, + }; + + const mergedFunctions = mergeMigrationFunctionMaps(migrationObj1, migrationObj2); + mergedFunctions['1.0.0'](caseComment, contextMock); + + const log = contextMock.log as jest.Mocked; + expect(log.error.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Failed to migrate comment with doc id: 1cefd0d0-e86d-11eb-bae5-3d065cd16a32 version: 8.0.0 error: an error", + Object { + "migrations": Object { + "comment": Object { + "id": "1cefd0d0-e86d-11eb-bae5-3d065cd16a32", + }, + }, + }, + ] + `); }); - it('does not remove spaces at the end of the comment', () => { - const originalComment = 'awesome '; - const parsedString = parseCommentString(originalComment); + it('it does not log an error when the migration function does not use the context', () => { + const migrationObj1 = { + '1.0.0': migrateByValueLensVisualizations(migrationFunction, '1.0.0'), + } as unknown as MigrateFunctionsObject; - expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual( - 'awesome ' - ); + const migrationObj2 = { + '2.0.0': (doc: SavedObjectUnsanitizedDoc<{ comment?: string }>) => { + throw new Error('2.0.0 error'); + }, + }; + + const mergedFunctions = mergeMigrationFunctionMaps(migrationObj1, migrationObj2); + + expect(() => mergedFunctions['2.0.0'](caseComment, contextMock)).toThrow(); + + const log = contextMock.log as jest.Mocked; + expect(log.error).not.toHaveBeenCalled(); }); }); }); + + describe('stringifyCommentWithoutTrailingNewline', () => { + it('removes the newline added by the markdown library when the comment did not originally have one', () => { + const originalComment = 'awesome'; + const parsedString = parseCommentString(originalComment); + + expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual( + 'awesome' + ); + }); + + it('leaves the newline if it was in the original comment', () => { + const originalComment = 'awesome\n'; + const parsedString = parseCommentString(originalComment); + + expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual( + 'awesome\n' + ); + }); + + it('does not remove newlines that are not at the end of the comment', () => { + const originalComment = 'awesome\ncomment'; + const parsedString = parseCommentString(originalComment); + + expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual( + 'awesome\ncomment' + ); + }); + + it('does not remove spaces at the end of the comment', () => { + const originalComment = 'awesome '; + const parsedString = parseCommentString(originalComment); + + expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual( + 'awesome ' + ); + }); + }); }); 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..0af9db13fce40 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 @@ -5,12 +5,9 @@ * 2.0. */ -import { mapValues, trimEnd } from 'lodash'; -import { SerializableRecord } from '@kbn/utility-types'; - -import { LensServerPluginSetup } from '../../../../lens/server'; +import { mapValues, trimEnd, mergeWith } from 'lodash'; +import type { SerializableRecord } from '@kbn/utility-types'; import { - mergeMigrationFunctionMaps, MigrateFunction, MigrateFunctionsObject, } from '../../../../../../src/plugins/kibana_utils/common'; @@ -19,8 +16,10 @@ import { SavedObjectSanitizedDoc, SavedObjectMigrationFn, SavedObjectMigrationMap, + SavedObjectMigrationContext, } from '../../../../../../src/core/server'; -import { CommentType, AssociationType } from '../../../common'; +import { LensServerPluginSetup } from '../../../../lens/server'; +import { CommentType, AssociationType } from '../../../common/api'; import { isLensMarkdownNode, LensMarkdownNode, @@ -29,6 +28,7 @@ import { stringifyMarkdownComment, } from '../../../common/utils/markdown_plugins/utils'; import { addOwnerToSO, SanitizedCaseOwner } from '.'; +import { logError } from './utils'; interface UnsanitizedComment { comment: string; @@ -103,33 +103,41 @@ export const createCommentsMigrations = ( return mergeMigrationFunctionMaps(commentsMigrations, embeddableMigrations); }; -const migrateByValueLensVisualizations = - (migrate: MigrateFunction, version: string): SavedObjectMigrationFn<{ comment?: string }> => - (doc: SavedObjectUnsanitizedDoc<{ comment?: string }>) => { +export const migrateByValueLensVisualizations = + ( + migrate: MigrateFunction, + version: string + ): SavedObjectMigrationFn<{ comment?: string }, { comment?: string }> => + (doc: SavedObjectUnsanitizedDoc<{ comment?: string }>, context: SavedObjectMigrationContext) => { if (doc.attributes.comment == null) { return doc; } - const parsedComment = parseCommentString(doc.attributes.comment); - const migratedComment = parsedComment.children.map((comment) => { - if (isLensMarkdownNode(comment)) { - // casting here because ts complains that comment isn't serializable because LensMarkdownNode - // extends Node which has fields that conflict with SerializableRecord even though it is serializable - return migrate(comment as SerializableRecord) as LensMarkdownNode; - } + try { + const parsedComment = parseCommentString(doc.attributes.comment); + const migratedComment = parsedComment.children.map((comment) => { + if (isLensMarkdownNode(comment)) { + // casting here because ts complains that comment isn't serializable because LensMarkdownNode + // extends Node which has fields that conflict with SerializableRecord even though it is serializable + return migrate(comment as SerializableRecord) as LensMarkdownNode; + } - return comment; - }); + return comment; + }); - const migratedMarkdown = { ...parsedComment, children: migratedComment }; + const migratedMarkdown = { ...parsedComment, children: migratedComment }; - return { - ...doc, - attributes: { - ...doc.attributes, - comment: stringifyCommentWithoutTrailingNewline(doc.attributes.comment, migratedMarkdown), - }, - }; + return { + ...doc, + attributes: { + ...doc.attributes, + comment: stringifyCommentWithoutTrailingNewline(doc.attributes.comment, migratedMarkdown), + }, + }; + } catch (error) { + logError({ id: doc.id, context, error, docType: 'comment', docKey: 'comment' }); + return doc; + } }; export const stringifyCommentWithoutTrailingNewline = ( @@ -147,3 +155,23 @@ export const stringifyCommentWithoutTrailingNewline = ( // so the comment stays consistent return trimEnd(stringifiedComment, '\n'); }; + +/** + * merge function maps adds the context param from the original implementation at: + * src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts + * */ +export const mergeMigrationFunctionMaps = ( + // using the saved object framework types here because they include the context, this avoids type errors in our tests + obj1: SavedObjectMigrationMap, + obj2: SavedObjectMigrationMap +) => { + const customizer = (objValue: SavedObjectMigrationFn, srcValue: SavedObjectMigrationFn) => { + if (!srcValue || !objValue) { + return srcValue || objValue; + } + return (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => + objValue(srcValue(doc, context), context); + }; + + return mergeWith({ ...obj1 }, obj2, customizer); +}; 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..3d0cff814b7d4 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 @@ -7,9 +7,14 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { SavedObjectMigrationContext, SavedObjectSanitizedDoc } from 'kibana/server'; +import { + SavedObjectMigrationContext, + SavedObjectSanitizedDoc, + SavedObjectsMigrationLogger, +} 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, @@ -216,7 +221,19 @@ describe('user action migrations', () => { userActionsConnectorIdMigration(userAction, context); - expect(context.log.error).toHaveBeenCalled(); + const log = context.log as jest.Mocked; + expect(log.error.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Failed to migrate user action connector with doc id: 1 version: 8.0.0 error: Unexpected token a in JSON at position 1", + Object { + "migrations": Object { + "userAction": Object { + "id": "1", + }, + }, + }, + ] + `); }); }); @@ -384,7 +401,19 @@ describe('user action migrations', () => { userActionsConnectorIdMigration(userAction, context); - expect(context.log.error).toHaveBeenCalled(); + const log = context.log as jest.Mocked; + expect(log.error.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Failed to migrate user action connector with doc id: 1 version: 8.0.0 error: Unexpected token b in JSON at position 1", + Object { + "migrations": Object { + "userAction": Object { + "id": "1", + }, + }, + }, + ] + `); }); }); @@ -554,7 +583,19 @@ describe('user action migrations', () => { userActionsConnectorIdMigration(userAction, context); - expect(context.log.error).toHaveBeenCalled(); + const log = context.log as jest.Mocked; + expect(log.error.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Failed to migrate user action connector with doc id: 1 version: 8.0.0 error: Unexpected token e in JSON at position 1", + Object { + "migrations": Object { + "userAction": Object { + "id": "1", + }, + }, + }, + ] + `); }); }); }); 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..4d8395eb189fc 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 @@ -12,12 +12,13 @@ import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc, 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'; +import { logError } from './utils'; interface UserActions { action_field: string[]; @@ -32,10 +33,6 @@ interface UserActionUnmigratedConnectorDocument { old_value?: string | null; } -interface UserActionLogMeta extends LogMeta { - migrations: { userAction: { id: string } }; -} - export function userActionsConnectorIdMigration( doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext @@ -49,7 +46,13 @@ export function userActionsConnectorIdMigration( try { return formatDocumentWithConnectorReferences(doc); } catch (error) { - logError(doc.id, context, error); + logError({ + id: doc.id, + context, + error, + docType: 'user action connector', + docKey: 'userAction', + }); return originalDocWithReferences; } @@ -98,19 +101,6 @@ function formatDocumentWithConnectorReferences( }; } -function logError(id: string, context: SavedObjectMigrationContext, error: Error) { - context.log.error( - `Failed to migrate user action connector doc id: ${id} version: ${context.migrationVersion} error: ${error.message}`, - { - migrations: { - userAction: { - id, - }, - }, - } - ); -} - export const userActionsMigrations = { '7.10.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => { const { action_field, new_value, old_value, ...restAttributes } = doc.attributes; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts new file mode 100644 index 0000000000000..565688cd6ac3c --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { SavedObjectsMigrationLogger } from 'kibana/server'; +import { migrationMocks } from '../../../../../../src/core/server/mocks'; +import { logError } from './utils'; + +describe('migration utils', () => { + const context = migrationMocks.createContext(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('logs an error', () => { + const log = context.log as jest.Mocked; + + logError({ + id: '1', + context, + error: new Error('an error'), + docType: 'a document', + docKey: 'key', + }); + + expect(log.error.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Failed to migrate a document with doc id: 1 version: 8.0.0 error: an error", + Object { + "migrations": Object { + "key": Object { + "id": "1", + }, + }, + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts new file mode 100644 index 0000000000000..993d70181974d --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogMeta, SavedObjectMigrationContext } from '../../../../../../src/core/server'; + +interface MigrationLogMeta extends LogMeta { + migrations: { + [x: string]: { + id: string; + }; + }; +} + +export function logError({ + id, + context, + error, + docType, + docKey, +}: { + id: string; + context: SavedObjectMigrationContext; + error: Error; + docType: string; + docKey: string; +}) { + context.log.error( + `Failed to migrate ${docType} with doc id: ${id} version: ${context.migrationVersion} error: ${error.message}`, + { + migrations: { + [docKey]: { + id, + }, + }, + } + ); +} 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..9553f7c7ed1e2 100644 --- a/x-pack/plugins/cases/server/services/attachments/index.ts +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -12,24 +12,30 @@ import { SavedObjectsUpdateOptions, } from 'kibana/server'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; 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'; +import { defaultSortField } from '../../common/utils'; interface GetAllAlertsAttachToCaseArgs extends ClientArgs { caseId: string; filter?: KueryNode; } +type CountAlertsAttachedToCaseArgs = GetAllAlertsAttachToCaseArgs; + interface GetAttachmentArgs extends ClientArgs { attachmentId: string; } @@ -55,6 +61,51 @@ interface BulkUpdateAttachmentArgs extends ClientArgs { export class AttachmentService { constructor(private readonly log: Logger) {} + public async countAlertsAttachedToCase({ + unsecuredSavedObjectsClient, + caseId, + filter, + }: CountAlertsAttachedToCaseArgs): Promise { + try { + this.log.debug(`Attempting to count alerts for case id ${caseId}`); + const alertsFilter = buildFilter({ + filters: [CommentType.alert, CommentType.generatedAlert], + field: 'type', + operator: 'or', + type: CASE_COMMENT_SAVED_OBJECT, + }); + + const combinedFilter = combineFilters([alertsFilter, filter]); + + const response = await unsecuredSavedObjectsClient.find< + AttachmentAttributes, + { alerts: { value: number } } + >({ + type: CASE_COMMENT_SAVED_OBJECT, + page: 1, + perPage: 1, + sortField: defaultSortField, + aggs: this.buildCountAlertsAggs(), + filter: combinedFilter, + }); + + return response.aggregations?.alerts?.value; + } catch (error) { + this.log.error(`Error while counting alerts for case id ${caseId}: ${error}`); + throw error; + } + } + + private buildCountAlertsAggs(): Record { + return { + alerts: { + cardinality: { + field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, + }, + }, + }; + } + /** * Retrieves all the alerts attached to a case. */ 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/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index f46bcd0906c60..3e68126967512 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -111,6 +111,7 @@ export const createAttachmentServiceMock = (): AttachmentServiceMock => { update: jest.fn(), bulkUpdate: jest.fn(), getAllAlertsAttachToCase: jest.fn(), + countAlertsAttachedToCase: jest.fn(), }; // the cast here is required because jest.Mocked tries to include private members and would throw an error 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/cross_cluster_replication/public/app/index.tsx b/x-pack/plugins/cross_cluster_replication/public/app/index.tsx index ea3eb50c46089..d6dc16a55a99f 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/index.tsx +++ b/x-pack/plugins/cross_cluster_replication/public/app/index.tsx @@ -8,9 +8,17 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Provider } from 'react-redux'; -import { I18nStart, ScopedHistory, ApplicationStart } from 'kibana/public'; -import { UnmountCallback } from 'src/core/public'; -import { DocLinksStart } from 'kibana/public'; +import { Observable } from 'rxjs'; + +import { + UnmountCallback, + I18nStart, + ScopedHistory, + ApplicationStart, + DocLinksStart, + CoreTheme, +} from 'src/core/public'; +import { KibanaThemeProvider } from '../shared_imports'; import { init as initBreadcrumbs, SetBreadcrumbs } from './services/breadcrumbs'; import { init as initDocumentation } from './services/documentation_links'; import { App } from './app'; @@ -20,13 +28,16 @@ const renderApp = ( element: Element, I18nContext: I18nStart['Context'], history: ScopedHistory, - getUrlForApp: ApplicationStart['getUrlForApp'] + getUrlForApp: ApplicationStart['getUrlForApp'], + theme$: Observable ): UnmountCallback => { render( - - - + + + + + , element ); @@ -41,6 +52,7 @@ export async function mountApp({ docLinks, history, getUrlForApp, + theme$, }: { element: Element; setBreadcrumbs: SetBreadcrumbs; @@ -48,11 +60,12 @@ export async function mountApp({ docLinks: DocLinksStart; history: ScopedHistory; getUrlForApp: ApplicationStart['getUrlForApp']; + theme$: Observable; }): Promise { // Import and initialize additional services here instead of in plugin.ts to reduce the size of the // initial bundle as much as possible. initBreadcrumbs(setBreadcrumbs); initDocumentation(docLinks); - return renderApp(element, I18nContext, history, getUrlForApp); + return renderApp(element, I18nContext, history, getUrlForApp, theme$); } diff --git a/x-pack/plugins/cross_cluster_replication/public/plugin.ts b/x-pack/plugins/cross_cluster_replication/public/plugin.ts index a45862d46beeb..bc2546bdacb2a 100644 --- a/x-pack/plugins/cross_cluster_replication/public/plugin.ts +++ b/x-pack/plugins/cross_cluster_replication/public/plugin.ts @@ -41,7 +41,7 @@ export class CrossClusterReplicationPlugin implements Plugin { id: MANAGEMENT_ID, title: PLUGIN.TITLE, order: 6, - mount: async ({ element, setBreadcrumbs, history }) => { + mount: async ({ element, setBreadcrumbs, history, theme$ }) => { const { mountApp } = await import('./app'); const [coreStart] = await getStartServices(); @@ -61,6 +61,7 @@ export class CrossClusterReplicationPlugin implements Plugin { docLinks, history, getUrlForApp, + theme$, }); return () => { diff --git a/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts b/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts index 38838968ad212..f850e054f9667 100644 --- a/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts +++ b/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts @@ -13,4 +13,6 @@ export { PageLoading, } from '../../../../src/plugins/es_ui_shared/public'; +export { KibanaThemeProvider } from '../../../../src/plugins/kibana_react/public'; + export { APP_WRAPPER_CLASS } from '../../../../src/core/public'; diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts index dd6dd58d02f70..bbb08d0ac2b66 100644 --- a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts +++ b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts @@ -8,6 +8,7 @@ export const DEFAULT_INITIAL_APP_DATA = { kibanaVersion: '7.16.0', enterpriseSearchVersion: '7.16.0', + errorConnectingMessage: '', readOnlyMode: false, searchOAuth: { clientId: 'someUID', diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts index 57fe3f3807783..17b3eb17d31bd 100644 --- a/x-pack/plugins/enterprise_search/common/types/index.ts +++ b/x-pack/plugins/enterprise_search/common/types/index.ts @@ -17,6 +17,7 @@ import { export interface InitialAppData { enterpriseSearchVersion?: string; kibanaVersion?: string; + errorConnectingMessage?: string; readOnlyMode?: boolean; searchOAuth?: SearchOAuth; configuredLimits?: ConfiguredLimits; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.test.tsx index 1810b05a938da..9f46d6750590f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; +import '../../../../../__mocks__/shallow_useeffect.mock'; import '../../../../__mocks__/engine_logic.mock'; import React from 'react'; @@ -15,21 +16,26 @@ import { EuiCodeBlock, EuiFlyout, EuiTab, EuiTabs } from '@elastic/eui'; import { Loading } from '../../../../../shared/loading'; -import { CrawlDetailActions, CrawlDetailValues } from '../../crawl_detail_logic'; import { CrawlRequestWithDetailsFromServer } from '../../types'; import { CrawlDetailsPreview } from './crawl_details_preview'; import { CrawlDetailsFlyout } from '.'; -const MOCK_VALUES: Partial = { +const MOCK_VALUES = { dataLoading: false, flyoutClosed: false, crawlRequestFromServer: {} as CrawlRequestWithDetailsFromServer, + logRetention: { + crawler: { + enabled: true, + }, + }, }; -const MOCK_ACTIONS: Partial = { +const MOCK_ACTIONS = { setSelectedTab: jest.fn(), + fetchLogRetention: jest.fn(), }; describe('CrawlDetailsFlyout', () => { @@ -38,6 +44,7 @@ describe('CrawlDetailsFlyout', () => { }); it('renders a flyout ', () => { + setMockActions(MOCK_ACTIONS); setMockValues(MOCK_VALUES); const wrapper = shallow(); @@ -82,7 +89,22 @@ describe('CrawlDetailsFlyout', () => { it('shows the human readable version of the crawl details', () => { const wrapper = shallow(); - expect(wrapper.find(CrawlDetailsPreview)).toHaveLength(1); + const crawlDetailsPreview = wrapper.find(CrawlDetailsPreview); + expect(crawlDetailsPreview).toHaveLength(1); + expect(crawlDetailsPreview.prop('crawlerLogsEnabled')).toEqual(true); + }); + + it('shows the preview differently if the crawler logs are disabled', () => { + setMockValues({ + ...MOCK_VALUES, + selectedTab: 'preview', + logRetention: null, + }); + const wrapper = shallow(); + + const crawlDetailsPreview = wrapper.find(CrawlDetailsPreview); + expect(crawlDetailsPreview).toHaveLength(1); + expect(crawlDetailsPreview.prop('crawlerLogsEnabled')).toEqual(false); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.tsx index 9c3c1da534f72..f1bfd3e5e5e9b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; @@ -21,6 +21,7 @@ import { import { i18n } from '@kbn/i18n'; import { Loading } from '../../../../../shared/loading'; +import { LogRetentionLogic } from '../../../log_retention'; import { CrawlDetailLogic } from '../../crawl_detail_logic'; import { CrawlDetailsPreview } from './crawl_details_preview'; @@ -29,6 +30,12 @@ export const CrawlDetailsFlyout: React.FC = () => { const { closeFlyout, setSelectedTab } = useActions(CrawlDetailLogic); const { crawlRequestFromServer, dataLoading, flyoutClosed, selectedTab } = useValues(CrawlDetailLogic); + const { fetchLogRetention } = useActions(LogRetentionLogic); + const { logRetention } = useValues(LogRetentionLogic); + + useEffect(() => { + fetchLogRetention(); + }, []); if (flyoutClosed) { return null; @@ -73,7 +80,11 @@ export const CrawlDetailsFlyout: React.FC = () => { ) : ( <> - {selectedTab === 'preview' && } + {selectedTab === 'preview' && ( + + )} {selectedTab === 'json' && ( {JSON.stringify(crawlRequestFromServer, null, 2)} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.test.tsx index 646c611901c7f..f97e2ff913150 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.test.tsx @@ -9,12 +9,14 @@ import { setMockValues } from '../../../../../__mocks__/kea_logic'; import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; +import { set } from 'lodash/fp'; import { CrawlDetailValues } from '../../crawl_detail_logic'; import { CrawlerStatus, CrawlType } from '../../types'; import { AccordionList } from './accordion_list'; import { CrawlDetailsPreview } from './crawl_details_preview'; +import { CrawlDetailsSummary } from './crawl_details_summary'; const MOCK_VALUES: Partial = { crawlRequest: { @@ -28,6 +30,15 @@ const MOCK_VALUES: Partial = { domainAllowlist: ['https://www.elastic.co', 'https://www.swiftype.com'], seedUrls: ['https://www.elastic.co/docs', 'https://www.swiftype.com/documentation'], sitemapUrls: ['https://www.elastic.co/sitemap.xml', 'https://www.swiftype.com/sitemap.xml'], + maxCrawlDepth: 10, + }, + stats: { + status: { + urlsAllowed: 10, + pagesVisited: 10, + crawlDurationMSec: 36000, + avgResponseTimeMSec: 100, + }, }, }, }; @@ -38,16 +49,44 @@ describe('CrawlDetailsPreview', () => { crawlRequest: null, }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.isEmptyRender()).toBe(true); }); describe('when a crawl request has been loaded', () => { let wrapper: ShallowWrapper; - beforeAll(() => { + beforeEach(() => { setMockValues(MOCK_VALUES); - wrapper = shallow(); + wrapper = shallow(); + }); + + it('contains a summary', () => { + const summary = wrapper.find(CrawlDetailsSummary); + expect(summary.props()).toEqual({ + crawlDepth: 10, + crawlType: 'full', + crawlerLogsEnabled: true, + domainCount: 2, + stats: { + status: { + avgResponseTimeMSec: 100, + crawlDurationMSec: 36000, + pagesVisited: 10, + urlsAllowed: 10, + }, + }, + }); + }); + + it('will default values on summary if missing', () => { + const values = set('crawlRequest.stats', undefined, MOCK_VALUES); + setMockValues(values); + wrapper = shallow(); + + const summary = wrapper.find(CrawlDetailsSummary); + expect(summary.prop('crawlerLogsEnabled')).toEqual(false); + expect(summary.prop('stats')).toEqual(null); }); it('contains a list of domains', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.tsx index 6f837d1db26e2..a9f3d95edf1fa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.tsx @@ -15,8 +15,15 @@ import { i18n } from '@kbn/i18n'; import { CrawlDetailLogic } from '../../crawl_detail_logic'; import { AccordionList } from './accordion_list'; +import { CrawlDetailsSummary } from './crawl_details_summary'; -export const CrawlDetailsPreview: React.FC = () => { +interface CrawlDetailsPreviewProps { + crawlerLogsEnabled?: boolean; +} + +export const CrawlDetailsPreview: React.FC = ({ + crawlerLogsEnabled = false, +}) => { const { crawlRequest } = useValues(CrawlDetailLogic); if (crawlRequest === null) { @@ -25,6 +32,14 @@ export const CrawlDetailsPreview: React.FC = () => { return ( <> + + 0} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_summary.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_summary.test.tsx new file mode 100644 index 0000000000000..f37060a9cef42 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_summary.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../__mocks__/engine_logic.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiPanel } from '@elastic/eui'; + +import { CrawlDetailsSummary } from './crawl_details_summary'; + +const MOCK_PROPS = { + crawlDepth: 8, + crawlerLogsEnabled: true, + crawlType: 'full', + domainCount: 15, + stats: { + status: { + urlsAllowed: 108, + crawlDurationMSec: 748382, + pagesVisited: 108, + avgResponseTimeMSec: 42, + statusCodes: { + 401: 4, + 404: 8, + 500: 0, + 503: 3, + }, + }, + }, +}; + +describe('CrawlDetailsSummary', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('renders as a panel with all fields', () => { + expect(wrapper.is(EuiPanel)).toBe(true); + }); + + it('renders the proper count for errors', () => { + const serverErrors = wrapper.find({ 'data-test-subj': 'serverErrors' }); + const clientErrors = wrapper.find({ 'data-test-subj': 'clientErrors' }); + + expect(serverErrors.prop('title')).toEqual(3); + expect(clientErrors.prop('title')).toEqual(12); + }); + + it('handles missing stats gracefully', () => { + wrapper.setProps({ stats: {} }); + expect(wrapper.find({ 'data-test-subj': 'crawlDuration' }).prop('title')).toEqual('--'); + expect(wrapper.find({ 'data-test-subj': 'pagesVisited' }).prop('title')).toEqual('--'); + expect(wrapper.find({ 'data-test-subj': 'avgResponseTime' }).prop('title')).toEqual('--'); + }); + + it('renders the stat object when logs are disabled but stats are not null', () => { + wrapper.setProps({ crawlerLogsEnabled: false }); + expect(wrapper.find({ 'data-test-subj': 'crawlDuration' })).toHaveLength(1); + expect(wrapper.find({ 'data-test-subj': 'pagesVisited' })).toHaveLength(1); + expect(wrapper.find({ 'data-test-subj': 'avgResponseTime' })).toHaveLength(1); + expect(wrapper.find({ 'data-test-subj': 'urlsAllowed' })).toHaveLength(1); + expect(wrapper.find({ 'data-test-subj': 'logsDisabledMessage' })).toHaveLength(0); + }); + + it('renders a message to enable logs when crawler logs are disabled and stats are null', () => { + wrapper.setProps({ crawlerLogsEnabled: false, stats: null }); + expect(wrapper.find({ 'data-test-subj': 'crawlDuration' })).toHaveLength(0); + expect(wrapper.find({ 'data-test-subj': 'pagesVisited' })).toHaveLength(0); + expect(wrapper.find({ 'data-test-subj': 'avgResponseTime' })).toHaveLength(0); + expect(wrapper.find({ 'data-test-subj': 'urlsAllowed' })).toHaveLength(0); + expect(wrapper.find({ 'data-test-subj': 'logsDisabledMessage' })).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_summary.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_summary.tsx new file mode 100644 index 0000000000000..e05cbd9101de6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_summary.tsx @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 moment from 'moment'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIconTip, + EuiPanel, + EuiSpacer, + EuiStat, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CrawlRequestStats } from '../../types'; + +interface ICrawlerSummaryProps { + crawlDepth: number; + crawlType: string; + crawlerLogsEnabled: boolean; + domainCount: number; + stats: CrawlRequestStats | null; +} + +export const CrawlDetailsSummary: React.FC = ({ + crawlDepth, + crawlType, + crawlerLogsEnabled, + domainCount, + stats, +}) => { + const duration = () => { + if (stats && stats.status && stats.status.crawlDurationMSec) { + const milliseconds = moment.duration(stats.status.crawlDurationMSec, 'milliseconds'); + const hours = milliseconds.hours(); + const minutes = milliseconds.minutes(); + const seconds = milliseconds.seconds(); + return `${hours}h ${minutes}m ${seconds}s`; + } else { + return '--'; + } + }; + + const getStatusCount = (code: string, codes: { [code: string]: number }) => { + return Object.entries(codes).reduce((count, [k, v]) => { + if (k[0] !== code) return count; + return v + count; + }, 0); + }; + + const statusCounts = { + clientErrorCount: + stats && stats.status && stats.status.statusCodes + ? getStatusCount('4', stats.status.statusCodes) + : 0, + serverErrorCount: + stats && stats.status && stats.status.statusCodes + ? getStatusCount('5', stats.status.statusCodes) + : 0, + }; + + const shouldHideStats = !crawlerLogsEnabled && !stats; + + return ( + + + + + + + + + {!shouldHideStats && ( + + + + )} + + + {!shouldHideStats ? ( + + + + URLs{' '} + + + } + /> + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlDetailsSummary.pagesVisitedTooltipTitle', + { + defaultMessage: 'Pages', + } + )}{' '} + + + } + /> + + + + + + + + + + + + ) : ( + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlDetailsSummary.logsDisabledMessage', + { + defaultMessage: + 'Enable Web Crawler logs in settings for more detailed crawl statistics.', + } + )} +

+
+ )} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_event_type_badge.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_event_type_badge.test.tsx index fd1f03c586f12..bc5efc7714495 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_event_type_badge.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_event_type_badge.test.tsx @@ -31,6 +31,7 @@ const MOCK_EVENT: CrawlEvent = { domainAllowlist: ['https://www.elastic.co'], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, }, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx index 03e5d835df9b3..c2b36f24d7582 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx @@ -35,6 +35,7 @@ const values: { events: CrawlEvent[] } = { domainAllowlist: ['https://www.elastic.co'], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, }, }, { @@ -49,6 +50,7 @@ const values: { events: CrawlEvent[] } = { domainAllowlist: ['https://www.elastic.co'], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, }, }, ], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawl_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawl_detail_logic.test.ts index a7d795c93e0a7..152fe0f64de4d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawl_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawl_detail_logic.test.ts @@ -35,6 +35,19 @@ const crawlRequestResponse: CrawlRequestWithDetailsFromServer = { domain_allowlist: [], seed_urls: [], sitemap_urls: [], + max_crawl_depth: 10, + }, + stats: { + status: { + urls_allowed: 4, + pages_visited: 4, + crawl_duration_msec: 100, + avg_response_time_msec: 10, + status_codes: { + 200: 4, + 404: 0, + }, + }, }, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts index 5af9b1652c889..0735b5262a20a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts @@ -138,6 +138,7 @@ describe('CrawlerLogic', () => { domainAllowlist: ['elastic.co'], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, }, }, ], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index 0d2c2e60abfa9..440e29e6d3002 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -80,6 +80,7 @@ const events: CrawlEventFromServer[] = [ domain_allowlist: ['moviedatabase.com', 'swiftype.com'], seed_urls: [], sitemap_urls: [], + max_crawl_depth: 10, }, }, { @@ -94,6 +95,7 @@ const events: CrawlEventFromServer[] = [ domain_allowlist: ['swiftype.com'], seed_urls: [], sitemap_urls: [], + max_crawl_depth: 10, }, }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts index 85ebb0032971d..3d8881601ae1a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts @@ -199,28 +199,54 @@ export interface CrawlRequest { completedAt: string | null; } +export interface CrawlRequestStats { + status: { + avgResponseTimeMSec?: number; + crawlDurationMSec?: number; + pagesVisited?: number; + urlsAllowed?: number; + statusCodes?: { + [code: string]: number; + }; + }; +} + +export interface CrawlRequestStatsFromServer { + status: { + avg_response_time_msec?: number; + crawl_duration_msec?: number; + pages_visited?: number; + urls_allowed?: number; + status_codes?: { + [code: string]: number; + }; + }; +} + export interface CrawlConfig { domainAllowlist: string[]; seedUrls: string[]; sitemapUrls: string[]; + maxCrawlDepth: number; } export interface CrawlConfigFromServer { domain_allowlist: string[]; seed_urls: string[]; sitemap_urls: string[]; + max_crawl_depth: number; } export type CrawlRequestWithDetailsFromServer = CrawlRequestFromServer & { type: CrawlType; crawl_config: CrawlConfigFromServer; - // TODO add other properties like stats + stats: CrawlRequestStatsFromServer; }; export type CrawlRequestWithDetails = CrawlRequest & { type: CrawlType; crawlConfig: CrawlConfig; - // TODO add other properties like stats + stats: CrawlRequestStats | null; }; export type CrawlEventStage = 'crawl' | 'process'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts index 0df1f57eaefa0..cab4023370291 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts @@ -22,6 +22,7 @@ import { CrawlRequestWithDetails, CrawlEvent, CrawlEventFromServer, + CrawlRequestStatsFromServer, } from './types'; import { @@ -34,6 +35,7 @@ import { getDeleteDomainConfirmationMessage, getDeleteDomainSuccessMessage, getCrawlRulePathPatternTooltip, + crawlRequestStatsServerToClient, } from './utils'; const DEFAULT_CRAWL_RULE: CrawlRule = { @@ -126,6 +128,36 @@ describe('crawlRequestServerToClient', () => { }); }); +describe('crawlRequestStatsServerToClient', () => { + it('converts the API payload into properties matching our code style', () => { + const defaultServerPayload: CrawlRequestStatsFromServer = { + status: { + urls_allowed: 4, + pages_visited: 4, + crawl_duration_msec: 100, + avg_response_time_msec: 10, + status_codes: { + 200: 4, + 404: 0, + }, + }, + }; + + expect(crawlRequestStatsServerToClient(defaultServerPayload)).toEqual({ + status: { + urlsAllowed: 4, + pagesVisited: 4, + crawlDurationMSec: 100, + avgResponseTimeMSec: 10, + statusCodes: { + 200: 4, + 404: 0, + }, + }, + }); + }); +}); + describe('crawlRequestWithDetailsServerToClient', () => { it('converts the API payload into properties matching our code style', () => { const id = '507f1f77bcf86cd799439011'; @@ -141,6 +173,19 @@ describe('crawlRequestWithDetailsServerToClient', () => { domain_allowlist: [], seed_urls: [], sitemap_urls: [], + max_crawl_depth: 10, + }, + stats: { + status: { + urls_allowed: 4, + pages_visited: 4, + crawl_duration_msec: 100, + avg_response_time_msec: 10, + status_codes: { + 200: 4, + 404: 0, + }, + }, }, }; @@ -155,6 +200,19 @@ describe('crawlRequestWithDetailsServerToClient', () => { domainAllowlist: [], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, + }, + stats: { + status: { + urlsAllowed: 4, + pagesVisited: 4, + crawlDurationMSec: 100, + avgResponseTimeMSec: 10, + statusCodes: { + 200: 4, + 404: 0, + }, + }, }, }; @@ -191,6 +249,7 @@ describe('crawlEventServerToClient', () => { domain_allowlist: [], seed_urls: [], sitemap_urls: [], + max_crawl_depth: 10, }, stage: 'crawl', }; @@ -206,6 +265,7 @@ describe('crawlEventServerToClient', () => { domainAllowlist: [], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, }, stage: 'crawl', }; @@ -274,6 +334,7 @@ describe('crawlerDataServerToClient', () => { domain_allowlist: ['https://www.elastic.co'], seed_urls: [], sitemap_urls: [], + max_crawl_depth: 10, }, }, ], @@ -329,6 +390,7 @@ describe('crawlerDataServerToClient', () => { domainAllowlist: ['https://www.elastic.co'], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, }, }, ]); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts index d1203e19c0208..4819b073cccb3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts @@ -16,6 +16,8 @@ import { CrawlerDomainValidationStep, CrawlRequestFromServer, CrawlRequest, + CrawlRequestStats, + CrawlRequestStatsFromServer, CrawlRule, CrawlerRules, CrawlEventFromServer, @@ -66,6 +68,30 @@ export function crawlerDomainServerToClient(payload: CrawlerDomainFromServer): C return clientPayload; } +export function crawlRequestStatsServerToClient( + crawlStats: CrawlRequestStatsFromServer +): CrawlRequestStats { + const { + status: { + avg_response_time_msec: avgResponseTimeMSec, + crawl_duration_msec: crawlDurationMSec, + pages_visited: pagesVisited, + urls_allowed: urlsAllowed, + status_codes: statusCodes, + }, + } = crawlStats; + + return { + status: { + urlsAllowed, + pagesVisited, + avgResponseTimeMSec, + crawlDurationMSec, + statusCodes, + }, + }; +} + export function crawlRequestServerToClient(crawlRequest: CrawlRequestFromServer): CrawlRequest { const { id, @@ -89,12 +115,14 @@ export function crawlConfigServerToClient(crawlConfig: CrawlConfigFromServer): C domain_allowlist: domainAllowlist, seed_urls: seedUrls, sitemap_urls: sitemapUrls, + max_crawl_depth: maxCrawlDepth, } = crawlConfig; return { domainAllowlist, seedUrls, sitemapUrls, + maxCrawlDepth, }; } @@ -126,24 +154,25 @@ export function crawlRequestWithDetailsServerToClient( event: CrawlRequestWithDetailsFromServer ): CrawlRequestWithDetails { const { - id, - status, - created_at: createdAt, began_at: beganAt, completed_at: completedAt, - type, crawl_config: crawlConfig, + created_at: createdAt, + id, + stats: crawlStats, + status, + type, } = event; return { - id, - status, - createdAt, beganAt, completedAt, - type, crawlConfig: crawlConfigServerToClient(crawlConfig), - // TODO add fields like stats + createdAt, + id, + stats: crawlStats && crawlRequestStatsServerToClient(crawlStats), + status, + type, }; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx index 9ec3fdda63656..be17bfaeb7127 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx @@ -15,8 +15,10 @@ import { ErrorConnecting } from './'; describe('ErrorConnecting', () => { it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find(ErrorStatePrompt)).toHaveLength(1); + const errorStatePrompt = wrapper.find(ErrorStatePrompt); + expect(errorStatePrompt).toHaveLength(1); + expect(errorStatePrompt.prop('errorConnectingMessage')).toEqual('I am an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx index 84dcb07a07474..a01fb264935c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx @@ -13,14 +13,16 @@ import { ErrorStatePrompt } from '../../../shared/error_state'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -export const ErrorConnecting: React.FC = () => { +export const ErrorConnecting: React.FC<{ errorConnectingMessage?: string }> = ({ + errorConnectingMessage, +}) => { return ( <> - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 2f415840a6c4a..2ffb1f80a3d32 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -57,9 +57,11 @@ describe('AppSearch', () => { it('renders ErrorConnecting when Enterprise Search is unavailable', () => { setMockValues({ errorConnecting: true }); - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find(ErrorConnecting)).toHaveLength(1); + const errorConnection = wrapper.find(ErrorConnecting); + expect(errorConnection).toHaveLength(1); + expect(errorConnection.prop('errorConnectingMessage')).toEqual('I am an error'); }); it('renders AppSearchConfigured when config.host is set & available', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 027a4dbee5ef6..605d82d2af601 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -45,7 +45,7 @@ import { export const AppSearch: React.FC = (props) => { const { config } = useValues(KibanaLogic); const { errorConnecting } = useValues(HttpLogic); - const { enterpriseSearchVersion, kibanaVersion } = props; + const { enterpriseSearchVersion, kibanaVersion, errorConnectingMessage } = props; const incompatibleVersions = isVersionMismatch(enterpriseSearchVersion, kibanaVersion); const showView = () => { @@ -59,7 +59,7 @@ export const AppSearch: React.FC = (props) => { /> ); } else if (errorConnecting) { - return ; + return ; } return )} />; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx index 9ec3fdda63656..be17bfaeb7127 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx @@ -15,8 +15,10 @@ import { ErrorConnecting } from './'; describe('ErrorConnecting', () => { it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find(ErrorStatePrompt)).toHaveLength(1); + const errorStatePrompt = wrapper.find(ErrorStatePrompt); + expect(errorStatePrompt).toHaveLength(1); + expect(errorStatePrompt.prop('errorConnectingMessage')).toEqual('I am an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx index 979847b4cf1c6..f9ffd6c992426 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx @@ -12,10 +12,12 @@ import { KibanaPageTemplate } from '../../../../../../../../src/plugins/kibana_r import { ErrorStatePrompt } from '../../../shared/error_state'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -export const ErrorConnecting: React.FC = () => ( +export const ErrorConnecting: React.FC<{ errorConnectingMessage?: string }> = ({ + errorConnectingMessage, +}) => ( - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx index 7b5c748b013e5..a366057797925 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -37,10 +37,12 @@ describe('EnterpriseSearch', () => { errorConnecting: true, config: { host: 'localhost' }, }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(VersionMismatchPage)).toHaveLength(0); - expect(wrapper.find(ErrorConnecting)).toHaveLength(1); + const errorConnecting = wrapper.find(ErrorConnecting); + expect(errorConnecting).toHaveLength(1); + expect(errorConnecting.prop('errorConnectingMessage')).toEqual('I am an error'); expect(wrapper.find(ProductSelector)).toHaveLength(0); setMockValues({ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index 81aa587e3a133..ded5909a0fa43 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -26,6 +26,7 @@ export const EnterpriseSearch: React.FC = ({ workplaceSearch, enterpriseSearchVersion, kibanaVersion, + errorConnectingMessage, }) => { const { errorConnecting } = useValues(HttpLogic); const { config } = useValues(KibanaLogic); @@ -45,7 +46,7 @@ export const EnterpriseSearch: React.FC = ({ /> ); } else if (showErrorConnecting) { - return ; + return ; } return ; 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/shared/error_state/error_state_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx index e8f0816de5225..2d21ea7c61444 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx @@ -5,20 +5,48 @@ * 2.0. */ -import '../../__mocks__/kea_logic'; +import { setMockValues } from '../../__mocks__/kea_logic'; import React from 'react'; -import { shallow } from 'enzyme'; - -import { EuiEmptyPrompt } from '@elastic/eui'; +import { mountWithIntl } from '../../test_helpers'; import { ErrorStatePrompt } from './'; describe('ErrorState', () => { - it('renders', () => { - const wrapper = shallow(); + const values = { + config: {}, + cloud: { isCloudEnabled: true }, + }; + + beforeAll(() => { + setMockValues(values); + }); + + it('renders a cloud specific error on cloud deployments', () => { + setMockValues({ + ...values, + cloud: { isCloudEnabled: true }, + }); + const wrapper = mountWithIntl(); + + expect(wrapper.find('[data-test-subj="CloudError"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="SelfManagedError"]').exists()).toBe(false); + }); + + it('renders a different error if not a cloud deployment', () => { + setMockValues({ + ...values, + cloud: { isCloudEnabled: false }, + }); + const wrapper = mountWithIntl(); + + expect(wrapper.find('[data-test-subj="CloudError"]').exists()).toBe(false); + expect(wrapper.find('[data-test-subj="SelfManagedError"]').exists()).toBe(true); + }); - expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + it('renders an error message', () => { + const wrapper = mountWithIntl(); + expect(wrapper.text()).toContain('I am an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx index eff483df10c7f..fea43b902993d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx @@ -9,16 +9,22 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiCode, EuiLink, EuiCodeBlock } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { CloudSetup } from '../../../../../cloud/public'; + import { KibanaLogic } from '../kibana'; -import { EuiButtonTo } from '../react_router_helpers'; +import { EuiButtonTo, EuiLinkTo } from '../react_router_helpers'; import './error_state_prompt.scss'; -export const ErrorStatePrompt: React.FC = () => { - const { config } = useValues(KibanaLogic); +export const ErrorStatePrompt: React.FC<{ errorConnectingMessage?: string }> = ({ + errorConnectingMessage, +}) => { + const { config, cloud } = useValues(KibanaLogic); + const isCloudEnabled = cloud.isCloudEnabled; return ( {

{config.host}, + enterpriseSearchUrl: ( + + {config.host} + + ), }} />

-
    -
  1. - config/kibana.yml, - }} - /> -
  2. -
  3. - -
  4. -
  5. - -
      -
    • - -
    • -
    • - -
    • -
    -
  6. -
  7. - [enterpriseSearch][plugins], - }} - /> -
  8. -
+ {errorConnectingMessage} + {isCloudEnabled ? cloudError(cloud) : nonCloudError()} } actions={[ @@ -103,3 +69,69 @@ export const ErrorStatePrompt: React.FC = () => { /> ); }; + +const cloudError = (cloud: Partial) => { + const deploymentUrl = cloud?.deploymentUrl; + return ( +

+ + {i18n.translate( + 'xpack.enterpriseSearch.errorConnectingState.cloudErrorMessageLinkText', + { + defaultMessage: 'Check your deployment settings', + } + )} + + ), + }} + /> +

+ ); +}; + +const nonCloudError = () => { + return ( +
    +
  1. + config/kibana.yml, + }} + /> +
  2. +
  3. + +
  4. +
  5. + +
      +
    • + +
    • +
    • + +
    • +
    +
  6. +
+ ); +}; 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/components/shared/license_callout/license_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx index 270daf195bd38..7bf80b5ff9180 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx @@ -9,8 +9,9 @@ import React from 'react'; import { EuiLink, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; + import { EXPLORE_PLATINUM_FEATURES_LINK } from '../../../constants'; -import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; interface LicenseCalloutProps { message?: string; @@ -20,7 +21,7 @@ export const LicenseCallout: React.FC = ({ message }) => { const title = ( <> {message}{' '} - + {EXPLORE_PLATINUM_FEATURES_LINK} 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.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 7274ee8855705..9fa2c211f1667 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -51,9 +51,11 @@ describe('WorkplaceSearch', () => { it('renders ErrorState', () => { setMockValues({ errorConnecting: true }); - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find(ErrorState)).toHaveLength(1); + const errorState = wrapper.find(ErrorState); + expect(errorState).toHaveLength(1); + expect(errorState.prop('errorConnectingMessage')).toEqual('I am an error'); }); }); 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..41ad1670019ba 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'; @@ -49,7 +51,7 @@ import { SetupGuide } from './views/setup_guide'; export const WorkplaceSearch: React.FC = (props) => { const { config } = useValues(KibanaLogic); const { errorConnecting } = useValues(HttpLogic); - const { enterpriseSearchVersion, kibanaVersion } = props; + const { enterpriseSearchVersion, kibanaVersion, errorConnectingMessage } = props; const incompatibleVersions = isVersionMismatch(enterpriseSearchVersion, kibanaVersion); if (!config.host) { @@ -62,7 +64,7 @@ export const WorkplaceSearch: React.FC = (props) => { /> ); } else if (errorConnecting) { - return ; + return ; } return ; @@ -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..ee180ae52e0b7 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 @@ -7,7 +7,10 @@ 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'; @@ -17,35 +20,6 @@ export const LOGOUT_ROUTE = '/logout'; export const LEAVE_FEEDBACK_EMAIL = 'support@elastic.co'; export const LEAVE_FEEDBACK_URL = `mailto:${LEAVE_FEEDBACK_EMAIL}?Subject=Elastic%20Workplace%20Search%20Feedback`; -export const BOX_DOCS_URL = docLinks.workplaceSearchBox; -export const CONFLUENCE_DOCS_URL = docLinks.workplaceSearchConfluenceCloud; -export const CONFLUENCE_SERVER_DOCS_URL = docLinks.workplaceSearchConfluenceServer; -export const CUSTOM_SOURCE_DOCS_URL = docLinks.workplaceSearchCustomSources; -export const CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL = - docLinks.workplaceSearchCustomSourcePermissions; -export const DIFFERENT_SYNC_TYPES_DOCS_URL = docLinks.workplaceSearchIndexingSchedule; -export const DOCUMENT_PERMISSIONS_DOCS_URL = docLinks.workplaceSearchDocumentPermissions; -export const DROPBOX_DOCS_URL = docLinks.workplaceSearchDropbox; -export const ENT_SEARCH_LICENSE_MANAGEMENT = docLinks.licenseManagement; -export const EXTERNAL_IDENTITIES_DOCS_URL = docLinks.workplaceSearchExternalIdentities; -export const GETTING_STARTED_DOCS_URL = docLinks.workplaceSearchGettingStarted; -export const GITHUB_DOCS_URL = docLinks.workplaceSearchGitHub; -export const GITHUB_ENTERPRISE_DOCS_URL = docLinks.workplaceSearchGitHub; -export const GMAIL_DOCS_URL = docLinks.workplaceSearchGmail; -export const GOOGLE_DRIVE_DOCS_URL = docLinks.workplaceSearchGoogleDrive; -export const JIRA_DOCS_URL = docLinks.workplaceSearchJiraCloud; -export const JIRA_SERVER_DOCS_URL = docLinks.workplaceSearchJiraServer; -export const OBJECTS_AND_ASSETS_DOCS_URL = docLinks.workplaceSearchSynch; -export const ONEDRIVE_DOCS_URL = docLinks.workplaceSearchOneDrive; -export const PRIVATE_SOURCES_DOCS_URL = docLinks.workplaceSearchPermissions; -export const SALESFORCE_DOCS_URL = docLinks.workplaceSearchSalesforce; -export const SECURITY_DOCS_URL = docLinks.workplaceSearchSecurity; -export const SERVICENOW_DOCS_URL = docLinks.workplaceSearchServiceNow; -export const SHAREPOINT_DOCS_URL = docLinks.workplaceSearchSharePoint; -export const SLACK_DOCS_URL = docLinks.workplaceSearchSlack; -export const SYNCHRONIZATION_DOCS_URL = docLinks.workplaceSearchSynch; -export const ZENDESK_DOCS_URL = docLinks.workplaceSearchZendesk; - export const PERSONAL_PATH = '/p'; export const OAUTH_AUTHORIZE_PATH = `${PERSONAL_PATH}/oauth/authorize`; @@ -53,6 +27,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 +46,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/request/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/request/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/request/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/config_completed.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx index 167bf1af4b9b1..9b34053bfe524 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx @@ -21,13 +21,9 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { docLinks } from '../../../../../shared/doc_links'; import { EuiLinkTo, EuiButtonTo } from '../../../../../shared/react_router_helpers'; -import { - getSourcesPath, - ADD_SOURCE_PATH, - SECURITY_PATH, - PRIVATE_SOURCES_DOCS_URL, -} from '../../../../routes'; +import { getSourcesPath, ADD_SOURCE_PATH, SECURITY_PATH } from '../../../../routes'; import { CONFIG_COMPLETED_PRIVATE_SOURCES_DISABLED_LINK, @@ -126,7 +122,7 @@ export const ConfigCompleted: React.FC = ({ {CONFIG_COMPLETED_PRIVATE_SOURCES_DOCS_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx index 4682d4329a964..e794323dc169e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { CUSTOM_SOURCE_DOCS_URL } from '../../../../routes'; +import { docLinks } from '../../../../../shared/doc_links'; import { SOURCE_NAME_LABEL } from '../../constants'; @@ -63,7 +63,7 @@ export const ConfigureCustom: React.FC = ({ defaultMessage="{link} to learn more about Custom API Sources." values={{ link: ( - + {CONFIG_CUSTOM_LINK_TEXT} ), 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/document_permissions_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_callout.tsx index 3c6980f74bcf5..d3879eabe08de 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_callout.tsx @@ -17,8 +17,8 @@ import { EuiText, } from '@elastic/eui'; +import { docLinks } from '../../../../../shared/doc_links'; import { EXPLORE_PLATINUM_FEATURES_LINK } from '../../../../constants'; -import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../../routes'; import { SOURCE_FEATURES_DOCUMENT_LEVEL_PERMISSIONS_FEATURE, @@ -45,7 +45,7 @@ export const DocumentPermissionsCallout: React.FC = () => { - + {EXPLORE_PLATINUM_FEATURES_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_field.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_field.tsx index 1b1043ecbc3d2..1cc953ee7c2ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_field.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_field.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DOCUMENT_PERMISSIONS_DOCS_URL } from '../../../../routes'; +import { docLinks } from '../../../../../shared/doc_links'; import { LEARN_MORE_LINK } from '../../constants'; import { @@ -42,7 +42,7 @@ export const DocumentPermissionsField: React.FC = ({ setValue, }) => { const whichDocsLink = ( - + {CONNECT_WHICH_OPTION_LINK} ); @@ -64,7 +64,7 @@ export const DocumentPermissionsField: React.FC = ({ defaultMessage="Document-level permissions are not yet available for this source. {link}" values={{ link: ( - + {LEARN_MORE_LINK} ), 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..9dbbcc537fa31 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 @@ -24,27 +24,25 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { docLinks } from '../../../../../shared/doc_links'; 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, SOURCE_DISPLAY_SETTINGS_PATH, - CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL, - ENT_SEARCH_LICENSE_MANAGEMENT, getContentSourcePath, 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 +60,7 @@ interface SaveCustomProps { export const SaveCustom: React.FC = ({ documentationUrl, - newCustomSource: { id, accessToken, name }, + newCustomSource: { id, name }, isOrganization, header, }) => { @@ -106,24 +104,8 @@ export const SaveCustom: React.FC = ({
- - - -

{SAVE_CUSTOM_API_KEYS_TITLE}

-
- -

{SAVE_CUSTOM_API_KEYS_BODY}

-
- - - - -
-
+ + @@ -195,7 +177,10 @@ export const SaveCustom: React.FC = ({ defaultMessage="{link} manage content access content on individual or group attributes. Allow or deny access to specific documents." values={{ link: ( - + {SAVE_CUSTOM_DOC_PERMISSIONS_LINK} ), @@ -206,7 +191,7 @@ export const SaveCustom: React.FC = ({ {!hasPlatinumLicense && ( - + {LEARN_CUSTOM_FEATURES_BUTTON} 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..d3714c2174b66 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 @@ -33,11 +33,11 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { CANCEL_BUTTON_LABEL, START_BUTTON_LABEL } from '../../../../shared/constants'; +import { docLinks } from '../../../../shared/doc_links'; import { EuiListGroupItemTo, EuiLinkTo } from '../../../../shared/react_router_helpers'; 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'; @@ -47,10 +47,6 @@ import { DOCUMENTATION_LINK_TITLE, } from '../../../constants'; import { - CUSTOM_SOURCE_DOCS_URL, - DOCUMENT_PERMISSIONS_DOCS_URL, - ENT_SEARCH_LICENSE_MANAGEMENT, - EXTERNAL_IDENTITIES_DOCS_URL, SYNC_FREQUENCY_PATH, BLOCKED_TIME_WINDOWS_PATH, getGroupPath, @@ -78,8 +74,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 +86,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 +101,6 @@ export const Overview: React.FC = () => { groups, details, custom, - accessToken, licenseSupportsPermissions, serviceTypeSupportsPermissions, indexPermissions, @@ -350,7 +344,7 @@ export const Overview: React.FC = () => { defaultMessage="{learnMoreLink} about permissions" values={{ learnMoreLink: ( - + {LEARN_MORE_LINK} ), @@ -411,7 +405,7 @@ export const Overview: React.FC = () => { defaultMessage="The {externalIdentitiesLink} must be used to configure user access mappings. Read the guide to learn more." values={{ externalIdentitiesLink: ( - + {EXTERNAL_IDENTITIES_LINK} ), @@ -432,9 +426,7 @@ export const Overview: React.FC = () => { - - - + ); @@ -471,7 +463,7 @@ export const Overview: React.FC = () => { - + {LEARN_CUSTOM_FEATURES_BUTTON} @@ -574,7 +566,7 @@ export const Overview: React.FC = () => { defaultMessage="{learnMoreLink} about custom sources." values={{ learnMoreLink: ( - + {LEARN_MORE_LINK} ), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx index 6b0e43fbce0c4..e37849033a144 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -31,12 +31,12 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { docLinks } from '../../../../shared/doc_links'; import { TruncatedContent } from '../../../../shared/truncate'; import { ComponentLoader } from '../../../components/shared/component_loader'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { NAV, CUSTOM_SERVICE_TYPE } from '../../../constants'; -import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes'; import { SourceContentItem } from '../../../types'; import { NO_CONTENT_MESSAGE, @@ -110,7 +110,7 @@ export const SourceContent: React.FC = () => { defaultMessage="Learn more about adding content in our {documentationLink}" values={{ documentationLink: ( - + {CUSTOM_DOCUMENTATION_LINK} ), 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_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx index 663088f797c18..f741cfdc538fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx @@ -12,11 +12,11 @@ import moment from 'moment'; import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; import { PageTemplateProps } from '../../../../shared/layout'; import { AppLogic } from '../../../app_logic'; import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../components/layout'; import { NAV } from '../../../constants'; -import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; import { SOURCE_DISABLED_CALLOUT_TITLE, @@ -53,7 +53,7 @@ export const SourceLayout: React.FC = ({ <>

{SOURCE_DISABLED_CALLOUT_DESCRIPTION}

- + {SOURCE_DISABLED_CALLOUT_BUTTON}
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'} + +
+
+ )} = ({ tabId }) => { description={ <> {SOURCE_FREQUENCY_DESCRIPTION}{' '} - + {LEARN_MORE_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx index 2dfa2a6420f7f..460f7e7f42055 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx @@ -22,10 +22,10 @@ import { } from '@elastic/eui'; import { SAVE_BUTTON_LABEL } from '../../../../../shared/constants'; +import { docLinks } from '../../../../../shared/doc_links'; import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; import { NAV, RESET_BUTTON } from '../../../../constants'; -import { OBJECTS_AND_ASSETS_DOCS_URL } from '../../../../routes'; import { LEARN_MORE_LINK, SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL, @@ -87,7 +87,7 @@ export const ObjectsAndAssets: React.FC = () => { description={ <> {SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION}{' '} - + {LEARN_MORE_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx index dec275adb3c50..2e777fa906dd6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx @@ -11,9 +11,9 @@ import { useActions, useValues } from 'kea'; import { EuiCallOut, EuiLink, EuiPanel, EuiSwitch, EuiSpacer, EuiText } from '@elastic/eui'; +import { docLinks } from '../../../../../shared/doc_links'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; import { NAV } from '../../../../constants'; -import { SYNCHRONIZATION_DOCS_URL } from '../../../../routes'; import { LEARN_MORE_LINK, SOURCE_SYNCHRONIZATION_DESCRIPTION, @@ -68,7 +68,7 @@ export const Synchronization: React.FC = () => { description={ <> {SOURCE_SYNCHRONIZATION_DESCRIPTION}{' '} - + {LEARN_MORE_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts index 087716e565ad0..61e4aa3fc3884 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts @@ -147,13 +147,6 @@ export const EXTERNAL_IDENTITIES_LINK = i18n.translate( } ); -export const ACCESS_TOKEN_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.accessToken.label', - { - defaultMessage: 'Access Token', - } -); - export const ID_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.id.label', { defaultMessage: 'Source Identifier', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index 687461296ac9e..20a0673709b5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -7,6 +7,8 @@ import { i18n } from '@kbn/i18n'; +import { docLinks } from '../../../shared/doc_links'; + import { SOURCE_NAMES, SOURCE_OBJ_TYPES, GITHUB_LINK_TITLE } from '../../constants'; import { ADD_BOX_PATH, @@ -45,23 +47,6 @@ import { EDIT_SLACK_PATH, EDIT_ZENDESK_PATH, EDIT_CUSTOM_PATH, - BOX_DOCS_URL, - CONFLUENCE_DOCS_URL, - CONFLUENCE_SERVER_DOCS_URL, - GITHUB_ENTERPRISE_DOCS_URL, - DROPBOX_DOCS_URL, - GITHUB_DOCS_URL, - GMAIL_DOCS_URL, - GOOGLE_DRIVE_DOCS_URL, - JIRA_DOCS_URL, - JIRA_SERVER_DOCS_URL, - ONEDRIVE_DOCS_URL, - SALESFORCE_DOCS_URL, - SERVICENOW_DOCS_URL, - SHAREPOINT_DOCS_URL, - SLACK_DOCS_URL, - ZENDESK_DOCS_URL, - CUSTOM_SOURCE_DOCS_URL, } from '../../routes'; import { FeatureIds, SourceDataItem } from '../../types'; @@ -75,7 +60,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: BOX_DOCS_URL, + documentationUrl: docLinks.workplaceSearchBox, applicationPortalUrl: 'https://app.box.com/developers/console', }, objTypes: [SOURCE_OBJ_TYPES.FOLDERS, SOURCE_OBJ_TYPES.ALL_FILES], @@ -104,7 +89,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: true, - documentationUrl: CONFLUENCE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchConfluenceCloud, applicationPortalUrl: 'https://developer.atlassian.com/console/myapps/', }, objTypes: [ @@ -138,7 +123,7 @@ export const staticSourceData = [ isPublicKey: true, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: CONFLUENCE_SERVER_DOCS_URL, + documentationUrl: docLinks.workplaceSearchConfluenceServer, }, objTypes: [ SOURCE_OBJ_TYPES.PAGES, @@ -170,7 +155,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: DROPBOX_DOCS_URL, + documentationUrl: docLinks.workplaceSearchDropbox, applicationPortalUrl: 'https://www.dropbox.com/developers/apps', }, objTypes: [SOURCE_OBJ_TYPES.FOLDERS, SOURCE_OBJ_TYPES.ALL_FILES], @@ -200,7 +185,7 @@ export const staticSourceData = [ hasOauthRedirect: true, needsBaseUrl: false, needsConfiguration: true, - documentationUrl: GITHUB_DOCS_URL, + documentationUrl: docLinks.workplaceSearchGitHub, applicationPortalUrl: 'https://github.com/settings/developers', applicationLinkTitle: GITHUB_LINK_TITLE, }, @@ -242,7 +227,7 @@ export const staticSourceData = [ defaultMessage: 'GitHub Enterprise URL', } ), - documentationUrl: GITHUB_ENTERPRISE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchGitHub, applicationPortalUrl: 'https://github.com/settings/developers', applicationLinkTitle: GITHUB_LINK_TITLE, }, @@ -277,7 +262,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: GMAIL_DOCS_URL, + documentationUrl: docLinks.workplaceSearchGmail, applicationPortalUrl: 'https://console.developers.google.com/', }, objTypes: [SOURCE_OBJ_TYPES.EMAILS], @@ -295,7 +280,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: GOOGLE_DRIVE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchGoogleDrive, applicationPortalUrl: 'https://console.developers.google.com/', }, objTypes: [ @@ -328,7 +313,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: true, - documentationUrl: JIRA_DOCS_URL, + documentationUrl: docLinks.workplaceSearchJiraCloud, applicationPortalUrl: 'https://developer.atlassian.com/console/myapps/', }, objTypes: [ @@ -364,7 +349,7 @@ export const staticSourceData = [ isPublicKey: true, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: JIRA_SERVER_DOCS_URL, + documentationUrl: docLinks.workplaceSearchJiraServer, applicationPortalUrl: '', }, objTypes: [ @@ -399,7 +384,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: ONEDRIVE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchOneDrive, applicationPortalUrl: 'https://portal.azure.com/', }, objTypes: [SOURCE_OBJ_TYPES.FOLDERS, SOURCE_OBJ_TYPES.ALL_FILES], @@ -428,7 +413,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: SALESFORCE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchSalesforce, applicationPortalUrl: 'https://salesforce.com/', }, objTypes: [ @@ -464,7 +449,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: SALESFORCE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchSalesforce, applicationPortalUrl: 'https://test.salesforce.com/', }, objTypes: [ @@ -500,7 +485,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: false, needsBaseUrl: true, - documentationUrl: SERVICENOW_DOCS_URL, + documentationUrl: docLinks.workplaceSearchServiceNow, applicationPortalUrl: 'https://www.servicenow.com/my-account/sign-in.html', }, objTypes: [ @@ -533,7 +518,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: SHAREPOINT_DOCS_URL, + documentationUrl: docLinks.workplaceSearchSharePoint, applicationPortalUrl: 'https://portal.azure.com/', }, objTypes: [SOURCE_OBJ_TYPES.FOLDERS, SOURCE_OBJ_TYPES.SITES, SOURCE_OBJ_TYPES.ALL_FILES], @@ -562,7 +547,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: SLACK_DOCS_URL, + documentationUrl: docLinks.workplaceSearchSlack, applicationPortalUrl: 'https://api.slack.com/apps/', }, objTypes: [ @@ -585,7 +570,7 @@ export const staticSourceData = [ hasOauthRedirect: true, needsBaseUrl: false, needsSubdomain: true, - documentationUrl: ZENDESK_DOCS_URL, + documentationUrl: docLinks.workplaceSearchZendesk, applicationPortalUrl: 'https://www.zendesk.com/login/', }, objTypes: [SOURCE_OBJ_TYPES.TICKETS], @@ -617,7 +602,7 @@ export const staticSourceData = [ defaultMessage: 'To create a Custom API Source, provide a human-readable and descriptive name. The name will appear as-is in the various search experiences and management interfaces.', }), - documentationUrl: CUSTOM_SOURCE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchCustomSources, applicationPortalUrl: '', }, accountContextOnly: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index be288ea208858..e7888175bb31a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -42,6 +42,8 @@ describe('SourceLogic', () => { 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/content_sources/sources_view.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx index 8697f10f8afaf..a7c981dad9103 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx @@ -24,9 +24,9 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { docLinks } from '../../../shared/doc_links'; import { Loading } from '../../../shared/loading'; import { SourceIcon } from '../../components/shared/source_icon'; -import { EXTERNAL_IDENTITIES_DOCS_URL, DOCUMENT_PERMISSIONS_DOCS_URL } from '../../routes'; import { EXTERNAL_IDENTITIES_LINK, @@ -82,7 +82,7 @@ export const SourcesView: React.FC = ({ children }) => { values={{ addedSourceName, externalIdentitiesLink: ( - + {EXTERNAL_IDENTITIES_LINK} ), @@ -96,7 +96,7 @@ export const SourcesView: React.FC = ({ children }) => { defaultMessage="Documents will not be searchable from Workplace Search until user and group mappings have been configured. {documentPermissionsLink}." values={{ documentPermissionsLink: ( - + {DOCUMENT_PERMISSIONS_LINK} ), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx index a8fcdfd7cb257..e4e14b19f1894 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx @@ -15,8 +15,10 @@ import { ErrorState } from './'; describe('ErrorState', () => { it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find(ErrorStatePrompt)).toHaveLength(1); + const prompt = wrapper.find(ErrorStatePrompt); + expect(prompt).toHaveLength(1); + expect(prompt.prop('errorConnectingMessage')).toEqual('I am an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx index 83ac3a26c44e5..493c37189ceb7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx @@ -15,7 +15,9 @@ import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kiban import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { ViewContentHeader } from '../../components/shared/view_content_header'; -export const ErrorState: React.FC = () => { +export const ErrorState: React.FC<{ errorConnectingMessage?: string }> = ({ + errorConnectingMessage, +}) => { return ( <> @@ -23,7 +25,7 @@ export const ErrorState: React.FC = () => { - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx index f7e578b1b4d23..c0362b44b618b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx @@ -12,6 +12,7 @@ import { useActions, useValues } from 'kea'; import { EuiSpacer } from '@elastic/eui'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { docLinks } from '../../../shared/doc_links'; import { RoleMappingsTable, RoleMappingsHeading, @@ -22,7 +23,6 @@ import { } from '../../../shared/role_mapping'; import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; import { WorkplaceSearchPageTemplate } from '../../components/layout'; -import { SECURITY_DOCS_URL } from '../../routes'; import { ROLE_MAPPINGS_TABLE_HEADER } from './constants'; @@ -56,7 +56,7 @@ export const RoleMappings: React.FC = () => { const rolesEmptyState = ( ); @@ -65,7 +65,7 @@ export const RoleMappings: React.FC = () => {
initializeRoleMapping()} /> { const updateButtons = ( - + {UPDATE_BUTTON} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx index b55828f78344c..22c484c1487ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx @@ -23,6 +23,7 @@ import { EuiText, } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; import { LicensingLogic } from '../../../../shared/licensing'; import { WorkplaceSearchPageTemplate } from '../../../components/layout'; import { ContentSection } from '../../../components/shared/content_section'; @@ -49,7 +50,6 @@ import { NON_PLATINUM_OAUTH_DESCRIPTION, EXPLORE_PLATINUM_FEATURES_LINK, } from '../../../constants'; -import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; import { SettingsLogic } from '../settings_logic'; export const OauthApplication: React.FC = () => { @@ -100,7 +100,7 @@ export const OauthApplication: React.FC = () => { <> {NON_PLATINUM_OAUTH_DESCRIPTION} - + {EXPLORE_PLATINUM_FEATURES_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx index 009dbffafebd8..3c3a7085d7116 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx @@ -12,14 +12,14 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { docLinks } from '../../../shared/doc_links'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import { GETTING_STARTED_DOCS_URL } from '../../routes'; import GettingStarted from './assets/getting_started.png'; -const GETTING_STARTED_LINK_URL = GETTING_STARTED_DOCS_URL; +const GETTING_STARTED_LINK_URL = docLinks.workplaceSearchGettingStarted; export const SetupGuide: React.FC = () => { return ( diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 19f2aa212d7fd..9a8ff64649f0e 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -39,6 +39,7 @@ export interface ClientConfigType { export interface ClientData extends InitialAppData { publicUrl?: string; errorConnecting?: boolean; + errorConnectingMessage?: string; } interface PluginsSetup { @@ -193,8 +194,9 @@ export class EnterpriseSearchPlugin implements Plugin { try { this.data = await http.get('/internal/enterprise_search/config_data'); this.hasInitialized = true; - } catch { + } catch (e) { this.data.errorConnecting = true; + this.data.errorConnectingMessage = `${e.res.status} ${e.message}`; } } } diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index f6e3280a8abb2..0a0a097da10aa 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -120,6 +120,7 @@ describe('callEnterpriseSearchConfigAPI', () => { expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({ ...DEFAULT_INITIAL_APP_DATA, + errorConnectingMessage: undefined, kibanaVersion: '1.0.0', access: { hasAppSearchAccess: true, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index 3070be1e56b5b..c9212bca322d7 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -28,7 +28,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler', + path: '/api/as/v1/engines/:name/crawler', }); }); @@ -61,7 +61,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests', + path: '/api/as/v1/engines/:name/crawler/crawl_requests', }); }); @@ -94,7 +94,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests/:id', + path: '/api/as/v1/engines/:name/crawler/crawl_requests/:id', }); }); @@ -132,7 +132,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests', + path: '/api/as/v1/engines/:name/crawler/crawl_requests', }); }); @@ -165,7 +165,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/domains', + path: '/api/as/v1/engines/:name/crawler/domains', }); }); @@ -204,7 +204,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests/active/cancel', + path: '/api/as/v1/engines/:name/crawler/crawl_requests/active/cancel', }); }); @@ -237,7 +237,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/domains', + path: '/api/as/v1/engines/:name/crawler/domains', }); }); @@ -293,7 +293,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/domains/:id', + path: '/api/as/v1/engines/:name/crawler/domains/:id', }); }); @@ -339,7 +339,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/domains/:id', + path: '/api/as/v1/engines/:name/crawler/domains/:id', }); }); @@ -397,7 +397,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/domains/:id', + path: '/api/as/v1/engines/:name/crawler/domains/:id', }); }); @@ -435,7 +435,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/crawler/validate_url', + path: '/api/as/v1/crawler/validate_url', }); }); @@ -472,7 +472,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/process_crawls', + path: '/api/as/v1/engines/:name/crawler/process_crawls', }); }); @@ -519,7 +519,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + path: '/api/as/v1/engines/:name/crawler/crawl_schedule', }); }); @@ -556,7 +556,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + path: '/api/as/v1/engines/:name/crawler/crawl_schedule', }); }); @@ -611,7 +611,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + path: '/api/as/v1/engines/:name/crawler/crawl_schedule', }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index f53b15dadd061..f0fdc5c16098b 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -23,7 +23,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler', + path: '/api/as/v1/engines/:name/crawler', }) ); @@ -37,7 +37,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests', + path: '/api/as/v1/engines/:name/crawler/crawl_requests', }) ); @@ -52,7 +52,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests/:id', + path: '/api/as/v1/engines/:name/crawler/crawl_requests/:id', }) ); @@ -66,7 +66,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests', + path: '/api/as/v1/engines/:name/crawler/crawl_requests', }) ); @@ -80,7 +80,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests/active/cancel', + path: '/api/as/v1/engines/:name/crawler/crawl_requests/active/cancel', }) ); @@ -98,7 +98,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/domains', + path: '/api/as/v1/engines/:name/crawler/domains', }) ); @@ -123,7 +123,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/domains', + path: '/api/as/v1/engines/:name/crawler/domains', }) ); @@ -138,7 +138,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/domains/:id', + path: '/api/as/v1/engines/:name/crawler/domains/:id', }) ); @@ -156,7 +156,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/domains/:id', + path: '/api/as/v1/engines/:name/crawler/domains/:id', }) ); @@ -183,7 +183,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/domains/:id', + path: '/api/as/v1/engines/:name/crawler/domains/:id', }) ); @@ -198,7 +198,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/crawler/validate_url', + path: '/api/as/v1/crawler/validate_url', }) ); @@ -215,7 +215,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/process_crawls', + path: '/api/as/v1/engines/:name/crawler/process_crawls', }) ); @@ -229,7 +229,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + path: '/api/as/v1/engines/:name/crawler/crawl_schedule', }) ); @@ -247,7 +247,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + path: '/api/as/v1/engines/:name/crawler/crawl_schedule', }) ); @@ -261,7 +261,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + path: '/api/as/v1/engines/:name/crawler/crawl_schedule', }) ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts index 018ab433536b2..c3d1468687ec4 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts @@ -28,7 +28,7 @@ describe('crawler crawl rules routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules', params: { respond_with: 'index', }, @@ -71,7 +71,7 @@ describe('crawler crawl rules routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', params: { respond_with: 'index', }, @@ -115,7 +115,7 @@ describe('crawler crawl rules routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', params: { respond_with: 'index', }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts index 7c82c73db7263..26637623f0885 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts @@ -29,7 +29,7 @@ export function registerCrawlerCrawlRulesRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules', params: { respond_with: 'index', }, @@ -54,7 +54,7 @@ export function registerCrawlerCrawlRulesRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', params: { respond_with: 'index', }, @@ -73,7 +73,7 @@ export function registerCrawlerCrawlRulesRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', params: { respond_with: 'index', }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts index 6fb7e99400877..dc7ad493a5149 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts @@ -28,7 +28,7 @@ describe('crawler entry point routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points', params: { respond_with: 'index', }, @@ -69,7 +69,7 @@ describe('crawler entry point routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', params: { respond_with: 'index', }, @@ -110,7 +110,7 @@ describe('crawler entry point routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', params: { respond_with: 'index', }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts index a6d6fdb24b41f..fd81475c860ad 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts @@ -27,7 +27,7 @@ export function registerCrawlerEntryPointRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points', params: { respond_with: 'index', }, @@ -49,7 +49,7 @@ export function registerCrawlerEntryPointRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', params: { respond_with: 'index', }, @@ -68,7 +68,7 @@ export function registerCrawlerEntryPointRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', params: { respond_with: 'index', }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts index a37a8311093c7..3d6eb86bcba26 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts @@ -28,7 +28,7 @@ describe('crawler sitemap routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps', params: { respond_with: 'index', }, @@ -69,7 +69,7 @@ describe('crawler sitemap routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', params: { respond_with: 'index', }, @@ -110,7 +110,7 @@ describe('crawler sitemap routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', params: { respond_with: 'index', }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts index b63473888eecc..0965acd967306 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts @@ -27,7 +27,7 @@ export function registerCrawlerSitemapRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps', params: { respond_with: 'index', }, @@ -49,7 +49,7 @@ export function registerCrawlerSitemapRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', params: { respond_with: 'index', }, @@ -68,7 +68,7 @@ export function registerCrawlerSitemapRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', params: { respond_with: 'index', }, 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/constants/preconfiguration.ts b/x-pack/plugins/fleet/common/constants/preconfiguration.ts index 3e7377477c93e..7d22716bc0f1e 100644 --- a/x-pack/plugins/fleet/common/constants/preconfiguration.ts +++ b/x-pack/plugins/fleet/common/constants/preconfiguration.ts @@ -6,6 +6,7 @@ */ import { uniqBy } from 'lodash'; +import uuidv5 from 'uuid/v5'; import type { PreconfiguredAgentPolicy } from '../types'; @@ -18,6 +19,9 @@ import { autoUpgradePoliciesPackages, } from './epm'; +// UUID v5 values require a namespace. We use UUID v5 for some of our preconfigured ID values. +export const UUID_V5_NAMESPACE = 'dde7c2de-1370-4c19-9975-b473d0e03508'; + export const PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE = 'fleet-preconfiguration-deletion-record'; @@ -25,17 +29,22 @@ export const PRECONFIGURATION_LATEST_KEYWORD = 'latest'; type PreconfiguredAgentPolicyWithDefaultInputs = Omit< PreconfiguredAgentPolicy, - 'package_policies' | 'id' + 'package_policies' > & { package_policies: Array>; }; +export const DEFAULT_AGENT_POLICY_ID_SEED = 'default-agent-policy'; +export const DEFAULT_SYSTEM_PACKAGE_POLICY_ID = 'default-system-policy'; + export const DEFAULT_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = { + id: uuidv5(DEFAULT_AGENT_POLICY_ID_SEED, UUID_V5_NAMESPACE), name: 'Default policy', namespace: 'default', description: 'Default agent policy created by Kibana', package_policies: [ { + id: DEFAULT_SYSTEM_PACKAGE_POLICY_ID, name: `${FLEET_SYSTEM_PACKAGE}-1`, package: { name: FLEET_SYSTEM_PACKAGE, @@ -47,12 +56,17 @@ export const DEFAULT_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = { monitoring_enabled: monitoringTypes, }; +export const DEFAULT_FLEET_SERVER_POLICY_ID = 'default-fleet-server-agent-policy'; +export const DEFAULT_FLEET_SERVER_AGENT_POLICY_ID_SEED = 'default-fleet-server'; + export const DEFAULT_FLEET_SERVER_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = { + id: uuidv5(DEFAULT_FLEET_SERVER_AGENT_POLICY_ID_SEED, UUID_V5_NAMESPACE), name: 'Default Fleet Server policy', namespace: 'default', description: 'Default Fleet Server agent policy created by Kibana', package_policies: [ { + id: DEFAULT_FLEET_SERVER_POLICY_ID, name: `${FLEET_SERVER_PACKAGE}-1`, package: { name: FLEET_SERVER_PACKAGE, diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index 9fc20bbf38eb7..69363f37d33e0 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -18,8 +18,8 @@ export const LIMITED_CONCURRENCY_ROUTE_TAG = 'ingest:limited-concurrency'; // EPM API routes const EPM_PACKAGES_MANY = `${EPM_API_ROOT}/packages`; const EPM_PACKAGES_BULK = `${EPM_PACKAGES_MANY}/_bulk`; -const EPM_PACKAGES_ONE = `${EPM_PACKAGES_MANY}/{pkgkey}`; -const EPM_PACKAGES_FILE = `${EPM_PACKAGES_MANY}/{pkgName}/{pkgVersion}`; +const EPM_PACKAGES_ONE_DEPRECATED = `${EPM_PACKAGES_MANY}/{pkgkey}`; +const EPM_PACKAGES_ONE = `${EPM_PACKAGES_MANY}/{pkgName}/{pkgVersion}`; export const EPM_API_ROUTES = { BULK_INSTALL_PATTERN: EPM_PACKAGES_BULK, LIST_PATTERN: EPM_PACKAGES_MANY, @@ -28,9 +28,13 @@ export const EPM_API_ROUTES = { INSTALL_FROM_REGISTRY_PATTERN: EPM_PACKAGES_ONE, INSTALL_BY_UPLOAD_PATTERN: EPM_PACKAGES_MANY, DELETE_PATTERN: EPM_PACKAGES_ONE, - FILEPATH_PATTERN: `${EPM_PACKAGES_FILE}/{filePath*}`, + FILEPATH_PATTERN: `${EPM_PACKAGES_ONE}/{filePath*}`, CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`, STATS_PATTERN: `${EPM_PACKAGES_MANY}/{pkgName}/stats`, + + INFO_PATTERN_DEPRECATED: EPM_PACKAGES_ONE_DEPRECATED, + INSTALL_FROM_REGISTRY_PATTERN_DEPRECATED: EPM_PACKAGES_ONE_DEPRECATED, + DELETE_PATTERN_DEPRECATED: EPM_PACKAGES_ONE_DEPRECATED, }; // Data stream API routes @@ -79,7 +83,9 @@ export const SETTINGS_API_ROUTES = { // App API routes export const APP_API_ROUTES = { CHECK_PERMISSIONS_PATTERN: `${API_ROOT}/check-permissions`, - GENERATE_SERVICE_TOKEN_PATTERN: `${API_ROOT}/service-tokens`, + GENERATE_SERVICE_TOKEN_PATTERN: `${API_ROOT}/service_tokens`, + // deprecated since 8.0 + GENERATE_SERVICE_TOKEN_PATTERN_DEPRECATED: `${API_ROOT}/service-tokens`, }; // Agent API routes @@ -95,16 +101,23 @@ export const AGENT_API_ROUTES = { BULK_UNENROLL_PATTERN: `${API_ROOT}/agents/bulk_unenroll`, REASSIGN_PATTERN: `${API_ROOT}/agents/{agentId}/reassign`, BULK_REASSIGN_PATTERN: `${API_ROOT}/agents/bulk_reassign`, - STATUS_PATTERN: `${API_ROOT}/agent-status`, + STATUS_PATTERN: `${API_ROOT}/agent_status`, + // deprecated since 8.0 + STATUS_PATTERN_DEPRECATED: `${API_ROOT}/agent-status`, UPGRADE_PATTERN: `${API_ROOT}/agents/{agentId}/upgrade`, BULK_UPGRADE_PATTERN: `${API_ROOT}/agents/bulk_upgrade`, }; export const ENROLLMENT_API_KEY_ROUTES = { - CREATE_PATTERN: `${API_ROOT}/enrollment-api-keys`, - LIST_PATTERN: `${API_ROOT}/enrollment-api-keys`, - INFO_PATTERN: `${API_ROOT}/enrollment-api-keys/{keyId}`, - DELETE_PATTERN: `${API_ROOT}/enrollment-api-keys/{keyId}`, + CREATE_PATTERN: `${API_ROOT}/enrollment_api_keys`, + LIST_PATTERN: `${API_ROOT}/enrollment_api_keys`, + INFO_PATTERN: `${API_ROOT}/enrollment_api_keys/{keyId}`, + DELETE_PATTERN: `${API_ROOT}/enrollment_api_keys/{keyId}`, + // deprecated since 8.0 + CREATE_PATTERN_DEPRECATED: `${API_ROOT}/enrollment-api-keys`, + LIST_PATTERN_DEPRECATED: `${API_ROOT}/enrollment-api-keys`, + INFO_PATTERN_DEPRECATED: `${API_ROOT}/enrollment-api-keys/{keyId}`, + DELETE_PATTERN_DEPRECATED: `${API_ROOT}/enrollment-api-keys/{keyId}`, }; // Agents setup API routes diff --git a/x-pack/plugins/fleet/common/constants/settings.ts b/x-pack/plugins/fleet/common/constants/settings.ts index 772d938086938..423e71edf10e6 100644 --- a/x-pack/plugins/fleet/common/constants/settings.ts +++ b/x-pack/plugins/fleet/common/constants/settings.ts @@ -6,3 +6,5 @@ */ export const GLOBAL_SETTINGS_SAVED_OBJECT_TYPE = 'ingest_manager_settings'; + +export const GLOBAL_SETTINGS_ID = 'fleet-default-settings'; diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 1e17c693e01b9..7423a4dc54bbe 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -157,6 +157,7 @@ "parameters": [] }, "/epm/packages/{pkgkey}": { + "deprecated": true, "get": { "summary": "Packages - Info", "tags": [], @@ -352,6 +353,210 @@ } } }, + "/epm/packages/{pkgName}/{pkgVersion}": { + "get": { + "summary": "Packages - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "allOf": [ + { + "properties": { + "item": { + "$ref": "#/components/schemas/package_info" + } + } + }, + { + "properties": { + "status": { + "type": "string", + "enum": [ + "installed", + "installing", + "install_failed", + "not_installed" + ] + }, + "savedObject": { + "type": "string" + } + }, + "required": [ + "status", + "savedObject" + ] + } + ] + } + } + } + } + }, + "operationId": "get-package", + "security": [ + { + "basicAuth": [] + } + ] + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "pkgName", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "pkgVersion", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Packages - Install", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "oneOf": [ + { + "$ref": "#/components/schemas/kibana_saved_object_type" + }, + { + "$ref": "#/components/schemas/elasticsearch_asset_type" + } + ] + } + }, + "required": [ + "id", + "type" + ] + } + } + }, + "required": [ + "items" + ] + } + } + } + } + }, + "operationId": "install-package", + "description": "", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "force": { + "type": "boolean" + } + } + } + } + } + } + }, + "delete": { + "summary": "Packages - Delete", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "oneOf": [ + { + "$ref": "#/components/schemas/kibana_saved_object_type" + }, + { + "$ref": "#/components/schemas/elasticsearch_asset_type" + } + ] + } + }, + "required": [ + "id", + "type" + ] + } + } + }, + "required": [ + "items" + ] + } + } + } + } + }, + "operationId": "delete-package", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "force": { + "type": "boolean" + } + } + } + } + } + } + } + }, "/agents/setup": { "get": { "summary": "Agents setup - Info", @@ -419,6 +624,72 @@ } }, "/agent-status": { + "deprecated": true, + "get": { + "summary": "Agents - Summary stats", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "integer" + }, + "events": { + "type": "integer" + }, + "inactive": { + "type": "integer" + }, + "offline": { + "type": "integer" + }, + "online": { + "type": "integer" + }, + "other": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "updating": { + "type": "integer" + } + }, + "required": [ + "error", + "events", + "inactive", + "offline", + "online", + "other", + "total", + "updating" + ] + } + } + } + } + }, + "operationId": "get-agent-status", + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "policyId", + "in": "query", + "required": false + } + ] + } + }, + "/agent_status": { "get": { "summary": "Agents - Summary stats", "tags": [], @@ -496,6 +767,13 @@ "type": "object", "properties": { "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/agent" + }, + "deprecated": true + }, + "items": { "type": "array", "items": { "$ref": "#/components/schemas/agent" @@ -512,7 +790,7 @@ } }, "required": [ - "list", + "items", "total", "page", "perPage" @@ -1294,6 +1572,7 @@ "parameters": [] }, "/enrollment-api-keys": { + "deprecated": true, "get": { "summary": "Enrollment API Keys - List", "tags": [], @@ -1306,6 +1585,13 @@ "type": "object", "properties": { "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/enrollment_api_key" + }, + "deprecated": true + }, + "items": { "type": "array", "items": { "$ref": "#/components/schemas/enrollment_api_key" @@ -1322,7 +1608,7 @@ } }, "required": [ - "list", + "items", "page", "perPage", "total" @@ -1370,6 +1656,160 @@ } }, "/enrollment-api-keys/{keyId}": { + "deprecated": true, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "keyId", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Enrollment API Key - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/enrollment_api_key" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "operationId": "get-enrollment-api-key" + }, + "delete": { + "summary": "Enrollment API Key - Delete", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "deleted" + ] + } + }, + "required": [ + "action" + ] + } + } + } + } + }, + "operationId": "delete-enrollment-api-key", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/enrollment_api_keys": { + "get": { + "summary": "Enrollment API Keys - List", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/enrollment_api_key" + }, + "deprecated": true + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/enrollment_api_key" + } + }, + "page": { + "type": "number" + }, + "perPage": { + "type": "number" + }, + "total": { + "type": "number" + } + }, + "required": [ + "items", + "page", + "perPage", + "total" + ] + } + } + } + } + }, + "operationId": "get-enrollment-api-keys", + "parameters": [] + }, + "post": { + "summary": "Enrollment API Key - Create", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/enrollment_api_key" + }, + "action": { + "type": "string", + "enum": [ + "created" + ] + } + } + } + } + } + } + }, + "operationId": "create-enrollment-api-keys", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/enrollment_api_keys/{keyId}": { "parameters": [ { "schema": { @@ -2520,10 +2960,6 @@ "unenrollment_started_at": { "type": "string" }, - "shared_id": { - "type": "string", - "deprecated": true - }, "access_api_key_id": { "type": "string" }, @@ -2673,8 +3109,7 @@ }, "required": [ "name", - "version", - "title" + "version" ] }, "namespace": { @@ -2713,8 +3148,7 @@ }, "required": [ "type", - "enabled", - "streams" + "enabled" ] } }, @@ -2729,9 +3163,7 @@ } }, "required": [ - "output_id", "inputs", - "policy_id", "name" ] }, @@ -2858,19 +3290,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..13ffa77279c21 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -100,6 +100,7 @@ paths: operationId: list-all-packages parameters: [] /epm/packages/{pkgkey}: + deprecated: true get: summary: Packages - Info tags: [] @@ -213,6 +214,125 @@ paths: properties: force: type: boolean + /epm/packages/{pkgName}/{pkgVersion}: + get: + summary: Packages - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + allOf: + - properties: + item: + $ref: '#/components/schemas/package_info' + - properties: + status: + type: string + enum: + - installed + - installing + - install_failed + - not_installed + savedObject: + type: string + required: + - status + - savedObject + operationId: get-package + security: + - basicAuth: [] + parameters: + - schema: + type: string + name: pkgName + in: path + required: true + - schema: + type: string + name: pkgVersion + in: path + required: true + post: + summary: Packages - Install + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + type: object + properties: + id: + type: string + type: + oneOf: + - $ref: '#/components/schemas/kibana_saved_object_type' + - $ref: '#/components/schemas/elasticsearch_asset_type' + required: + - id + - type + required: + - items + operationId: install-package + description: '' + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: boolean + delete: + summary: Packages - Delete + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + type: object + properties: + id: + type: string + type: + oneOf: + - $ref: '#/components/schemas/kibana_saved_object_type' + - $ref: '#/components/schemas/elasticsearch_asset_type' + required: + - id + - type + required: + - items + operationId: delete-package + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: boolean /agents/setup: get: summary: Agents setup - Info @@ -253,6 +373,51 @@ paths: parameters: - $ref: '#/components/parameters/kbn_xsrf' /agent-status: + deprecated: true + get: + summary: Agents - Summary stats + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + error: + type: integer + events: + type: integer + inactive: + type: integer + offline: + type: integer + online: + type: integer + other: + type: integer + total: + type: integer + updating: + type: integer + required: + - error + - events + - inactive + - offline + - online + - other + - total + - updating + operationId: get-agent-status + parameters: + - schema: + type: string + name: policyId + in: query + required: false + /agent_status: get: summary: Agents - Summary stats tags: [] @@ -312,6 +477,11 @@ paths: type: array items: $ref: '#/components/schemas/agent' + deprecated: true + items: + type: array + items: + $ref: '#/components/schemas/agent' total: type: number page: @@ -319,7 +489,7 @@ paths: perPage: type: number required: - - list + - items - total - page - perPage @@ -784,6 +954,7 @@ paths: - $ref: '#/components/parameters/kbn_xsrf' parameters: [] /enrollment-api-keys: + deprecated: true get: summary: Enrollment API Keys - List tags: [] @@ -799,6 +970,11 @@ paths: type: array items: $ref: '#/components/schemas/enrollment_api_key' + deprecated: true + items: + type: array + items: + $ref: '#/components/schemas/enrollment_api_key' page: type: number perPage: @@ -806,7 +982,7 @@ paths: total: type: number required: - - list + - items - page - perPage - total @@ -833,6 +1009,104 @@ paths: parameters: - $ref: '#/components/parameters/kbn_xsrf' /enrollment-api-keys/{keyId}: + deprecated: true + parameters: + - schema: + type: string + name: keyId + in: path + required: true + get: + summary: Enrollment API Key - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/enrollment_api_key' + required: + - item + operationId: get-enrollment-api-key + delete: + summary: Enrollment API Key - Delete + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + enum: + - deleted + required: + - action + operationId: delete-enrollment-api-key + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + /enrollment_api_keys: + get: + summary: Enrollment API Keys - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + list: + type: array + items: + $ref: '#/components/schemas/enrollment_api_key' + deprecated: true + items: + type: array + items: + $ref: '#/components/schemas/enrollment_api_key' + page: + type: number + perPage: + type: number + total: + type: number + required: + - items + - page + - perPage + - total + operationId: get-enrollment-api-keys + parameters: [] + post: + summary: Enrollment API Key - Create + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/enrollment_api_key' + action: + type: string + enum: + - created + operationId: create-enrollment-api-keys + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + /enrollment_api_keys/{keyId}: parameters: - schema: type: string @@ -1582,9 +1856,6 @@ components: type: string unenrollment_started_at: type: string - shared_id: - type: string - deprecated: true access_api_key_id: type: string default_api_key_id: @@ -1683,7 +1954,6 @@ components: required: - name - version - - title namespace: type: string output_id: @@ -1711,7 +1981,6 @@ components: required: - type - enabled - - streams policy_id: type: string name: @@ -1719,9 +1988,7 @@ components: description: type: string required: - - output_id - inputs - - policy_id - name package_policy: title: Package policy @@ -1801,12 +2068,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/agent.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/agent.yaml index c21651ca7f8be..72679dd1dab64 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/agent.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/agent.yaml @@ -11,9 +11,6 @@ properties: type: string unenrollment_started_at: type: string - shared_id: - type: string - deprecated: true access_api_key_id: type: string default_api_key_id: 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/output.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/output.yaml index e695f0048e6ad..a91ec2cb14e94 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/output.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/output.yaml @@ -18,6 +18,8 @@ properties: type: string ca_sha256: type: string + ca_trusted_fingerprint: + type: string api_key: type: string config: 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/common/openapi/entrypoint.yaml b/x-pack/plugins/fleet/common/openapi/entrypoint.yaml index 5495f2b3ccacf..8dbf54582299a 100644 --- a/x-pack/plugins/fleet/common/openapi/entrypoint.yaml +++ b/x-pack/plugins/fleet/common/openapi/entrypoint.yaml @@ -25,11 +25,17 @@ paths: $ref: paths/epm@packages.yaml '/epm/packages/{pkgkey}': $ref: 'paths/epm@packages@{pkgkey}.yaml' + deprecated: true + '/epm/packages/{pkgName}/{pkgVersion}': + $ref: 'paths/epm@packages@{pkg_name}@{pkg_version}.yaml' # Agent-related endpoints /agents/setup: $ref: paths/agents@setup.yaml /agent-status: $ref: paths/agent_status.yaml + deprecated: true + /agent_status: + $ref: paths/agent_status.yaml /agents: $ref: paths/agents.yaml /agents/bulk_upgrade: @@ -56,7 +62,13 @@ paths: $ref: paths/agent_policies@delete.yaml /enrollment-api-keys: $ref: paths/enrollment_api_keys.yaml + deprecated: true '/enrollment-api-keys/{keyId}': + $ref: 'paths/enrollment_api_keys@{key_id}.yaml' + deprecated: true + /enrollment_api_keys: + $ref: paths/enrollment_api_keys.yaml + '/enrollment_api_keys/{keyId}': $ref: 'paths/enrollment_api_keys@{key_id}.yaml' /package_policies: $ref: paths/package_policies.yaml diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents.yaml index 4a217eda5c5ed..19ea27956dac3 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents.yaml @@ -13,6 +13,11 @@ get: type: array items: $ref: ../components/schemas/agent.yaml + deprecated: true + items: + type: array + items: + $ref: ../components/schemas/agent.yaml total: type: number page: @@ -20,7 +25,7 @@ get: perPage: type: number required: - - list + - items - total - page - perPage diff --git a/x-pack/plugins/fleet/common/openapi/paths/enrollment_api_keys.yaml b/x-pack/plugins/fleet/common/openapi/paths/enrollment_api_keys.yaml index 6cfbede4a7ead..9f6ac6de0ebd6 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/enrollment_api_keys.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/enrollment_api_keys.yaml @@ -13,6 +13,11 @@ get: type: array items: $ref: ../components/schemas/enrollment_api_key.yaml + deprecated: true + items: + type: array + items: + $ref: ../components/schemas/enrollment_api_key.yaml page: type: number perPage: @@ -20,7 +25,7 @@ get: total: type: number required: - - list + - items - page - perPage - total diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml new file mode 100644 index 0000000000000..1c3c92d99ab38 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml @@ -0,0 +1,118 @@ +get: + summary: Packages - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + allOf: + - properties: + item: + $ref: ../components/schemas/package_info.yaml + - properties: + status: + type: string + enum: + - installed + - installing + - install_failed + - not_installed + savedObject: + type: string + required: + - status + - savedObject + operationId: get-package + security: + - basicAuth: [] +parameters: + - schema: + type: string + name: pkgName + in: path + required: true + - schema: + type: string + name: pkgVersion + in: path + required: true +post: + summary: Packages - Install + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + type: object + properties: + id: + type: string + type: + oneOf: + - $ref: ../components/schemas/kibana_saved_object_type.yaml + - $ref: ../components/schemas/elasticsearch_asset_type.yaml + required: + - id + - type + required: + - items + operationId: install-package + description: '' + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: boolean +delete: + summary: Packages - Delete + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + type: object + properties: + id: + type: string + type: + oneOf: + - $ref: ../components/schemas/kibana_saved_object_type.yaml + - $ref: ../components/schemas/elasticsearch_asset_type.yaml + required: + - id + - type + required: + - items + operationId: delete-package + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: boolean diff --git a/x-pack/plugins/fleet/common/openapi/paths/outputs@{output_id}.yaml b/x-pack/plugins/fleet/common/openapi/paths/outputs@{output_id}.yaml index 326a65692a03b..d70c78dd7de56 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/outputs@{output_id}.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/outputs@{output_id}.yaml @@ -61,6 +61,8 @@ put: type: string ca_sha256: type: string + ca_trusted_fingerprint: + type: string config_yaml: type: string required: diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index ba3fb44753643..7698308270fff 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -34,3 +34,4 @@ export { } from './validate_package_policy'; export { normalizeHostsForAgents } from './hosts_utils'; +export { splitPkgKey } from './split_pkg_key'; diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 8ab02c462cfa4..d7954aff70dd2 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -33,8 +33,11 @@ export const epmRouteService = { return EPM_API_ROUTES.LIMITED_LIST_PATTERN; }, - getInfoPath: (pkgkey: string) => { - return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgkey}', pkgkey); + getInfoPath: (pkgName: string, pkgVersion: string) => { + return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgName}', pkgName).replace( + '{pkgVersion}', + pkgVersion + ); }, getStatsPath: (pkgName: string) => { @@ -45,23 +48,27 @@ export const epmRouteService = { return `${EPM_API_ROOT}${filePath.replace('/package', '/packages')}`; }, - getInstallPath: (pkgkey: string) => { - return EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN.replace('{pkgkey}', pkgkey).replace( - /\/$/, - '' - ); // trim trailing slash + getInstallPath: (pkgName: string, pkgVersion: string) => { + return EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN.replace('{pkgName}', pkgName) + .replace('{pkgVersion}', pkgVersion) + .replace(/\/$/, ''); // trim trailing slash }, getBulkInstallPath: () => { return EPM_API_ROUTES.BULK_INSTALL_PATTERN; }, - getRemovePath: (pkgkey: string) => { - return EPM_API_ROUTES.DELETE_PATTERN.replace('{pkgkey}', pkgkey).replace(/\/$/, ''); // trim trailing slash + getRemovePath: (pkgName: string, pkgVersion: string) => { + return EPM_API_ROUTES.DELETE_PATTERN.replace('{pkgName}', pkgName) + .replace('{pkgVersion}', pkgVersion) + .replace(/\/$/, ''); // trim trailing slash }, - getUpdatePath: (pkgkey: string) => { - return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgkey}', pkgkey); + getUpdatePath: (pkgName: string, pkgVersion: string) => { + return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgName}', pkgName).replace( + '{pkgVersion}', + pkgVersion + ); }, }; diff --git a/x-pack/plugins/fleet/common/services/split_pkg_key.ts b/x-pack/plugins/fleet/common/services/split_pkg_key.ts new file mode 100644 index 0000000000000..8bbc5b37a2e41 --- /dev/null +++ b/x-pack/plugins/fleet/common/services/split_pkg_key.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import semverValid from 'semver/functions/valid'; + +/** + * Extract the package name and package version from a string. + * + * @param pkgkey a string containing the package name delimited by the package version + */ +export function splitPkgKey(pkgkey: string): { pkgName: string; pkgVersion: string } { + // If no version is provided, use the provided package key as the + // package name and return an empty version value + if (!pkgkey.includes('-')) { + return { pkgName: pkgkey, pkgVersion: '' }; + } + + const pkgName = pkgkey.includes('-') ? pkgkey.substr(0, pkgkey.indexOf('-')) : pkgkey; + + if (pkgName === '') { + throw new Error('Package key parsing failed: package name was empty'); + } + + // this will return the entire string if `indexOf` return -1 + const pkgVersion = pkgkey.substr(pkgkey.indexOf('-') + 1); + if (!semverValid(pkgVersion)) { + throw new Error('Package key parsing failed: package version was not a valid semver'); + } + return { pkgName, pkgVersion }; +} diff --git a/x-pack/plugins/fleet/common/types/models/output.ts b/x-pack/plugins/fleet/common/types/models/output.ts index fada8171b91fc..2ff50c0fc7bdb 100644 --- a/x-pack/plugins/fleet/common/types/models/output.ts +++ b/x-pack/plugins/fleet/common/types/models/output.ts @@ -17,6 +17,7 @@ export interface NewOutput { type: ValueOf; hosts?: string[]; ca_sha256?: string; + ca_trusted_fingerprint?: string; api_key?: string; config_yaml?: string; is_preconfigured?: boolean; diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index df484646ef66b..75932fd4a790a 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -56,6 +56,7 @@ export interface PackagePolicyInput extends Omit> & { + id?: string | number; name: string; package: Partial & { name: string }; inputs?: InputsOverride[]; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index e6da9d4498ce2..5e091b9c543f2 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -7,22 +7,19 @@ import type { Agent, AgentAction, NewAgentAction } from '../models'; +import type { ListResult, ListWithKuery } from './common'; + export interface GetAgentsRequest { - query: { - page: number; - perPage: number; - kuery?: string; + query: ListWithKuery & { showInactive: boolean; showUpgradeable?: boolean; }; } -export interface GetAgentsResponse { - list: Agent[]; - total: number; +export interface GetAgentsResponse extends ListResult { totalInactive: number; - page: number; - perPage: number; + // deprecated in 8.x + list?: Agent[]; } export interface GetOneAgentRequest { diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts index 0975b1e28fb8b..cbf3c9806d388 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts @@ -7,7 +7,7 @@ import type { AgentPolicy, NewAgentPolicy, FullAgentPolicy } from '../models'; -import type { ListWithKuery } from './common'; +import type { ListResult, ListWithKuery } from './common'; export interface GetAgentPoliciesRequest { query: ListWithKuery & { @@ -17,12 +17,7 @@ export interface GetAgentPoliciesRequest { export type GetAgentPoliciesResponseItem = AgentPolicy & { agents?: number }; -export interface GetAgentPoliciesResponse { - items: GetAgentPoliciesResponseItem[]; - total: number; - page: number; - perPage: number; -} +export type GetAgentPoliciesResponse = ListResult; export interface GetOneAgentPolicyRequest { params: { diff --git a/x-pack/plugins/fleet/common/types/rest_spec/enrollment_api_key.ts b/x-pack/plugins/fleet/common/types/rest_spec/enrollment_api_key.ts index da870deb31d9c..7fa724e5079c8 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/enrollment_api_key.ts @@ -7,20 +7,16 @@ import type { EnrollmentAPIKey } from '../models'; +import type { ListResult, ListWithKuery } from './common'; + export interface GetEnrollmentAPIKeysRequest { - query: { - page: number; - perPage: number; - kuery?: string; - }; + query: ListWithKuery; } -export interface GetEnrollmentAPIKeysResponse { - list: EnrollmentAPIKey[]; - total: number; - page: number; - perPage: number; -} +export type GetEnrollmentAPIKeysResponse = ListResult & { + // deprecated in 8.x + list?: EnrollmentAPIKey[]; +}; export interface GetOneEnrollmentAPIKeyRequest { params: { diff --git a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts index cfe0b4abdcd3c..6a72792e780ef 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts @@ -22,7 +22,9 @@ export interface GetCategoriesRequest { } export interface GetCategoriesResponse { - response: CategorySummaryList; + items: CategorySummaryList; + // deprecated in 8.0 + response?: CategorySummaryList; } export interface GetPackagesRequest { @@ -33,33 +35,46 @@ export interface GetPackagesRequest { } export interface GetPackagesResponse { - response: PackageList; + items: PackageList; + // deprecated in 8.0 + response?: PackageList; } export interface GetLimitedPackagesResponse { - response: string[]; + items: string[]; + // deprecated in 8.0 + response?: string[]; } export interface GetFileRequest { params: { - pkgkey: string; + pkgName: string; + pkgVersion: string; filePath: string; }; } export interface GetInfoRequest { params: { - pkgkey: string; + // deprecated in 8.0 + pkgkey?: string; + pkgName: string; + pkgVersion: string; }; } export interface GetInfoResponse { - response: PackageInfo; + item: PackageInfo; + // deprecated in 8.0 + response?: PackageInfo; } export interface UpdatePackageRequest { params: { - pkgkey: string; + // deprecated in 8.0 + pkgkey?: string; + pkgName: string; + pkgVersion: string; }; body: { keepPoliciesUpToDate?: boolean; @@ -67,7 +82,9 @@ export interface UpdatePackageRequest { } export interface UpdatePackageResponse { - response: PackageInfo; + item: PackageInfo; + // deprecated in 8.0 + response?: PackageInfo; } export interface GetStatsRequest { @@ -82,12 +99,17 @@ export interface GetStatsResponse { export interface InstallPackageRequest { params: { - pkgkey: string; + // deprecated in 8.0 + pkgkey?: string; + pkgName: string; + pkgVersion: string; }; } export interface InstallPackageResponse { - response: AssetReference[]; + items: AssetReference[]; + // deprecated in 8.0 + response?: AssetReference[]; } export interface IBulkInstallPackageHTTPError { @@ -110,7 +132,9 @@ export interface BulkInstallPackageInfo { } export interface BulkInstallPackagesResponse { - response: Array; + items: Array; + // deprecated in 8.0 + response?: Array; } export interface BulkInstallPackagesRequest { @@ -125,10 +149,15 @@ export interface MessageResponse { export interface DeletePackageRequest { params: { - pkgkey: string; + // deprecated in 8.0 + pkgkey?: string; + pkgName: string; + pkgVersion: string; }; } export interface DeletePackageResponse { - response: AssetReference[]; + // deprecated in 8.0 + response?: AssetReference[]; + items: AssetReference[]; } diff --git a/x-pack/plugins/fleet/common/types/rest_spec/output.ts b/x-pack/plugins/fleet/common/types/rest_spec/output.ts index 4e380feeb83a8..9a5001a3af10b 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/output.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/output.ts @@ -7,6 +7,8 @@ import type { Output } from '../models'; +import type { ListResult } from './common'; + export interface GetOneOutputResponse { item: Output; } @@ -30,6 +32,7 @@ export interface PutOutputRequest { name?: string; hosts?: string[]; ca_sha256?: string; + ca_trusted_fingerprint?: string; config_yaml?: string; is_default?: boolean; is_default_monitoring?: boolean; @@ -43,6 +46,7 @@ export interface PostOutputRequest { name: string; hosts?: string[]; ca_sha256?: string; + ca_trusted_fingerprint?: string; is_default?: boolean; is_default_monitoring?: boolean; config_yaml?: string; @@ -53,9 +57,4 @@ export interface PutOutputResponse { item: Output; } -export interface GetOutputsResponse { - items: Output[]; - total: number; - page: number; - perPage: number; -} +export type GetOutputsResponse = ListResult; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts index b050a7c798a0b..9eb20383d57bd 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts @@ -13,20 +13,13 @@ import type { PackagePolicyPackage, } from '../models'; +import type { ListResult, ListWithKuery } from './common'; + export interface GetPackagePoliciesRequest { - query: { - page: number; - perPage: number; - kuery?: string; - }; + query: ListWithKuery; } -export interface GetPackagePoliciesResponse { - items: PackagePolicy[]; - total: number; - page: number; - perPage: number; -} +export type GetPackagePoliciesResponse = ListResult; export interface GetOnePackagePolicyRequest { params: { diff --git a/x-pack/plugins/fleet/cypress/fixtures/integrations/apache.json b/x-pack/plugins/fleet/cypress/fixtures/integrations/apache.json index 3b78048fdd83f..397bc6d653409 100644 --- a/x-pack/plugins/fleet/cypress/fixtures/integrations/apache.json +++ b/x-pack/plugins/fleet/cypress/fixtures/integrations/apache.json @@ -1,5 +1,5 @@ { - "response": { + "item": { "name": "apache", "title": "Apache", "version": "1.1.0", diff --git a/x-pack/plugins/fleet/cypress/integration/integrations.spec.ts b/x-pack/plugins/fleet/cypress/integration/integrations.spec.ts index 88769ece39f2f..8b1a5e97279e8 100644 --- a/x-pack/plugins/fleet/cypress/integration/integrations.spec.ts +++ b/x-pack/plugins/fleet/cypress/integration/integrations.spec.ts @@ -88,7 +88,7 @@ describe('Add Integration', () => { fixture: 'integrations/agent_policy.json', }); // TODO fixture includes 1 package policy, should be empty initially - cy.intercept('GET', '/api/fleet/epm/packages/apache-1.1.0', { + cy.intercept('GET', '/api/fleet/epm/packages/apache/1.1.0', { fixture: 'integrations/apache.json', }); addAndVerifyIntegration(); diff --git a/x-pack/plugins/fleet/cypress/tasks/integrations.ts b/x-pack/plugins/fleet/cypress/tasks/integrations.ts index f1c891fa1186c..e9e3f2613c3e8 100644 --- a/x-pack/plugins/fleet/cypress/tasks/integrations.ts +++ b/x-pack/plugins/fleet/cypress/tasks/integrations.ts @@ -50,7 +50,7 @@ export const deleteIntegrations = async (integration: string) => { export const installPackageWithVersion = (integration: string, version: string) => { cy.request({ - url: `/api/fleet/epm/packages/${integration}-${version}`, + url: `/api/fleet/epm/packages/${integration}/${version}`, headers: { 'kbn-xsrf': 'cypress' }, body: '{ "force": true }', method: 'POST', diff --git a/x-pack/plugins/fleet/dev_docs/api/epm.md b/x-pack/plugins/fleet/dev_docs/api/epm.md index 90b636a5a92a1..1588e228c438b 100644 --- a/x-pack/plugins/fleet/dev_docs/api/epm.md +++ b/x-pack/plugins/fleet/dev_docs/api/epm.md @@ -14,11 +14,11 @@ curl localhost:5601/api/fleet/epm/packages Install a package: ``` -curl -X POST localhost:5601/api/fleet/epm/packages/iptables-1.0.4 +curl -X POST localhost:5601/api/fleet/epm/packages/iptables/1.0.4 ``` Delete a package: ``` -curl -X DELETE localhost:5601/api/fleet/epm/packages/iptables-1.0.4 +curl -X DELETE localhost:5601/api/fleet/epm/packages/iptables/1.0.4 ``` diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index 4f1211a83ebba..c731936c775e5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -24,6 +24,7 @@ import { import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; import { safeLoad } from 'js-yaml'; +import { splitPkgKey } from '../../../../../../common'; import type { AgentPolicy, NewPackagePolicy, @@ -152,15 +153,16 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { // Form state const [formState, setFormState] = useState('VALID'); + const { pkgName, pkgVersion } = splitPkgKey(params.pkgkey); // Fetch package info const { data: packageInfoData, error: packageInfoError, isLoading: isPackageInfoLoading, - } = useGetPackageInfoByKey(params.pkgkey); + } = useGetPackageInfoByKey(pkgName, pkgVersion); const packageInfo = useMemo(() => { - if (packageInfoData && packageInfoData.response) { - return packageInfoData.response; + if (packageInfoData && packageInfoData.item) { + return packageInfoData.item; } }, [packageInfoData]); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index c0914e41872b1..8d7ac07867605 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -213,15 +213,16 @@ export const EditPackagePolicyForm = memo<{ } const { data: packageData } = await sendGetPackageInfoByKey( - pkgKeyFromPackageInfo(_packageInfo!) + _packageInfo!.name, + _packageInfo!.version ); - if (packageData?.response) { - setPackageInfo(packageData.response); + if (packageData?.item) { + setPackageInfo(packageData.item); const newValidationResults = validatePackagePolicy( newPackagePolicy, - packageData.response, + packageData.item, safeLoad ); setValidationResults(newValidationResults); @@ -348,7 +349,8 @@ export const EditPackagePolicyForm = memo<{ const [formState, setFormState] = useState('INVALID'); const savePackagePolicy = async () => { setFormState('LOADING'); - const result = await sendUpdatePackagePolicy(packagePolicyId, packagePolicy); + const { elasticsearch, ...restPackagePolicy } = packagePolicy; // ignore 'elasticsearch' property since it fails route validation + const result = await sendUpdatePackagePolicy(packagePolicyId, restPackagePolicy); setFormState('SUBMITTED'); return result; }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 0dbe947369ad3..c64d065c1e058 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -241,7 +241,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { .join(' or '); if (kueryBuilder) { - kueryBuilder = `(${kueryBuilder}) and ${kueryStatus}`; + kueryBuilder = `(${kueryBuilder}) and (${kueryStatus})`; } else { kueryBuilder = kueryStatus; } @@ -308,7 +308,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { inactive: agentsRequest.data.totalInactive, }); - setAgents(agentsRequest.data.list); + setAgents(agentsRequest.data.items); setTotalAgents(agentsRequest.data.total); setTotalInactiveAgents(agentsRequest.data.totalInactive); } catch (error) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx index 4efff98fe39b2..b2eaf904ee1bb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx @@ -245,6 +245,7 @@ export const useFleetServerInstructions = (policyId?: string) => { const { data: settings, resendRequest: refreshSettings } = useGetSettings(); const fleetServerHost = settings?.item.fleet_server_hosts?.[0]; const esHost = output?.hosts?.[0]; + const sslCATrustedFingerprint: string | undefined = output?.ca_trusted_fingerprint; const installCommand = useMemo((): string => { if (!serviceToken || !esHost) { @@ -257,9 +258,18 @@ export const useFleetServerInstructions = (policyId?: string) => { serviceToken, policyId, fleetServerHost, - deploymentMode === 'production' + deploymentMode === 'production', + sslCATrustedFingerprint ); - }, [serviceToken, esHost, platform, policyId, fleetServerHost, deploymentMode]); + }, [ + serviceToken, + esHost, + platform, + policyId, + fleetServerHost, + deploymentMode, + sslCATrustedFingerprint, + ]); const getServiceToken = useCallback(async () => { setIsLoadingServiceToken(true); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts index 62580a1445f06..d05107e5058d4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts @@ -17,7 +17,7 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo ./elastic-agent install \\\\ + "sudo ./elastic-agent install \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1" `); @@ -31,7 +31,7 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - ".\\\\elastic-agent.exe install \` + ".\\\\elastic-agent.exe install \` --fleet-server-es=http://elasticsearch:9200 \` --fleet-server-service-token=service-token-1" `); @@ -45,11 +45,30 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo elastic-agent enroll \\\\ + "sudo elastic-agent enroll \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1" `); }); + + it('should return the correct command sslCATrustedFingerprint option is passed', () => { + const res = getInstallCommandForPlatform( + 'linux-mac', + 'http://elasticsearch:9200', + 'service-token-1', + undefined, + undefined, + false, + 'fingerprint123456' + ); + + expect(res).toMatchInlineSnapshot(` + "sudo ./elastic-agent install \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ + --fleet-server-es-ca-trusted-fingerprint=fingerprint123456" + `); + }); }); describe('with policy id', () => { @@ -62,7 +81,7 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo ./elastic-agent install \\\\ + "sudo ./elastic-agent install \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ --fleet-server-policy=policy-1" @@ -78,7 +97,7 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - ".\\\\elastic-agent.exe install \` + ".\\\\elastic-agent.exe install \` --fleet-server-es=http://elasticsearch:9200 \` --fleet-server-service-token=service-token-1 \` --fleet-server-policy=policy-1" @@ -94,7 +113,7 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo elastic-agent enroll \\\\ + "sudo elastic-agent enroll \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ --fleet-server-policy=policy-1" @@ -178,7 +197,7 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo elastic-agent enroll \\\\ + "sudo elastic-agent enroll \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1" `); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts index f5c40e8071691..64ae4903af53f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts @@ -13,37 +13,49 @@ export function getInstallCommandForPlatform( serviceToken: string, policyId?: string, fleetServerHost?: string, - isProductionDeployment?: boolean + isProductionDeployment?: boolean, + sslCATrustedFingerprint?: string ) { - let commandArguments = ''; - const newLineSeparator = platform === 'windows' ? '`' : '\\'; + const commandArguments = []; + const newLineSeparator = platform === 'windows' ? '`\n' : '\\\n'; if (isProductionDeployment && fleetServerHost) { - commandArguments += `--url=${fleetServerHost} ${newLineSeparator}\n`; - } else { - commandArguments += ` ${newLineSeparator}\n`; + commandArguments.push(['url', fleetServerHost]); } - commandArguments += ` --fleet-server-es=${esHost}`; - commandArguments += ` ${newLineSeparator}\n --fleet-server-service-token=${serviceToken}`; + commandArguments.push(['fleet-server-es', esHost]); + commandArguments.push(['fleet-server-service-token', serviceToken]); if (policyId) { - commandArguments += ` ${newLineSeparator}\n --fleet-server-policy=${policyId}`; + commandArguments.push(['fleet-server-policy', policyId]); + } + + if (sslCATrustedFingerprint) { + commandArguments.push(['fleet-server-es-ca-trusted-fingerprint', sslCATrustedFingerprint]); } if (isProductionDeployment) { - commandArguments += ` ${newLineSeparator}\n --certificate-authorities=`; - commandArguments += ` ${newLineSeparator}\n --fleet-server-es-ca=`; - commandArguments += ` ${newLineSeparator}\n --fleet-server-cert=`; - commandArguments += ` ${newLineSeparator}\n --fleet-server-cert-key=`; + commandArguments.push(['certificate-authorities', '']); + if (!sslCATrustedFingerprint) { + commandArguments.push(['fleet-server-es-ca', '']); + } + commandArguments.push(['fleet-server-cert', '']); + commandArguments.push(['fleet-server-cert-key', '']); } + const commandArgumentsStr = commandArguments.reduce((acc, [key, val]) => { + if (acc === '' && key === 'url') { + return `--${key}=${val}`; + } + return (acc += ` ${newLineSeparator} --${key}=${val}`); + }, ''); + switch (platform) { case 'linux-mac': - return `sudo ./elastic-agent install ${commandArguments}`; + return `sudo ./elastic-agent install ${commandArgumentsStr}`; case 'windows': - return `.\\elastic-agent.exe install ${commandArguments}`; + return `.\\elastic-agent.exe install ${commandArgumentsStr}`; case 'rpm-deb': - return `sudo elastic-agent enroll ${commandArguments}`; + return `sudo elastic-agent enroll ${commandArgumentsStr}`; default: return ''; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/fleet_server_upgrade_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/fleet_server_upgrade_modal.tsx index 2d963ea0ddf30..5902f73cae3bc 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/fleet_server_upgrade_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/fleet_server_upgrade_modal.tsx @@ -63,7 +63,7 @@ export const FleetServerUpgradeModal: React.FunctionComponent = ({ onClos throw res.error; } - for (const agent of res.data?.list ?? []) { + for (const agent of res.data?.items ?? []) { if (!agent.policy_id || agentPoliciesAlreadyChecked[agent.policy_id]) { continue; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx index b8b66b42b533d..72160fb4ae897 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx @@ -182,7 +182,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { const total = enrollmentAPIKeysRequest?.data?.total ?? 0; const rowItems = - enrollmentAPIKeysRequest?.data?.list.filter((enrollmentKey) => { + enrollmentAPIKeysRequest?.data?.items.filter((enrollmentKey) => { if (!agentPolicies.length || !enrollmentKey.policy_id) return false; const agentPolicy = agentPoliciesById[enrollmentKey.policy_id]; return !agentPolicy?.is_managed; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_fleet_server_unhealthy.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_fleet_server_unhealthy.test.tsx index 87a269672ed9c..a36b4fb25793f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_fleet_server_unhealthy.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_fleet_server_unhealthy.test.tsx @@ -37,7 +37,7 @@ const mockApiCallsWithHealthyFleetServer = (http: MockedFleetStartServices['http }; } - if (path === '/api/fleet/agent-status') { + if (path === '/api/fleet/agent_status') { return { data: { results: { online: 1, updating: 0, offline: 0 }, @@ -65,7 +65,7 @@ const mockApiCallsWithoutHealthyFleetServer = (http: MockedFleetStartServices['h }; } - if (path === '/api/fleet/agent-status') { + if (path === '/api/fleet/agent_status') { return { data: { results: { online: 0, updating: 0, offline: 1 }, diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_links.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_links.tsx index 032554a4ec439..c39e4e0d097c5 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_links.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_links.tsx @@ -39,8 +39,7 @@ export function useLinks() { version: string; }) => { const imagePath = removeRelativePath(path); - const pkgkey = `${packageName}-${version}`; - const filePath = `${epmRouteService.getInfoPath(pkgkey)}/${imagePath}`; + const filePath = `${epmRouteService.getInfoPath(packageName, version)}/${imagePath}`; return http.basePath.prepend(filePath); }, }; diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx index ad6f492bc5fce..90a2231da40c6 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx @@ -61,9 +61,8 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar const currStatus = getPackageInstallStatus(name); const newStatus = { ...currStatus, name, status: InstallStatus.installing }; setPackageInstallStatus(newStatus); - const pkgkey = `${name}-${version}`; - const res = await sendInstallPackage(pkgkey); + const res = await sendInstallPackage(name, version); if (res.error) { if (fromUpdate) { // if there is an error during update, set it back to the previous version @@ -126,9 +125,8 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar redirectToVersion, }: Pick & { redirectToVersion: string }) => { setPackageInstallStatus({ name, status: InstallStatus.uninstalling, version }); - const pkgkey = `${name}-${version}`; - const res = await sendRemovePackage(pkgkey); + const res = await sendRemovePackage(name, version); if (res.error) { setPackageInstallStatus({ name, status: InstallStatus.installed, version }); notifications.toasts.addWarning({ 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/public/applications/integrations/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx index d442f8a13e27e..02874f12c6592 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx @@ -75,7 +75,7 @@ describe('when on integration detail', () => { describe('and the package is not installed', () => { beforeEach(() => { const unInstalledPackage = mockedApi.responseProvider.epmGetInfo(); - unInstalledPackage.response.status = 'not_installed'; + unInstalledPackage.item.status = 'not_installed'; mockedApi.responseProvider.epmGetInfo.mockReturnValue(unInstalledPackage); render(); }); @@ -283,7 +283,7 @@ const mockApiCalls = ( // @ts-ignore const epmPackageResponse: GetInfoResponse = { - response: { + item: { name: 'nginx', title: 'Nginx', version: '0.3.7', @@ -770,7 +770,7 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos http.get.mockImplementation(async (path: any) => { if (typeof path === 'string') { - if (path === epmRouteService.getInfoPath(`nginx-0.3.7`)) { + if (path === epmRouteService.getInfoPath(`nginx`, `0.3.7`)) { markApiCallAsHandled(); return mockedApiInterface.responseProvider.epmGetInfo(); } diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index 1a3a5c7eadd35..cdebc5f8b3ce1 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -27,6 +27,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import semverLt from 'semver/functions/lt'; +import { splitPkgKey } from '../../../../../../../common'; import { useGetPackageInstallStatus, useSetPackageInstallStatus, @@ -132,26 +133,27 @@ export function Detail() { packageInfo.savedObject && semverLt(packageInfo.savedObject.attributes.version, packageInfo.latestVersion); + const { pkgName, pkgVersion } = splitPkgKey(pkgkey); // Fetch package info const { data: packageInfoData, error: packageInfoError, isLoading: packageInfoLoading, - } = useGetPackageInfoByKey(pkgkey); + } = useGetPackageInfoByKey(pkgName, pkgVersion); const isLoading = packageInfoLoading || permissionCheck.isLoading; const showCustomTab = - useUIExtension(packageInfoData?.response.name ?? '', 'package-detail-custom') !== undefined; + useUIExtension(packageInfoData?.item.name ?? '', 'package-detail-custom') !== undefined; // Track install status state useEffect(() => { - if (packageInfoData?.response) { - const packageInfoResponse = packageInfoData.response; + if (packageInfoData?.item) { + const packageInfoResponse = packageInfoData.item; setPackageInfo(packageInfoResponse); let installedVersion; - const { name } = packageInfoData.response; + const { name } = packageInfoData.item; if ('savedObject' in packageInfoResponse) { installedVersion = packageInfoResponse.savedObject.attributes.version; } diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx index 73c762d34a2cf..a28f63c3f9163 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx @@ -122,7 +122,7 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { try { setKeepPoliciesUpToDateSwitchValue((prev) => !prev); - await sendUpdatePackage(`${packageInfo.name}-${packageInfo.version}`, { + await sendUpdatePackage(packageInfo.name, packageInfo.version, { keepPoliciesUpToDate: !keepPoliciesUpToDateSwitchValue, }); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index 81d1701c4a986..7547e06201171 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -215,7 +215,7 @@ export const AvailablePackages: React.FC = memo(() => { category: '', }); const eprIntegrationList = useMemo( - () => packageListToIntegrationsList(eprPackages?.response || []), + () => packageListToIntegrationsList(eprPackages?.items || []), [eprPackages] ); @@ -256,7 +256,7 @@ export const AvailablePackages: React.FC = memo(() => { ? [] : mergeCategoriesAndCount( eprCategories - ? (eprCategories.response as Array<{ id: string; title: string; count: number }>) + ? (eprCategories.items as Array<{ id: string; title: string; count: number }>) : [], cards ); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx index 3d069c1d0336b..52c4d09a58c56 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx @@ -103,7 +103,7 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ onKeyChange, }) => { const { notifications } = useStartServices(); - const [enrollmentAPIKeys, setEnrollmentAPIKeys] = useState( + const [enrollmentAPIKeys, setEnrollmentAPIKeys] = useState( [] ); @@ -143,7 +143,7 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ throw new Error('No data while fetching enrollment API keys'); } - const enrollmentAPIKeysResponse = res.data.list.filter( + const enrollmentAPIKeysResponse = res.data.items.filter( (key) => key.policy_id === agentPolicyId && key.active === true ); diff --git a/x-pack/plugins/fleet/public/hooks/use_package_icon_type.ts b/x-pack/plugins/fleet/public/hooks/use_package_icon_type.ts index 1b21b7bfd78eb..733aaef8b9267 100644 --- a/x-pack/plugins/fleet/public/hooks/use_package_icon_type.ts +++ b/x-pack/plugins/fleet/public/hooks/use_package_icon_type.ts @@ -66,11 +66,11 @@ export const usePackageIconType = ({ } if (tryApi && !paramIcons && !iconList) { - sendGetPackageInfoByKey(cacheKey) + sendGetPackageInfoByKey(packageName, version) .catch((error) => undefined) // Ignore API errors .then((res) => { CACHED_ICONS.delete(cacheKey); - setIconList(res?.data?.response?.icons); + setIconList(res?.data?.item?.icons); }); } diff --git a/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx b/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx index f4735e6f85546..4789770b7046f 100644 --- a/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx +++ b/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx @@ -36,9 +36,8 @@ export const usePackageInstallations = () => { }); const allInstalledPackages = useMemo( - () => - (allPackages?.response || []).filter((pkg) => pkg.status === installationStatuses.Installed), - [allPackages?.response] + () => (allPackages?.items || []).filter((pkg) => pkg.status === installationStatuses.Installed), + [allPackages?.items] ); const updatablePackages = useMemo( diff --git a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts index a7078dd3a3f91..c5e82316e5eb3 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts @@ -67,9 +67,9 @@ export const useGetLimitedPackages = () => { }); }; -export const useGetPackageInfoByKey = (pkgkey: string) => { +export const useGetPackageInfoByKey = (pkgName: string, pkgVersion: string) => { return useRequest({ - path: epmRouteService.getInfoPath(pkgkey), + path: epmRouteService.getInfoPath(pkgName, pkgVersion), method: 'get', }); }; @@ -81,9 +81,9 @@ export const useGetPackageStats = (pkgName: string) => { }); }; -export const sendGetPackageInfoByKey = (pkgkey: string) => { +export const sendGetPackageInfoByKey = (pkgName: string, pkgVersion: string) => { return sendRequest({ - path: epmRouteService.getInfoPath(pkgkey), + path: epmRouteService.getInfoPath(pkgName, pkgVersion), method: 'get', }); }; @@ -102,23 +102,27 @@ export const sendGetFileByPath = (filePath: string) => { }); }; -export const sendInstallPackage = (pkgkey: string) => { +export const sendInstallPackage = (pkgName: string, pkgVersion: string) => { return sendRequest({ - path: epmRouteService.getInstallPath(pkgkey), + path: epmRouteService.getInstallPath(pkgName, pkgVersion), method: 'post', }); }; -export const sendRemovePackage = (pkgkey: string) => { +export const sendRemovePackage = (pkgName: string, pkgVersion: string) => { return sendRequest({ - path: epmRouteService.getRemovePath(pkgkey), + path: epmRouteService.getRemovePath(pkgName, pkgVersion), method: 'delete', }); }; -export const sendUpdatePackage = (pkgkey: string, body: UpdatePackageRequest['body']) => { +export const sendUpdatePackage = ( + pkgName: string, + pkgVersion: string, + body: UpdatePackageRequest['body'] +) => { return sendRequest({ - path: epmRouteService.getUpdatePath(pkgkey), + path: epmRouteService.getUpdatePath(pkgName, pkgVersion), method: 'put', body, }); diff --git a/x-pack/plugins/fleet/public/search_provider.test.ts b/x-pack/plugins/fleet/public/search_provider.test.ts index 97ed199c44502..43e6d93c8031c 100644 --- a/x-pack/plugins/fleet/public/search_provider.test.ts +++ b/x-pack/plugins/fleet/public/search_provider.test.ts @@ -24,7 +24,7 @@ import { sendGetPackages } from './hooks'; const mockSendGetPackages = sendGetPackages as jest.Mock; -const testResponse: GetPackagesResponse['response'] = [ +const testResponse: GetPackagesResponse['items'] = [ { description: 'test', download: 'test', diff --git a/x-pack/plugins/fleet/public/search_provider.ts b/x-pack/plugins/fleet/public/search_provider.ts index d919462f38c28..fe7cca92cf48d 100644 --- a/x-pack/plugins/fleet/public/search_provider.ts +++ b/x-pack/plugins/fleet/public/search_provider.ts @@ -105,7 +105,7 @@ export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResult const toSearchResults = ( coreStart: CoreStart, - packagesResponse: GetPackagesResponse['response'] + packagesResponse: GetPackagesResponse['items'] ): GlobalSearchProviderResult[] => { return packagesResponse .flatMap( 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/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index dd77c216413f3..578d4281cba3b 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -122,7 +122,8 @@ export const getAgentsHandler: RequestHandler< : 0; const body: GetAgentsResponse = { - list: agents, + list: agents, // deprecated + items: agents, total, totalInactive, page, diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index db5b01b319e00..7297252ff3230 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -116,6 +116,15 @@ export const registerAPIRoutes = (router: IRouter, config: FleetConfigType) => { }, getAgentStatusForAgentPolicyHandler ); + router.get( + { + path: AGENT_API_ROUTES.STATUS_PATTERN_DEPRECATED, + validate: GetAgentStatusRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getAgentStatusForAgentPolicyHandler + ); + // upgrade agent router.post( { diff --git a/x-pack/plugins/fleet/server/routes/app/index.ts b/x-pack/plugins/fleet/server/routes/app/index.ts index aa2d61732eed5..cb2a01deecb4f 100644 --- a/x-pack/plugins/fleet/server/routes/app/index.ts +++ b/x-pack/plugins/fleet/server/routes/app/index.ts @@ -90,4 +90,13 @@ export const registerRoutes = (router: IRouter) => { }, generateServiceTokenHandler ); + + router.post( + { + path: APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN_DEPRECATED, + validate: {}, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + generateServiceTokenHandler + ); }; diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts index 0465614c49432..7fef583af50cd 100644 --- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts +++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts @@ -36,7 +36,13 @@ export const getEnrollmentApiKeysHandler: RequestHandler< perPage: request.query.perPage, kuery: request.query.kuery, }); - const body: GetEnrollmentAPIKeysResponse = { list: items, total, page, perPage }; + const body: GetEnrollmentAPIKeysResponse = { + list: items, // deprecated + items, + total, + page, + perPage, + }; return response.ok({ body }); } catch (error) { diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts index 6429d4d29d5c9..39665f14484ba 100644 --- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts +++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts @@ -61,4 +61,44 @@ export const registerRoutes = (routers: { superuser: FleetRouter; fleetSetup: Fl }, postEnrollmentApiKeyHandler ); + + routers.fleetSetup.get( + { + path: ENROLLMENT_API_KEY_ROUTES.INFO_PATTERN_DEPRECATED, + validate: GetOneEnrollmentAPIKeyRequestSchema, + // Disable this tag and the automatic RBAC support until elastic/fleet-server access is removed in 8.0 + // Required to allow elastic/fleet-server to access this API. + // options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getOneEnrollmentApiKeyHandler + ); + + routers.superuser.delete( + { + path: ENROLLMENT_API_KEY_ROUTES.DELETE_PATTERN_DEPRECATED, + validate: DeleteEnrollmentAPIKeyRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + deleteEnrollmentApiKeyHandler + ); + + routers.fleetSetup.get( + { + path: ENROLLMENT_API_KEY_ROUTES.LIST_PATTERN_DEPRECATED, + validate: GetEnrollmentAPIKeysRequestSchema, + // Disable this tag and the automatic RBAC support until elastic/fleet-server access is removed in 8.0 + // Required to allow elastic/fleet-server to access this API. + // options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getEnrollmentApiKeysHandler + ); + + routers.superuser.post( + { + path: ENROLLMENT_API_KEY_ROUTES.CREATE_PATTERN_DEPRECATED, + validate: PostEnrollmentAPIKeyRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postEnrollmentApiKeyHandler + ); }; diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index c98038427cafc..4f3f969defd0c 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -9,6 +9,7 @@ import path from 'path'; import type { TypeOf } from '@kbn/config-schema'; import mime from 'mime-types'; +import semverValid from 'semver/functions/valid'; import type { ResponseHeaders, KnownHeaders } from 'src/core/server'; import type { @@ -50,8 +51,11 @@ import { getInstallation, } from '../../services/epm/packages'; import type { BulkInstallResponse } from '../../services/epm/packages'; -import { defaultIngestErrorHandler, ingestErrorToResponseOptions } from '../../errors'; -import { splitPkgKey } from '../../services/epm/registry'; +import { + defaultIngestErrorHandler, + ingestErrorToResponseOptions, + IngestManagerError, +} from '../../errors'; import { licenseService } from '../../services'; import { getArchiveEntry } from '../../services/epm/archive/cache'; import { getAsset } from '../../services/epm/archive/storage'; @@ -65,6 +69,7 @@ export const getCategoriesHandler: FleetRequestHandler< try { const res = await getCategories(request.query); const body: GetCategoriesResponse = { + items: res, response: res, }; return response.ok({ body }); @@ -84,6 +89,7 @@ export const getListHandler: FleetRequestHandler< ...request.query, }); const body: GetPackagesResponse = { + items: res, response: res, }; return response.ok({ @@ -99,6 +105,7 @@ export const getLimitedListHandler: FleetRequestHandler = async (context, reques const savedObjectsClient = context.fleet.epm.internalSoClient; const res = await getLimitedPackages({ savedObjectsClient }); const body: GetLimitedPackagesResponse = { + items: res, response: res, }; return response.ok({ @@ -186,13 +193,18 @@ export const getFileHandler: FleetRequestHandler> = async (context, request, response) => { try { - const { pkgkey } = request.params; const savedObjectsClient = context.fleet.epm.internalSoClient; - // TODO: change epm API to /packageName/version so we don't need to do this - const { pkgName, pkgVersion } = splitPkgKey(pkgkey); - const res = await getPackageInfo({ savedObjectsClient, pkgName, pkgVersion }); + const { pkgName, pkgVersion } = request.params; + if (pkgVersion && !semverValid(pkgVersion)) { + throw new IngestManagerError('Package version is not a valid semver'); + } + const res = await getPackageInfo({ + savedObjectsClient, + pkgName, + pkgVersion: pkgVersion || '', + }); const body: GetInfoResponse = { - response: res, + item: res, }; return response.ok({ body }); } catch (error) { @@ -206,14 +218,12 @@ export const updatePackageHandler: FleetRequestHandler< TypeOf > = async (context, request, response) => { try { - const { pkgkey } = request.params; const savedObjectsClient = context.fleet.epm.internalSoClient; - - const { pkgName } = splitPkgKey(pkgkey); + const { pkgName } = request.params; const res = await updatePackage({ savedObjectsClient, pkgName, ...request.body }); const body: UpdatePackageResponse = { - response: res, + item: res, }; return response.ok({ body }); @@ -243,18 +253,18 @@ export const installPackageFromRegistryHandler: FleetRequestHandler< > = async (context, request, response) => { const savedObjectsClient = context.fleet.epm.internalSoClient; const esClient = context.core.elasticsearch.client.asInternalUser; - const { pkgkey } = request.params; + const { pkgName, pkgVersion } = request.params; const res = await installPackage({ installSource: 'registry', savedObjectsClient, - pkgkey, + pkgkey: pkgVersion ? `${pkgName}-${pkgVersion}` : pkgName, esClient, force: request.body?.force, }); if (!res.error) { const body: InstallPackageResponse = { - response: res.assets || [], + items: res.assets || [], }; return response.ok({ body }); } else { @@ -291,6 +301,7 @@ export const bulkInstallPackagesFromRegistryHandler: FleetRequestHandler< }); const payload = bulkInstalledResponses.map(bulkInstallServiceResponseToHttpEntry); const body: BulkInstallPackagesResponse = { + items: payload, response: payload, }; return response.ok({ body }); @@ -321,6 +332,7 @@ export const installPackageByUploadHandler: FleetRequestHandler< }); if (!res.error) { const body: InstallPackageResponse = { + items: res.assets || [], response: res.assets || [], }; return response.ok({ body }); @@ -335,17 +347,18 @@ export const deletePackageHandler: FleetRequestHandler< TypeOf > = async (context, request, response) => { try { - const { pkgkey } = request.params; + const { pkgName, pkgVersion } = request.params; const savedObjectsClient = context.fleet.epm.internalSoClient; const esClient = context.core.elasticsearch.client.asInternalUser; const res = await removeInstallation({ savedObjectsClient, - pkgkey, + pkgName, + pkgVersion, esClient, force: request.body?.force, }); const body: DeletePackageResponse = { - response: res, + items: res, }; return response.ok({ body }); } catch (error) { diff --git a/x-pack/plugins/fleet/server/routes/epm/index.ts b/x-pack/plugins/fleet/server/routes/epm/index.ts index a2f2df4a00c55..da2e245827608 100644 --- a/x-pack/plugins/fleet/server/routes/epm/index.ts +++ b/x-pack/plugins/fleet/server/routes/epm/index.ts @@ -5,18 +5,32 @@ * 2.0. */ +import type { IKibanaResponse } from 'src/core/server'; + +import type { + DeletePackageResponse, + GetInfoResponse, + InstallPackageResponse, + UpdatePackageResponse, +} from '../../../common'; + import { PLUGIN_ID, EPM_API_ROUTES } from '../../constants'; +import { splitPkgKey } from '../../services/epm/registry'; import { GetCategoriesRequestSchema, GetPackagesRequestSchema, GetFileRequestSchema, GetInfoRequestSchema, + GetInfoRequestSchemaDeprecated, InstallPackageFromRegistryRequestSchema, + InstallPackageFromRegistryRequestSchemaDeprecated, InstallPackageByUploadRequestSchema, DeletePackageRequestSchema, + DeletePackageRequestSchemaDeprecated, BulkUpgradePackagesFromRegistryRequestSchema, GetStatsRequestSchema, UpdatePackageRequestSchema, + UpdatePackageRequestSchemaDeprecated, } from '../../types'; import type { FleetRouter } from '../../types/request_context'; @@ -142,4 +156,85 @@ export const registerRoutes = (routers: { rbac: FleetRouter; superuser: FleetRou }, deletePackageHandler ); + + // deprecated since 8.0 + routers.rbac.get( + { + path: EPM_API_ROUTES.INFO_PATTERN_DEPRECATED, + validate: GetInfoRequestSchemaDeprecated, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const newRequest = { ...request, params: splitPkgKey(request.params.pkgkey) } as any; + const resp: IKibanaResponse = await getInfoHandler( + context, + newRequest, + response + ); + if (resp.payload?.item) { + return response.ok({ body: { response: resp.payload.item } }); + } + return resp; + } + ); + + routers.superuser.put( + { + path: EPM_API_ROUTES.INFO_PATTERN_DEPRECATED, + validate: UpdatePackageRequestSchemaDeprecated, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + async (context, request, response) => { + const newRequest = { ...request, params: splitPkgKey(request.params.pkgkey) } as any; + const resp: IKibanaResponse = await updatePackageHandler( + context, + newRequest, + response + ); + if (resp.payload?.item) { + return response.ok({ body: { response: resp.payload.item } }); + } + return resp; + } + ); + + routers.superuser.post( + { + path: EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN_DEPRECATED, + validate: InstallPackageFromRegistryRequestSchemaDeprecated, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + async (context, request, response) => { + const newRequest = { ...request, params: splitPkgKey(request.params.pkgkey) } as any; + const resp: IKibanaResponse = await installPackageFromRegistryHandler( + context, + newRequest, + response + ); + if (resp.payload?.items) { + return response.ok({ body: { response: resp.payload.items } }); + } + return resp; + } + ); + + routers.superuser.delete( + { + path: EPM_API_ROUTES.DELETE_PATTERN_DEPRECATED, + validate: DeletePackageRequestSchemaDeprecated, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + async (context, request, response) => { + const newRequest = { ...request, params: splitPkgKey(request.params.pkgkey) } as any; + const resp: IKibanaResponse = await deletePackageHandler( + context, + newRequest, + response + ); + if (resp.payload?.items) { + return response.ok({ body: { response: resp.payload.items } }); + } + return resp; + } + ); }; 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 { expect(agentPolicy?.outputs.default).toBeDefined(); }); }); + +describe('transformOutputToFullPolicyOutput', () => { + it('should works with only required field on a output', () => { + const policyOutput = transformOutputToFullPolicyOutput({ + id: 'id123', + hosts: ['http://host.fr'], + is_default: false, + is_default_monitoring: false, + name: 'test output', + type: 'elasticsearch', + api_key: 'apikey123', + }); + + expect(policyOutput).toMatchInlineSnapshot(` + Object { + "api_key": "apikey123", + "ca_sha256": undefined, + "hosts": Array [ + "http://host.fr", + ], + "type": "elasticsearch", + } + `); + }); + it('should support ca_trusted_fingerprint field on a output', () => { + const policyOutput = transformOutputToFullPolicyOutput({ + id: 'id123', + hosts: ['http://host.fr'], + is_default: false, + is_default_monitoring: false, + name: 'test output', + type: 'elasticsearch', + api_key: 'apikey123', + ca_trusted_fingerprint: 'fingerprint123', + config_yaml: ` +test: 1234 +ssl.test: 123 + `, + }); + + expect(policyOutput).toMatchInlineSnapshot(` + Object { + "api_key": "apikey123", + "ca_sha256": undefined, + "hosts": Array [ + "http://host.fr", + ], + "ssl.ca_trusted_fingerprint": "fingerprint123", + "ssl.test": 123, + "test": 1234, + "type": "elasticsearch", + } + `); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts index f89a186c1a5f9..166b2f77dc27b 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts @@ -168,19 +168,20 @@ export async function getFullAgentPolicy( return fullAgentPolicy; } -function transformOutputToFullPolicyOutput( +export function transformOutputToFullPolicyOutput( output: Output, standalone = false ): FullAgentPolicyOutput { // eslint-disable-next-line @typescript-eslint/naming-convention - const { config_yaml, type, hosts, ca_sha256, api_key } = output; + const { config_yaml, type, hosts, ca_sha256, ca_trusted_fingerprint, api_key } = output; const configJs = config_yaml ? safeLoad(config_yaml) : {}; const newOutput: FullAgentPolicyOutput = { + ...configJs, type, hosts, ca_sha256, api_key, - ...configJs, + ...(ca_trusted_fingerprint ? { 'ssl.ca_trusted_fingerprint': ca_trusted_fingerprint } : {}), }; if (standalone) { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 69859855d74f0..b63f86e0bf81f 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -7,6 +7,7 @@ import { uniq, omit } from 'lodash'; import uuid from 'uuid/v4'; +import uuidv5 from 'uuid/v5'; import type { ElasticsearchClient, SavedObjectsClientContract, @@ -33,7 +34,12 @@ import type { ListWithKuery, NewPackagePolicy, } from '../types'; -import { agentPolicyStatuses, packageToPackagePolicy, AGENT_POLICY_INDEX } from '../../common'; +import { + agentPolicyStatuses, + packageToPackagePolicy, + AGENT_POLICY_INDEX, + UUID_V5_NAMESPACE, +} from '../../common'; import type { DeleteAgentPolicyResponse, FleetServerPolicy, @@ -57,6 +63,7 @@ import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { normalizeKuery, escapeSearchQueryPhrase } from './saved_object'; import { appContextService } from './app_context'; import { getFullAgentPolicy } from './agent_policies'; + const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; class AgentPolicyService { @@ -127,14 +134,11 @@ class AgentPolicyService { }; let searchParams; - if (id) { - searchParams = { - id: String(id), - }; - } else if ( - preconfiguredAgentPolicy.is_default || - preconfiguredAgentPolicy.is_default_fleet_server - ) { + + const isDefaultPolicy = + preconfiguredAgentPolicy.is_default || preconfiguredAgentPolicy.is_default_fleet_server; + + if (isDefaultPolicy) { searchParams = { searchFields: [ preconfiguredAgentPolicy.is_default_fleet_server @@ -143,10 +147,15 @@ class AgentPolicyService { ], search: 'true', }; + } else if (id) { + searchParams = { + id: String(id), + }; } + if (!searchParams) throw new Error('Missing ID'); - return await this.ensureAgentPolicy(soClient, esClient, newAgentPolicy, searchParams); + return await this.ensureAgentPolicy(soClient, esClient, newAgentPolicy, searchParams, id); } private async ensureAgentPolicy( @@ -158,7 +167,8 @@ class AgentPolicyService { | { searchFields: string[]; search: string; - } + }, + id?: string | number ): Promise<{ created: boolean; policy: AgentPolicy; @@ -196,7 +206,7 @@ class AgentPolicyService { if (agentPolicies.total === 0) { return { created: true, - policy: await this.create(soClient, esClient, newAgentPolicy), + policy: await this.create(soClient, esClient, newAgentPolicy, { id: String(id) }), }; } @@ -780,6 +790,7 @@ export async function addPackageToAgentPolicy( agentPolicy: AgentPolicy, defaultOutput: Output, packagePolicyName?: string, + packagePolicyId?: string | number, packagePolicyDescription?: string, transformPackagePolicy?: (p: NewPackagePolicy) => NewPackagePolicy, bumpAgentPolicyRevison = false @@ -803,7 +814,14 @@ export async function addPackageToAgentPolicy( ? transformPackagePolicy(basePackagePolicy) : basePackagePolicy; + // If an ID is provided via preconfiguration, use that value. Otherwise fall back to + // a UUID v5 value seeded from the agent policy's ID and the provided package policy name. + const id = packagePolicyId + ? String(packagePolicyId) + : uuidv5(`${agentPolicy.id}-${packagePolicyName}`, UUID_V5_NAMESPACE); + await packagePolicyService.create(soClient, esClient, newPackagePolicy, { + id, bumpRevision: bumpAgentPolicyRevison, skipEnsureInstalled: true, skipUniqueNameVerification: true, diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index eb5b43650dad7..7031ef1e6a33f 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -291,7 +291,6 @@ async function installDataStreamComponentTemplates(params: { }); const templateNames = Object.keys(templates); const templateEntries = Object.entries(templates); - // TODO: Check return values for errors await Promise.all( templateEntries.map(async ([name, body]) => { @@ -307,7 +306,6 @@ async function installDataStreamComponentTemplates(params: { const { clusterPromise } = putComponentTemplate(esClient, logger, { body, name, - create: true, }); return clusterPromise; } @@ -343,7 +341,6 @@ export async function ensureDefaultComponentTemplate( await putComponentTemplate(esClient, logger, { name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME, body: FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT, - create: true, }); } diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index 50c0239cd8c56..5ab15a1f52e75 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -9,15 +9,27 @@ import type { SavedObject, SavedObjectsBulkCreateObject, SavedObjectsClientContract, + SavedObjectsImporter, + Logger, } from 'src/core/server'; +import type { SavedObjectsImportSuccess, SavedObjectsImportFailure } from 'src/core/server/types'; + +import { createListStream } from '@kbn/utils'; +import { partition } from 'lodash'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import { getAsset, getPathParts } from '../../archive'; import { KibanaAssetType, KibanaSavedObjectType } from '../../../../types'; import type { AssetType, AssetReference, AssetParts } from '../../../../types'; import { savedObjectTypes } from '../../packages'; -import { indexPatternTypes } from '../index_pattern/install'; +import { indexPatternTypes, getIndexPatternSavedObjects } from '../index_pattern/install'; +type SavedObjectsImporterContract = Pick; +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..77fcc429b2084 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'; @@ -165,7 +164,7 @@ export async function handleInstallPackageFailure({ const installType = getInstallType({ pkgVersion, installedPkg }); if (installType === 'install' || installType === 'reinstall') { logger.error(`uninstalling ${pkgkey} after error installing: [${error.toString()}]`); - await removeInstallation({ savedObjectsClient, pkgkey, esClient }); + await removeInstallation({ savedObjectsClient, pkgName, pkgVersion, esClient }); } await updateInstallStatus({ savedObjectsClient, pkgName, status: 'install_failed' }); @@ -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..848d17f78c929 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -18,26 +18,24 @@ 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 '../..'; -import { splitPkgKey } from '../registry'; 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; - pkgkey: string; + pkgName: string; + pkgVersion: string; esClient: ElasticsearchClient; force?: boolean; }): Promise { - const { savedObjectsClient, pkgkey, esClient, force } = options; - // TODO: the epm api should change to /name/version so we don't need to do this - const { pkgName, pkgVersion } = splitPkgKey(pkgkey); + const { savedObjectsClient, pkgName, pkgVersion, esClient, force } = options; const installation = await getInstallation({ savedObjectsClient, pkgName }); if (!installation) throw Boom.badRequest(`${pkgName} is not installed`); if (installation.removable === false && !force) @@ -62,10 +60,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 +78,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 +155,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 +187,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/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index 1b6e28a07f8e0..8cfb2844159bc 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -8,9 +8,11 @@ import { URL } from 'url'; import mime from 'mime-types'; -import semverValid from 'semver/functions/valid'; + import type { Response } from 'node-fetch'; +import { splitPkgKey as split } from '../../../../common'; + import { KibanaAssetType } from '../../../types'; import type { AssetsGroupedByServiceByType, @@ -31,12 +33,7 @@ import { } from '../archive'; import { streamToBuffer } from '../streams'; import { appContextService } from '../..'; -import { - PackageKeyInvalidError, - PackageNotFoundError, - PackageCacheError, - RegistryResponseError, -} from '../../../errors'; +import { PackageNotFoundError, PackageCacheError, RegistryResponseError } from '../../../errors'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { getRegistryUrl } from './registry_url'; @@ -46,33 +43,7 @@ export interface SearchParams { experimental?: boolean; } -/** - * Extract the package name and package version from a string. - * - * @param pkgkey a string containing the package name delimited by the package version - */ -export function splitPkgKey(pkgkey: string): { pkgName: string; pkgVersion: string } { - // If no version is provided, use the provided package key as the - // package name and return an empty version value - if (!pkgkey.includes('-')) { - return { pkgName: pkgkey, pkgVersion: '' }; - } - - const pkgName = pkgkey.includes('-') ? pkgkey.substr(0, pkgkey.indexOf('-')) : pkgkey; - - if (pkgName === '') { - throw new PackageKeyInvalidError('Package key parsing failed: package name was empty'); - } - - // this will return the entire string if `indexOf` return -1 - const pkgVersion = pkgkey.substr(pkgkey.indexOf('-') + 1); - if (!semverValid(pkgVersion)) { - throw new PackageKeyInvalidError( - 'Package key parsing failed: package version was not a valid semver' - ); - } - return { pkgName, pkgVersion }; -} +export const splitPkgKey = split; export const pkgToPkgKey = ({ name, version }: { name: string; version: string }) => `${name}-${version}`; 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/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 4b87c0957c961..8324079e10da8 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -446,6 +446,7 @@ describe('policy preconfiguration', () => { id: 'test-id', package_policies: [ { + id: 'test-package', package: { name: 'test_package' }, name: 'Test package', }, diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 76fa7778eafa2..a41c7606287ee 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -61,6 +61,7 @@ function isPreconfiguredOutputDifferentFromCurrent( preconfiguredOutput.hosts.map(normalizeHostsForAgents) )) || existingOutput.ca_sha256 !== preconfiguredOutput.ca_sha256 || + existingOutput.ca_trusted_fingerprint !== preconfiguredOutput.ca_trusted_fingerprint || existingOutput.config_yaml !== preconfiguredOutput.config_yaml ); } @@ -404,6 +405,7 @@ async function addPreconfiguredPolicyPackages( agentPolicy: AgentPolicy, installedPackagePolicies: Array< Partial> & { + id?: string | number; name: string; installedPackage: Installation; inputs?: InputsOverride[]; @@ -413,7 +415,7 @@ async function addPreconfiguredPolicyPackages( bumpAgentPolicyRevison = false ) { // Add packages synchronously to avoid overwriting - for (const { installedPackage, name, description, inputs } of installedPackagePolicies) { + for (const { installedPackage, id, name, description, inputs } of installedPackagePolicies) { const packageInfo = await getPackageInfo({ savedObjectsClient: soClient, pkgName: installedPackage.name, @@ -427,6 +429,7 @@ async function addPreconfiguredPolicyPackages( agentPolicy, defaultOutput, name, + id, description, (policy) => preconfigurePackageInputs(policy, packageInfo, inputs), bumpAgentPolicyRevison diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts index 26d581f32d9a2..0e7b7c5e7a093 100644 --- a/x-pack/plugins/fleet/server/services/settings.ts +++ b/x-pack/plugins/fleet/server/services/settings.ts @@ -11,6 +11,7 @@ import type { SavedObjectsClientContract } from 'kibana/server'; import { decodeCloudId, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + GLOBAL_SETTINGS_ID, normalizeHostsForAgents, } from '../../common'; import type { SettingsSOAttributes, Settings, BaseSettings } from '../../common'; @@ -80,10 +81,17 @@ export async function saveSettings( } catch (e) { if (e.isBoom && e.output.statusCode === 404) { const defaultSettings = createDefaultSettings(); - const res = await soClient.create(GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, { - ...defaultSettings, - ...data, - }); + const res = await soClient.create( + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + { + ...defaultSettings, + ...data, + }, + { + id: GLOBAL_SETTINGS_ID, + overwrite: true, + } + ); return { id: res.id, 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/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index 3ba89f1e526b3..4d030e1e87ed4 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -87,6 +87,7 @@ export const PreconfiguredOutputsSchema = schema.arrayOf( type: schema.oneOf([schema.literal(outputType.Elasticsearch)]), hosts: schema.maybe(schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }))), ca_sha256: schema.maybe(schema.string()), + ca_trusted_fingerprint: schema.maybe(schema.string()), config: schema.maybe(schema.object({}, { unknowns: 'allow' })), }), { @@ -106,6 +107,7 @@ export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( monitoring_output_id: schema.maybe(schema.string()), package_policies: schema.arrayOf( schema.object({ + id: schema.maybe(schema.oneOf([schema.string(), schema.number()])), name: schema.string(), package: schema.object({ name: schema.string(), diff --git a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts index 918def62a9d0e..390d5dea792cb 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts @@ -30,12 +30,29 @@ export const GetFileRequestSchema = { }; export const GetInfoRequestSchema = { + params: schema.object({ + pkgName: schema.string(), + pkgVersion: schema.maybe(schema.string()), + }), +}; + +export const GetInfoRequestSchemaDeprecated = { params: schema.object({ pkgkey: schema.string(), }), }; export const UpdatePackageRequestSchema = { + params: schema.object({ + pkgName: schema.string(), + pkgVersion: schema.maybe(schema.string()), + }), + body: schema.object({ + keepPoliciesUpToDate: schema.boolean(), + }), +}; + +export const UpdatePackageRequestSchemaDeprecated = { params: schema.object({ pkgkey: schema.string(), }), @@ -51,6 +68,18 @@ export const GetStatsRequestSchema = { }; export const InstallPackageFromRegistryRequestSchema = { + params: schema.object({ + pkgName: schema.string(), + pkgVersion: schema.maybe(schema.string()), + }), + body: schema.nullable( + schema.object({ + force: schema.boolean(), + }) + ), +}; + +export const InstallPackageFromRegistryRequestSchemaDeprecated = { params: schema.object({ pkgkey: schema.string(), }), @@ -72,6 +101,18 @@ export const InstallPackageByUploadRequestSchema = { }; export const DeletePackageRequestSchema = { + params: schema.object({ + pkgName: schema.string(), + pkgVersion: schema.string(), + }), + body: schema.nullable( + schema.object({ + force: schema.boolean(), + }) + ), +}; + +export const DeletePackageRequestSchemaDeprecated = { params: schema.object({ pkgkey: schema.string(), }), diff --git a/x-pack/plugins/fleet/server/types/rest_spec/output.ts b/x-pack/plugins/fleet/server/types/rest_spec/output.ts index dc60b26087219..de2ddeb3a1bfd 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/output.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/output.ts @@ -30,6 +30,7 @@ export const PostOutputRequestSchema = { is_default_monitoring: schema.boolean({ defaultValue: false }), hosts: schema.maybe(schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }))), ca_sha256: schema.maybe(schema.string()), + ca_trusted_fingerprint: schema.maybe(schema.string()), config_yaml: schema.maybe(schema.string()), }), }; @@ -45,6 +46,7 @@ export const PutOutputRequestSchema = { is_default_monitoring: schema.maybe(schema.boolean()), hosts: schema.maybe(schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }))), ca_sha256: schema.maybe(schema.string()), + ca_trusted_fingerprint: schema.maybe(schema.string()), config_yaml: 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/fleet/storybook/context/fixtures/categories.ts b/x-pack/plugins/fleet/storybook/context/fixtures/categories.ts index 002748bd3d967..1bb8d3300624e 100644 --- a/x-pack/plugins/fleet/storybook/context/fixtures/categories.ts +++ b/x-pack/plugins/fleet/storybook/context/fixtures/categories.ts @@ -6,7 +6,7 @@ */ import type { GetCategoriesResponse } from '../../../public/types'; -export const response: GetCategoriesResponse['response'] = [ +export const items: GetCategoriesResponse['items'] = [ { id: 'aws', title: 'AWS', diff --git a/x-pack/plugins/fleet/storybook/context/fixtures/integration.nginx.ts b/x-pack/plugins/fleet/storybook/context/fixtures/integration.nginx.ts index de4fd228b5342..6f48b15158f8d 100644 --- a/x-pack/plugins/fleet/storybook/context/fixtures/integration.nginx.ts +++ b/x-pack/plugins/fleet/storybook/context/fixtures/integration.nginx.ts @@ -7,7 +7,7 @@ import type { GetInfoResponse } from '../../../public/types'; import { KibanaAssetType, ElasticsearchAssetType } from '../../../common/types'; -export const response: GetInfoResponse['response'] = { +export const item: GetInfoResponse['item'] = { name: 'nginx', title: 'Nginx', version: '0.7.0', diff --git a/x-pack/plugins/fleet/storybook/context/fixtures/integration.okta.ts b/x-pack/plugins/fleet/storybook/context/fixtures/integration.okta.ts index 360c340c9645f..6b766c2d126df 100644 --- a/x-pack/plugins/fleet/storybook/context/fixtures/integration.okta.ts +++ b/x-pack/plugins/fleet/storybook/context/fixtures/integration.okta.ts @@ -7,7 +7,7 @@ import type { GetInfoResponse } from '../../../public/types'; import { KibanaAssetType, ElasticsearchAssetType } from '../../../common/types'; -export const response: GetInfoResponse['response'] = { +export const item: GetInfoResponse['item'] = { name: 'okta', title: 'Okta', version: '1.2.0', diff --git a/x-pack/plugins/fleet/storybook/context/fixtures/packages.ts b/x-pack/plugins/fleet/storybook/context/fixtures/packages.ts index dfe8e905be089..4c13b6b6bf8cb 100644 --- a/x-pack/plugins/fleet/storybook/context/fixtures/packages.ts +++ b/x-pack/plugins/fleet/storybook/context/fixtures/packages.ts @@ -7,7 +7,7 @@ import type { GetPackagesResponse } from '../../../public/types'; -export const response: GetPackagesResponse['response'] = [ +export const items: GetPackagesResponse['items'] = [ { name: 'ga_not_installed', title: 'a. GA, Not Installed', diff --git a/x-pack/plugins/fleet/storybook/context/http.ts b/x-pack/plugins/fleet/storybook/context/http.ts index 3e515c075a595..491b62201e532 100644 --- a/x-pack/plugins/fleet/storybook/context/http.ts +++ b/x-pack/plugins/fleet/storybook/context/http.ts @@ -55,7 +55,7 @@ export const getHttp = (basepath = BASE_PATH) => { // Ideally, this would be a markdown file instead of a ts file, but we don't have // markdown-loader in our package.json, so we'll make do with what we have. - if (path.startsWith('/api/fleet/epm/packages/nginx/')) { + if (path.match('/api/fleet/epm/packages/nginx/.*/.*/')) { const { readme } = await import('./fixtures/readme.nginx'); return readme; } @@ -66,7 +66,7 @@ export const getHttp = (basepath = BASE_PATH) => { // Ideally, this would be a markdown file instead of a ts file, but we don't have // markdown-loader in our package.json, so we'll make do with what we have. - if (path.startsWith('/api/fleet/epm/packages/okta/')) { + if (path.match('/api/fleet/epm/packages/okta/.*/.*/')) { const { readme } = await import('./fixtures/readme.okta'); return readme; } diff --git a/x-pack/plugins/grokdebugger/public/plugin.js b/x-pack/plugins/grokdebugger/public/plugin.js index 63ebbb761b88d..b92cfce6ec0ef 100644 --- a/x-pack/plugins/grokdebugger/public/plugin.js +++ b/x-pack/plugins/grokdebugger/public/plugin.js @@ -22,11 +22,11 @@ export class GrokDebuggerUIPlugin { }), id: PLUGIN.ID, enableRouting: false, - async mount({ element }) { + async mount({ element, theme$ }) { const [coreStart] = await coreSetup.getStartServices(); const license = await plugins.licensing.license$.pipe(first()).toPromise(); const { renderApp } = await import('./render_app'); - return renderApp(license, element, coreStart); + return renderApp(license, element, coreStart, theme$); }, }); diff --git a/x-pack/plugins/grokdebugger/public/render_app.js b/x-pack/plugins/grokdebugger/public/render_app.js index 9666d69d978f0..bcb2560a3c0b7 100644 --- a/x-pack/plugins/grokdebugger/public/render_app.js +++ b/x-pack/plugins/grokdebugger/public/render_app.js @@ -7,23 +7,27 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { I18nProvider } from '@kbn/i18n-react'; +import { KibanaContextProvider, KibanaThemeProvider } from './shared_imports'; import { GrokDebugger } from './components/grok_debugger'; import { GrokdebuggerService } from './services/grokdebugger/grokdebugger_service'; -import { I18nProvider } from '@kbn/i18n-react'; -import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; import { InactiveLicenseSlate } from './components/inactive_license'; -export function renderApp(license, element, coreStart) { +export function renderApp(license, element, coreStart, theme$) { const content = license.isActive ? ( - + + + ) : ( - + + + ); diff --git a/x-pack/plugins/grokdebugger/public/shared_imports.ts b/x-pack/plugins/grokdebugger/public/shared_imports.ts index cab31cb683786..2779673d665b9 100644 --- a/x-pack/plugins/grokdebugger/public/shared_imports.ts +++ b/x-pack/plugins/grokdebugger/public/shared_imports.ts @@ -6,3 +6,8 @@ */ export { EuiCodeEditor } from '../../../../src/plugins/es_ui_shared/public'; + +export { + KibanaContextProvider, + KibanaThemeProvider, +} from '../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap index 8f375305d359e..f4d7fc149a694 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap @@ -42,57 +42,70 @@ exports[`policy table shows empty state when there are no policies 1`] = ` role="main" >
-
-

- Create your first index lifecycle policy -

- -
-

- An index lifecycle policy helps you manage your indices as they age. -

-
- -
- +

+ Create your first index lifecycle policy +

+ +
+
+

+ An index lifecycle policy helps you manage your indices as they age. +

+
+ +
+ +
+
+
`; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx index 5a6d8bb878c37..933a2fd28e07f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -7,17 +7,25 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nStart, ScopedHistory, ApplicationStart } from 'kibana/public'; -import { UnmountCallback } from 'src/core/public'; -import { CloudSetup } from '../../../cloud/public'; -import { ILicense } from '../../../licensing/public'; - -import { KibanaContextProvider, APP_WRAPPER_CLASS } from '../shared_imports'; +import { Observable } from 'rxjs'; +import { + I18nStart, + ScopedHistory, + ApplicationStart, + UnmountCallback, + CoreTheme, +} from 'src/core/public'; +import { + CloudSetup, + ILicense, + KibanaContextProvider, + APP_WRAPPER_CLASS, + RedirectAppLinks, + KibanaThemeProvider, +} from '../shared_imports'; import { App } from './app'; - import { BreadcrumbService } from './services/breadcrumbs'; -import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; export const renderApp = ( element: Element, @@ -26,15 +34,18 @@ export const renderApp = ( application: ApplicationStart, breadcrumbService: BreadcrumbService, license: ILicense, + theme$: Observable, cloud?: CloudSetup ): UnmountCallback => { const { getUrlForApp } = application; render( - - - + + + + + , element diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index bc0981529c34f..d59fd4f20e63f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -50,7 +50,7 @@ export class IndexLifecycleManagementPlugin id: PLUGIN.ID, title: PLUGIN.TITLE, order: 2, - mount: async ({ element, history, setBreadcrumbs }) => { + mount: async ({ element, history, setBreadcrumbs, theme$ }) => { const [coreStart, { licensing }] = await getStartServices(); const { chrome: { docTitle }, @@ -78,6 +78,7 @@ export class IndexLifecycleManagementPlugin application, this.breadcrumbService, license, + theme$, cloud ); diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts index dab299c476eea..dcf435fd72831 100644 --- a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts +++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts @@ -20,6 +20,7 @@ export type { ValidationConfig, ValidationError, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + export { useForm, useFormData, @@ -43,8 +44,16 @@ export { export { attemptToURIDecode } from '../../../../src/plugins/es_ui_shared/public'; -export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; +export { + KibanaContextProvider, + KibanaThemeProvider, + RedirectAppLinks, +} from '../../../../src/plugins/kibana_react/public'; export { APP_WRAPPER_CLASS } from '../../../../src/core/public'; export const useKibana = () => _useKibana(); + +export type { CloudSetup } from '../../cloud/public'; + +export type { ILicense } from '../../licensing/public'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index e26eeadd4edcd..64b8b79d4b2a1 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -140,6 +140,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadNodesPluginsResponse = (response?: HttpResponse, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? error.body : response; + + server.respondWith('GET', `${API_BASE_PATH}/nodes/plugins`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadTemplatesResponse, setLoadIndicesResponse, @@ -154,6 +165,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setUpdateIndexSettingsResponse, setSimulateTemplateResponse, setLoadComponentTemplatesResponse, + setLoadNodesPluginsResponse, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx index 67c9ed067227d..65d3678735689 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx @@ -82,6 +82,7 @@ describe('', () => { jest.useFakeTimers(); httpRequestsMockHelpers.setLoadComponentTemplatesResponse(componentTemplates); + httpRequestsMockHelpers.setLoadNodesPluginsResponse([]); // disable all react-beautiful-dnd development warnings (window as any)['__react-beautiful-dnd-disable-dev-warnings'] = true; @@ -296,7 +297,7 @@ describe('', () => { }); describe('mappings (step 4)', () => { - beforeEach(async () => { + const navigateToMappingsStep = async () => { const { actions } = testBed; // Logistics await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); @@ -304,6 +305,10 @@ describe('', () => { await actions.completeStepTwo(); // Index settings await actions.completeStepThree('{}'); + }; + + beforeEach(async () => { + await navigateToMappingsStep(); }); it('should set the correct page title', () => { @@ -337,6 +342,43 @@ describe('', () => { expect(find('fieldsListItem').length).toBe(1); }); + + describe('plugin parameters', () => { + const selectMappingsEditorTab = async ( + tab: 'fields' | 'runtimeFields' | 'templates' | 'advanced' + ) => { + const tabIndex = ['fields', 'runtimeFields', 'templates', 'advanced'].indexOf(tab); + const tabElement = testBed.find('mappingsEditor.formTab').at(tabIndex); + await act(async () => { + tabElement.simulate('click'); + }); + testBed.component.update(); + }; + + test('should not render the _size parameter if the mapper size plugin is not installed', async () => { + const { exists } = testBed; + // Navigate to the advanced configuration + await selectMappingsEditorTab('advanced'); + + expect(exists('mappingsEditor.advancedConfiguration.sizeEnabledToggle')).toBe(false); + }); + + test('should render the _size parameter if the mapper size plugin is installed', async () => { + httpRequestsMockHelpers.setLoadNodesPluginsResponse(['mapper-size']); + + await act(async () => { + testBed = await setup(); + }); + testBed.component.update(); + await navigateToMappingsStep(); + + await selectMappingsEditorTab('advanced'); + + expect(testBed.exists('mappingsEditor.advancedConfiguration.sizeEnabledToggle')).toBe( + true + ); + }); + }); }); describe('aliases (step 5)', () => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts index 3a8d34c341834..f2fcf7bbab50c 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts @@ -339,4 +339,6 @@ export type TestSubjects = | 'versionField' | 'aliasesEditor' | 'settingsEditor' - | 'versionField.input'; + | 'versionField.input' + | 'mappingsEditor.formTab' + | 'mappingsEditor.advancedConfiguration.sizeEnabledToggle'; diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 5cd0864a4df21..f44ff13b205db 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -6,15 +6,15 @@ */ import React, { createContext, useContext } from 'react'; -import { ScopedHistory } from 'kibana/public'; +import { Observable } from 'rxjs'; import SemVer from 'semver/classes/semver'; import { ManagementAppMountParams } from 'src/plugins/management/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { CoreSetup, CoreStart, CoreTheme, ScopedHistory } from 'src/core/public'; +import { SharePluginStart } from 'src/plugins/share/public'; -import { CoreSetup, CoreStart } from '../../../../../src/core/public'; -import { UiMetricService, NotificationService, HttpService } from './services'; import { ExtensionsService } from '../services'; -import { SharePluginStart } from '../../../../../src/plugins/share/public'; +import { UiMetricService, NotificationService, HttpService } from './services'; const AppContext = createContext(undefined); @@ -39,6 +39,7 @@ export interface AppDependencies { url: SharePluginStart['url']; docLinks: CoreStart['docLinks']; kibanaVersion: SemVer; + theme$: Observable; } export const AppContextProvider = ({ diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx index a39baf59d1f05..1f4abac806276 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx @@ -74,13 +74,15 @@ describe('', () => { expect(nameInput.props().disabled).toEqual(true); }); - // FLAKY: https://github.com/elastic/kibana/issues/84906 - describe.skip('form payload', () => { + describe('form payload', () => { it('should send the correct payload with changed values', async () => { const { actions, component, form } = testBed; await act(async () => { form.setInputValue('versionField.input', '1'); + }); + + await act(async () => { actions.clickNextButton(); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx index d80712dfa0fea..49922b45f2fde 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { ComponentType, MemoExoticComponent } from 'react'; import SemVer from 'semver/classes/semver'; /* eslint-disable-next-line @kbn/eslint/no-restricted-paths */ @@ -18,6 +18,7 @@ import { import { MAJOR_VERSION } from '../../../../../../../common'; import { MappingsEditorProvider } from '../../../mappings_editor_context'; import { createKibanaReactContext } from '../../../shared_imports'; +import { Props as MappingsEditorProps } from '../../../mappings_editor'; export const kibanaVersion = new SemVer(MAJOR_VERSION); @@ -82,17 +83,21 @@ const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ }, }); -const defaultProps = { +const defaultProps: MappingsEditorProps = { docLinks: docLinksServiceMock.createStartContract(), + onChange: () => undefined, + esNodesPlugins: [], }; -export const WithAppDependencies = (Comp: any) => (props: any) => - ( - - - - - - - - ); +export const WithAppDependencies = + (Comp: MemoExoticComponent>) => + (props: Partial) => + ( + + + + + + + + ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx index 5a7c6d439d101..4e4c146c85957 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -10,15 +10,19 @@ import { EuiSpacer } from '@elastic/eui'; import { useForm, Form } from '../../shared_imports'; import { GenericObject, MappingsConfiguration } from '../../types'; +import { MapperSizePluginId } from '../../constants'; import { useDispatch } from '../../mappings_state_context'; import { DynamicMappingSection } from './dynamic_mapping_section'; import { SourceFieldSection } from './source_field_section'; import { MetaFieldSection } from './meta_field_section'; import { RoutingSection } from './routing_section'; +import { MapperSizePluginSection } from './mapper_size_plugin_section'; import { configurationFormSchema } from './configuration_form_schema'; interface Props { value?: MappingsConfiguration; + /** List of plugins installed in the cluster nodes */ + esNodesPlugins: string[]; } const formSerializer = (formData: GenericObject) => { @@ -35,6 +39,7 @@ const formSerializer = (formData: GenericObject) => { sourceField, metaField, _routing, + _size, } = formData; const dynamic = dynamicMappingsEnabled ? true : throwErrorsForUnmappedFields ? 'strict' : false; @@ -47,6 +52,7 @@ const formSerializer = (formData: GenericObject) => { _source: sourceField, _meta: metaField, _routing, + _size, }; return serialized; @@ -67,6 +73,8 @@ const formDeserializer = (formData: GenericObject) => { }, _meta, _routing, + // For the Mapper Size plugin + _size, } = formData; return { @@ -84,10 +92,11 @@ const formDeserializer = (formData: GenericObject) => { }, metaField: _meta ?? {}, _routing, + _size, }; }; -export const ConfigurationForm = React.memo(({ value }: Props) => { +export const ConfigurationForm = React.memo(({ value, esNodesPlugins }: Props) => { const isMounted = useRef(false); const { form } = useForm({ @@ -100,6 +109,9 @@ export const ConfigurationForm = React.memo(({ value }: Props) => { const dispatch = useDispatch(); const { subscribe, submit, reset, getFormData } = form; + const isMapperSizeSectionVisible = + value?._size !== undefined || esNodesPlugins.includes(MapperSizePluginId); + useEffect(() => { const subscription = subscribe(({ data, isValid, validate }) => { dispatch({ @@ -150,6 +162,7 @@ export const ConfigurationForm = React.memo(({ value }: Props) => { + {isMapperSizeSectionVisible && } ); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx index b5fa5f25b865b..d8e3e8d5ae7c2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx @@ -192,4 +192,12 @@ export const configurationFormSchema: FormSchema = { defaultValue: false, }, }, + _size: { + enabled: { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.sizeLabel', { + defaultMessage: 'Index the _source field size in bytes', + }), + defaultValue: false, + }, + }, }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/mapper_size_plugin_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/mapper_size_plugin_section.tsx new file mode 100644 index 0000000000000..db2ded2e09990 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/mapper_size_plugin_section.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLink, EuiCode } from '@elastic/eui'; + +import { documentationService } from '../../../../services/documentation'; +import { UseField, FormRow, ToggleField } from '../../shared_imports'; + +export const MapperSizePluginSection = () => { + return ( + + {i18n.translate('xpack.idxMgmt.mappingsEditor.sizeDocumentionLink', { + defaultMessage: 'Learn more.', + })} + + ), + _source: _source, + }} + /> + } + > + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx index 701cd86510b5d..a875b9985a8f4 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx @@ -13,10 +13,12 @@ import { LoadMappingsProvider } from './load_mappings_provider'; interface Props { onJson(json: { [key: string]: any }): void; + /** List of plugins installed in the cluster nodes */ + esNodesPlugins: string[]; } -export const LoadMappingsFromJsonButton = ({ onJson }: Props) => ( - +export const LoadMappingsFromJsonButton = ({ onJson, esNodesPlugins }: Props) => ( + {(openModal) => ( {i18n.translate('xpack.idxMgmt.mappingsEditor.loadFromJsonButtonLabel', { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx index 2413df5e5d64d..8259c78b8e140 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx @@ -22,7 +22,7 @@ import { registerTestBed, TestBed } from '@kbn/test/jest'; import { LoadMappingsProvider } from './load_mappings_provider'; const ComponentToTest = ({ onJson }: { onJson: () => void }) => ( - + {(openModal) => (
"`; +exports[`ResetSessionPage renders as expected 1`] = `"ElasticMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 25c6b07fd03af..7fa387207e3ff 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 @@ -40,7 +39,7 @@ export const DEFAULT_APP_TIME_RANGE = 'securitySolution:timeDefaults' as const; export const DEFAULT_APP_REFRESH_INTERVAL = 'securitySolution:refreshIntervalDefaults' as const; export const DEFAULT_ALERTS_INDEX = '.alerts-security.alerts' as const; export const DEFAULT_SIGNALS_INDEX = '.siem-signals' as const; -export const DEFAULT_PREVIEW_INDEX = '.siem-preview-signals' as const; +export const DEFAULT_PREVIEW_INDEX = '.preview.alerts-security.alerts' as const; export const DEFAULT_LISTS_INDEX = '.lists' as const; export const DEFAULT_ITEMS_INDEX = '.items' as const; // The DEFAULT_MAX_SIGNALS value exists also in `x-pack/plugins/cases/common/constants.ts` @@ -252,15 +251,11 @@ 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 = `${DETECTION_ENGINE_RULES_URL}/_bulk_action` as const; export const DETECTION_ENGINE_RULES_PREVIEW = `${DETECTION_ENGINE_RULES_URL}/preview` as const; -export const DETECTION_ENGINE_RULES_PREVIEW_INDEX_URL = - `${DETECTION_ENGINE_RULES_PREVIEW}/index` as const; /** * Internal detection engine routes @@ -362,8 +357,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/constants.ts b/x-pack/plugins/security_solution/common/detection_engine/constants.ts new file mode 100644 index 0000000000000..7f3c822800673 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/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 enum RULE_PREVIEW_INVOCATION_COUNT { + HOUR = 20, + DAY = 24, + WEEK = 168, + MONTH = 30, +} 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..97079253606f1 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 = < @@ -367,6 +370,7 @@ export const previewRulesSchema = t.intersection([ createTypeSpecific, t.type({ invocationCount: t.number }), ]); +export type PreviewRulesSchema = t.TypeOf; type UpdateSchema = SharedUpdateSchema & T; export type EqlUpdateSchema = UpdateSchema>; @@ -422,6 +426,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/detection_engine/schemas/response/error_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.ts index 9a4bd3c65c367..d6e1faa7a5180 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import * as t from 'io-ts'; import { rule_id, status_code, message } from '../common/schemas'; @@ -12,7 +13,9 @@ import { rule_id, status_code, message } from '../common/schemas'; // We use id: t.string intentionally and _never_ the id from global schemas as // sometimes echo back out the id that the user gave us and it is not guaranteed // to be a UUID but rather just a string -const partial = t.exact(t.partial({ id: t.string, rule_id })); +const partial = t.exact( + t.partial({ id: t.string, rule_id, list_id: NonEmptyString, item_id: NonEmptyString }) +); const required = t.exact( t.type({ error: t.type({ 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/endpoint/data_loaders/index_endpoint_hosts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts index ed75823cd30d3..be26f8496c5e9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts @@ -97,7 +97,7 @@ export async function indexEndpointHostDocs({ client: Client; kbnClient: KbnClient; realPolicies: Record; - epmEndpointPackage: GetPackagesResponse['response'][0]; + epmEndpointPackage: GetPackagesResponse['items'][0]; metadataIndex: string; policyResponseIndex: string; enrollFleet: boolean; diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/setup_fleet_for_endpoint.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/setup_fleet_for_endpoint.ts index 61f7123c36840..a236b56737e03 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/setup_fleet_for_endpoint.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/setup_fleet_for_endpoint.ts @@ -101,7 +101,7 @@ export const installOrUpgradeEndpointFleetPackage = async ( }) .catch(wrapErrorAndRejectPromise)) as AxiosResponse; - const bulkResp = installEndpointPackageResp.data.response; + const bulkResp = installEndpointPackageResp.data.items; if (bulkResp.length <= 0) { throw new EndpointDataLoadingError( diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 9cbe1e19530ca..f9ecb2e018dff 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -1600,7 +1600,7 @@ export class EndpointDocGenerator extends BaseDataGenerator { /** * Generate an EPM Package for Endpoint */ - public generateEpmPackage(): GetPackagesResponse['response'][0] { + public generateEpmPackage(): GetPackagesResponse['items'][0] { return { id: this.seededUUIDv4(), name: 'endpoint', diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 5bb3bd3dbae52..5c81196a3709c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -124,13 +124,13 @@ export async function indexHostsAndAlerts( const getEndpointPackageInfo = async ( kbnClient: KbnClient -): Promise => { +): Promise => { const endpointPackage = ( (await kbnClient.request({ path: `${EPM_API_ROUTES.LIST_PATTERN}?category=security`, method: 'GET', })) as AxiosResponse - ).data.response.find((epmPackage) => epmPackage.name === 'endpoint'); + ).data.items.find((epmPackage) => epmPackage.name === 'endpoint'); if (!endpointPackage) { throw new Error('EPM Endpoint package was not found!'); 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/common/types/timeline/store.ts b/x-pack/plugins/security_solution/common/types/timeline/store.ts index 1b32f12dafd5b..250ff061e81dd 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/store.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/store.ts @@ -39,7 +39,7 @@ export interface SortColumnTimeline { export interface TimelinePersistInput { columns: ColumnHeaderOptions[]; dataProviders?: DataProvider[]; - dataViewId: string; + dataViewId: string | null; // null if legacy pre-8.0 timeline dateRange?: { start: string; end: string; 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..0e4dbc9a95f9c 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,9 @@ describe('CTI Enrichment', () => { it('Displays persisted enrichments on the JSON view', () => { const expectedEnrichment = [ { + feed: { + name: 'AbuseCH malware', + }, indicator: { first_seen: '2021-03-10T08:02:14.000Z', file: { @@ -112,6 +115,7 @@ describe('CTI Enrichment', () => { it('Displays threat indicator details on the threat intel tab', () => { const expectedThreatIndicatorData = [ + { field: 'feed.name', value: 'AbuseCH malware' }, { field: 'indicator.file.hash.md5', value: '9b6c3518a91d23ed77504b5416bfb5b3' }, { field: 'indicator.file.hash.sha256', @@ -172,11 +176,12 @@ describe('CTI Enrichment', () => { const indicatorMatchRuleEnrichment = { field: 'myhash.mysha256', value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + feedName: 'AbuseCH malware', }; const investigationTimeEnrichment = { field: 'source.ip', value: '192.168.1.1', - provider: 'another_provider', + feedName: 'feed_name', }; expandFirstAlert(); @@ -187,14 +192,14 @@ describe('CTI Enrichment', () => { .should('exist') .should( 'have.text', - `${indicatorMatchRuleEnrichment.field} ${indicatorMatchRuleEnrichment.value}` + `${indicatorMatchRuleEnrichment.field} ${indicatorMatchRuleEnrichment.value} from ${indicatorMatchRuleEnrichment.feedName}` ); cy.get(`${INVESTIGATION_TIME_ENRICHMENT_SECTION} ${THREAT_DETAILS_ACCORDION}`) .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/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index 02d8837261f2f..81022a43ff683 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -174,7 +174,7 @@ describe('Detection rules, threshold', () => { cy.get(ALERT_GRID_CELL).contains(rule.name); }); - it('Preview results of keyword using "host.name"', () => { + it.skip('Preview results of keyword using "host.name"', () => { rule.index = [...rule.index, '.siem-signals*']; createCustomRuleActivated(getNewRule()); @@ -188,7 +188,7 @@ describe('Detection rules, threshold', () => { cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '3 unique hits'); }); - it('Preview results of "ip" using "source.ip"', () => { + it.skip('Preview results of "ip" using "source.ip"', () => { const previewRule: ThresholdRule = { ...rule, thresholdField: 'source.ip', 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/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index aadaa5dfa0d88..a3e5e8af3f598 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -104,9 +104,9 @@ export const DEFINE_INDEX_INPUT = export const EQL_TYPE = '[data-test-subj="eqlRuleType"]'; -export const EQL_QUERY_INPUT = '[data-test-subj="eqlQueryBarTextInput"]'; +export const PREVIEW_HISTOGRAM = '[data-test-subj="preview-histogram-panel"]'; -export const EQL_QUERY_PREVIEW_HISTOGRAM = '[data-test-subj="queryPreviewEqlHistogram"]'; +export const EQL_QUERY_INPUT = '[data-test-subj="eqlQueryBarTextInput"]'; export const EQL_QUERY_VALIDATION_SPINNER = '[data-test-subj="eql-validation-loading"]'; @@ -170,7 +170,7 @@ export const RISK_OVERRIDE = export const RULES_CREATION_FORM = '[data-test-subj="stepDefineRule"]'; -export const RULES_CREATION_PREVIEW = '[data-test-subj="ruleCreationQueryPreview"]'; +export const RULES_CREATION_PREVIEW = '[data-test-subj="rule-preview"]'; export const RULE_DESCRIPTION_INPUT = '[data-test-subj="detectionEngineStepAboutRuleDescription"] [data-test-subj="input"]'; 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/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 68449363b8643..538f95c3c0a80 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -33,7 +33,6 @@ import { DEFAULT_RISK_SCORE_INPUT, DEFINE_CONTINUE_BUTTON, EQL_QUERY_INPUT, - EQL_QUERY_PREVIEW_HISTOGRAM, EQL_QUERY_VALIDATION_SPINNER, EQL_TYPE, FALSE_POSITIVES_INPUT, @@ -92,6 +91,7 @@ import { EMAIL_CONNECTOR_USER_INPUT, EMAIL_CONNECTOR_PASSWORD_INPUT, EMAIL_CONNECTOR_SERVICE_SELECTOR, + PREVIEW_HISTOGRAM, } from '../screens/create_new_rule'; import { TOAST_ERROR } from '../screens/shared'; import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; @@ -324,12 +324,12 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { .find(QUERY_PREVIEW_BUTTON) .should('not.be.disabled') .click({ force: true }); - cy.get(EQL_QUERY_PREVIEW_HISTOGRAM) + cy.get(PREVIEW_HISTOGRAM) .invoke('text') .then((text) => { if (text !== 'Hits') { cy.get(RULES_CREATION_PREVIEW).find(QUERY_PREVIEW_BUTTON).click({ force: true }); - cy.get(EQL_QUERY_PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); + cy.get(PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); } }); cy.get(TOAST_ERROR).should('not.exist'); diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx index c16e77e9182f2..cfcf5307de8d4 100644 --- a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx @@ -12,6 +12,7 @@ import { SecurityPageName } from '../../../../common/constants'; import { createSecuritySolutionStorageMock, mockGlobalState, + mockIndexPattern, SUB_PLUGINS_REDUCER, TestProviders, } from '../../../common/mock'; @@ -36,6 +37,10 @@ jest.mock('../../../common/lib/kibana', () => { }; }); +jest.mock('../../../common/containers/source', () => ({ + useFetchIndex: () => [false, { indicesExist: true, indexPatterns: mockIndexPattern }], +})); + jest.mock('react-reverse-portal', () => ({ InPortal: ({ children }: { children: React.ReactNode }) => <>{children}, OutPortal: ({ children }: { children: React.ReactNode }) => <>{children}, diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx index c283bb10c7928..6d09f369be044 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx @@ -24,10 +24,10 @@ export const SecuritySolutionBottomBar = React.memo( ({ onAppLeave }: { onAppLeave: (handler: AppLeaveHandler) => void }) => { const [showTimeline] = useShowTimeline(); - const { indicesExist } = useSourcererDataView(SourcererScopeName.timeline); - useResolveRedirect(); + const { indicesExist, dataViewId } = useSourcererDataView(SourcererScopeName.timeline); - return indicesExist && showTimeline ? ( + useResolveRedirect(); + return (indicesExist || dataViewId === null) && showTimeline ? ( <> diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index 751e8fde530ba..e6c8e1f6f8c4f 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -98,7 +98,7 @@ const CaseContainerComponent: React.FC = () => { timelineActions.createTimeline({ id: TimelineId.casePage, columns: [], - dataViewId: '', + dataViewId: null, indexNames: [], expandedDetail: {}, show: false, 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/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index afff935619740..617c66fa3c3da 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -327,11 +327,16 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const alertIdToClose = shouldCloseAlert && alertData ? alertData._id : undefined; const bulkCloseIndex = shouldBulkCloseAlert && signalIndexName != null ? [signalIndexName] : undefined; - addOrUpdateExceptionItems(ruleId, enrichExceptionItems(), alertIdToClose, bulkCloseIndex); + addOrUpdateExceptionItems( + maybeRule?.rule_id ?? '', + enrichExceptionItems(), + alertIdToClose, + bulkCloseIndex + ); } }, [ addOrUpdateExceptionItems, - ruleId, + maybeRule, enrichExceptionItems, shouldCloseAlert, shouldBulkCloseAlert, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 1724f616e7fc8..f88bf855049ef 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -267,11 +267,16 @@ export const EditExceptionModal = memo(function EditExceptionModal({ if (addOrUpdateExceptionItems !== null) { const bulkCloseIndex = shouldBulkCloseAlert && signalIndexName !== null ? [signalIndexName] : undefined; - addOrUpdateExceptionItems(ruleId, enrichExceptionItems(), undefined, bulkCloseIndex); + addOrUpdateExceptionItems( + maybeRule?.rule_id ?? '', + enrichExceptionItems(), + undefined, + bulkCloseIndex + ); } }, [ addOrUpdateExceptionItems, - ruleId, + maybeRule, enrichExceptionItems, shouldBulkCloseAlert, signalIndexName, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 0d7366557ff6e..564bb965a8782 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -44,16 +44,14 @@ describe('useAddOrUpdateException', () => { let updateExceptionListItem: jest.SpyInstance>; let getQueryFilter: jest.SpyInstance>; let buildAlertStatusesFilter: jest.SpyInstance< - ReturnType - >; - let buildAlertsRuleIdFilter: jest.SpyInstance< - ReturnType + ReturnType >; + let buildAlertsFilter: jest.SpyInstance>; let addOrUpdateItemsArgs: Parameters; let render: () => RenderHookResult; const onError = jest.fn(); const onSuccess = jest.fn(); - const ruleId = 'rule-id'; + const ruleStaticId = 'rule-id'; const alertIdToClose = 'idToClose'; const bulkCloseIndex = ['.custom']; const itemsToAdd: CreateExceptionListItemSchema[] = [ @@ -128,14 +126,11 @@ describe('useAddOrUpdateException', () => { getQueryFilter = jest.spyOn(getQueryFilterHelper, 'getQueryFilter'); - buildAlertStatusesFilter = jest.spyOn( - buildFilterHelpers, - 'buildAlertStatusesFilterRuleRegistry' - ); + buildAlertStatusesFilter = jest.spyOn(buildFilterHelpers, 'buildAlertStatusesFilter'); - buildAlertsRuleIdFilter = jest.spyOn(buildFilterHelpers, 'buildAlertsRuleIdFilter'); + buildAlertsFilter = jest.spyOn(buildFilterHelpers, 'buildAlertsFilter'); - addOrUpdateItemsArgs = [ruleId, itemsToAddOrUpdate]; + addOrUpdateItemsArgs = [ruleStaticId, itemsToAddOrUpdate]; render = () => renderHook( () => @@ -262,7 +257,7 @@ describe('useAddOrUpdateException', () => { describe('when alertIdToClose is passed in', () => { beforeEach(() => { - addOrUpdateItemsArgs = [ruleId, itemsToAddOrUpdate, alertIdToClose]; + addOrUpdateItemsArgs = [ruleStaticId, itemsToAddOrUpdate, alertIdToClose]; }); it('should update the alert status', async () => { await act(async () => { @@ -317,7 +312,7 @@ describe('useAddOrUpdateException', () => { describe('when bulkCloseIndex is passed in', () => { beforeEach(() => { - addOrUpdateItemsArgs = [ruleId, itemsToAddOrUpdate, undefined, bulkCloseIndex]; + addOrUpdateItemsArgs = [ruleStaticId, itemsToAddOrUpdate, undefined, bulkCloseIndex]; }); it('should update the status of only alerts that are open', async () => { await act(async () => { @@ -351,8 +346,8 @@ describe('useAddOrUpdateException', () => { addOrUpdateItems(...addOrUpdateItemsArgs); } await waitForNextUpdate(); - expect(buildAlertsRuleIdFilter).toHaveBeenCalledTimes(1); - expect(buildAlertsRuleIdFilter.mock.calls[0][0]).toEqual(ruleId); + expect(buildAlertsFilter).toHaveBeenCalledTimes(1); + expect(buildAlertsFilter.mock.calls[0][0]).toEqual(ruleStaticId); }); }); it('should generate the query filter using exceptions', async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx index 7cb8b643aa0e8..71c49f7c2daad 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx @@ -17,27 +17,25 @@ import { HttpStart } from '../../../../../../../src/core/public'; import { updateAlertStatus } from '../../../detections/containers/detection_engine/alerts/api'; import { getUpdateAlertsQuery } from '../../../detections/components/alerts_table/actions'; import { - buildAlertsRuleIdFilter, + buildAlertsFilter, buildAlertStatusesFilter, - buildAlertStatusesFilterRuleRegistry, } from '../../../detections/components/alerts_table/default_config'; import { getQueryFilter } from '../../../../common/detection_engine/get_query_filter'; import { Index } from '../../../../common/detection_engine/schemas/common/schemas'; -import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { formatExceptionItemForUpdate, prepareExceptionItemsForBulkClose } from './helpers'; import { useKibana } from '../../lib/kibana'; /** * Adds exception items to the list. Also optionally closes alerts. * - * @param ruleId id of the rule where the exception updates will be applied + * @param ruleStaticId static id of the rule (rule.ruleId, not rule.id) where the exception updates will be applied * @param exceptionItemsToAddOrUpdate array of ExceptionListItemSchema to add or update * @param alertIdToClose - optional string representing alert to close * @param bulkCloseIndex - optional index used to create bulk close query * */ export type AddOrUpdateExceptionItemsFunc = ( - ruleId: string, + ruleStaticId: string, exceptionItemsToAddOrUpdate: Array, alertIdToClose?: string, bulkCloseIndex?: Index @@ -72,10 +70,10 @@ export const useAddOrUpdateException = ({ const addOrUpdateExceptionRef = useRef(null); const { addExceptionListItem, updateExceptionListItem } = useApi(services.http); const addOrUpdateException = useCallback( - async (ruleId, exceptionItemsToAddOrUpdate, alertIdToClose, bulkCloseIndex) => { + async (ruleStaticId, exceptionItemsToAddOrUpdate, alertIdToClose, bulkCloseIndex) => { if (addOrUpdateExceptionRef.current != null) { addOrUpdateExceptionRef.current( - ruleId, + ruleStaticId, exceptionItemsToAddOrUpdate, alertIdToClose, bulkCloseIndex @@ -84,15 +82,13 @@ export const useAddOrUpdateException = ({ }, [] ); - // TODO: Once we are past experimental phase this code should be removed - const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); useEffect(() => { let isSubscribed = true; const abortCtrl = new AbortController(); const onUpdateExceptionItemsAndAlertStatus: AddOrUpdateExceptionItemsFunc = async ( - ruleId, + ruleStaticId, exceptionItemsToAddOrUpdate, alertIdToClose, bulkCloseIndex @@ -131,15 +127,16 @@ export const useAddOrUpdateException = ({ } if (bulkCloseIndex != null) { - // TODO: Once we are past experimental phase this code should be removed - const alertStatusFilter = ruleRegistryEnabled - ? buildAlertStatusesFilterRuleRegistry(['open', 'acknowledged', 'in-progress']) - : buildAlertStatusesFilter(['open', 'acknowledged', 'in-progress']); + const alertStatusFilter = buildAlertStatusesFilter([ + 'open', + 'acknowledged', + 'in-progress', + ]); const filter = getQueryFilter( '', 'kuery', - [...buildAlertsRuleIdFilter(ruleId), ...alertStatusFilter], + [...buildAlertsFilter(ruleStaticId), ...alertStatusFilter], bulkCloseIndex, prepareExceptionItemsForBulkClose(exceptionItemsToAddOrUpdate), false @@ -185,14 +182,7 @@ export const useAddOrUpdateException = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [ - addExceptionListItem, - http, - onSuccess, - onError, - ruleRegistryEnabled, - updateExceptionListItem, - ]); + }, [addExceptionListItem, http, onSuccess, onError, updateExceptionListItem]); return [{ isLoading }, addOrUpdateException]; }; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index b67505a66be44..e5da55f740033 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -15,7 +15,7 @@ import { MatrixHistogramType } from '../../../../common/search_strategy/security import { UpdateDateRange } from '../charts/common'; import { GlobalTimeArgs } from '../../containers/use_global_time'; import { DocValueFields } from '../../../../common/search_strategy'; -import { Threshold } from '../../../detections/components/rules/query_preview'; +import { FieldValueThreshold } from '../../../detections/components/rules/threshold_input'; export type MatrixHistogramMappingTypes = Record< string, @@ -77,7 +77,7 @@ export interface MatrixHistogramQueryProps { stackByField: string; startDate: string; histogramType: MatrixHistogramType; - threshold?: Threshold; + threshold?: FieldValueThreshold; skip?: boolean; isPtrIncluded?: boolean; includeMissingData?: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 9666a0837b046..d17f5ceb4f9b1 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -27,6 +27,10 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar z-index: 9900 !important; min-width: 24px; } + .euiPopover__panel.euiPopover__panel-isOpen.sourcererPopoverPanel { + // needs to appear under modal + z-index: 5900 !important; + } .euiToolTip { z-index: 9950 !important; } diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx index af21a018ee47a..3d378e72edbf4 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx @@ -14,7 +14,7 @@ import { EuiFormRow, EuiFormRowProps, } from '@elastic/eui'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { sourcererModel } from '../../store/sourcerer'; @@ -50,11 +50,25 @@ export const PopoverContent = styled.div` export const StyledBadge = styled(EuiBadge)` margin-left: 8px; + &, + .euiBadge__text { + cursor: pointer; + } +`; + +export const Blockquote = styled.span` + ${({ theme }) => css` + display: block; + border-color: ${theme.eui.euiColorDarkShade}; + border-left: ${theme.eui.euiBorderThick}; + margin: ${theme.eui.euiSizeS} 0 ${theme.eui.euiSizeS} ${theme.eui.euiSizeS}; + padding: ${theme.eui.euiSizeS}; + `} `; interface GetDataViewSelectOptionsProps { dataViewId: string; - defaultDataView: sourcererModel.KibanaDataView; + defaultDataViewId: sourcererModel.KibanaDataView['id']; isModified: boolean; isOnlyDetectionAlerts: boolean; kibanaDataViews: sourcererModel.KibanaDataView[]; @@ -62,7 +76,7 @@ interface GetDataViewSelectOptionsProps { export const getDataViewSelectOptions = ({ dataViewId, - defaultDataView, + defaultDataViewId, isModified, isOnlyDetectionAlerts, kibanaDataViews, @@ -78,12 +92,12 @@ export const getDataViewSelectOptions = ({ ), - value: defaultDataView.id, + value: defaultDataViewId, }, ] : kibanaDataViews.map(({ title, id }) => ({ inputDisplay: - id === defaultDataView.id ? ( + id === defaultDataViewId ? ( {i18n.SECURITY_DEFAULT_DATA_VIEW_LABEL} {isModified && id === dataViewId && ( diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx index 763898378e6f4..7d3dc9641929a 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx @@ -19,8 +19,16 @@ import { } from '../../mock'; import { createStore } from '../../store'; import { EuiSuperSelectOption } from '@elastic/eui/src/components/form/super_select/super_select_control'; +import { waitFor } from '@testing-library/dom'; +import { useSourcererDataView } from '../../containers/sourcerer'; const mockDispatch = jest.fn(); + +jest.mock('../../containers/sourcerer'); +const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true); +jest.mock('./use_update_data_view', () => ({ + useUpdateDataView: () => mockUseUpdateDataView, +})); jest.mock('react-redux', () => { const original = jest.requireActual('react-redux'); @@ -30,6 +38,15 @@ jest.mock('react-redux', () => { }; }); +jest.mock('../../../../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../../../../src/plugins/kibana_react/public'); + + return { + ...original, + toMountPoint: jest.fn(), + }; +}); + const mockOptions = [ { label: 'apm-*-transaction*', value: 'apm-*-transaction*' }, { label: 'auditbeat-*', value: 'auditbeat-*' }, @@ -57,12 +74,21 @@ const patternListNoSignals = patternList .filter((p) => p !== mockGlobalState.sourcerer.signalIndexName) .sort(); let store: ReturnType; +const sourcererDataView = { + indicesExist: true, + loading: false, +}; + describe('Sourcerer component', () => { const { storage } = createSecuritySolutionStorageMock(); beforeEach(() => { store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + (useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView); jest.clearAllMocks(); + }); + + afterAll(() => { jest.restoreAllMocks(); }); @@ -215,7 +241,6 @@ describe('Sourcerer component', () => { ...mockGlobalState.sourcerer.sourcererScopes, [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], - loading: false, selectedDataViewId: '1234', selectedPatterns: ['filebeat-*'], }, @@ -267,7 +292,6 @@ describe('Sourcerer component', () => { ...mockGlobalState.sourcerer.sourcererScopes, [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], - loading: false, selectedDataViewId: id, selectedPatterns: patternListNoSignals.slice(0, 2), }, @@ -313,8 +337,6 @@ describe('Sourcerer component', () => { ...mockGlobalState.sourcerer.sourcererScopes, [SourcererScopeName.timeline]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], - loading: false, - patternList, selectedDataViewId: id, selectedPatterns: patternList.slice(0, 2), }, @@ -355,7 +377,6 @@ describe('Sourcerer component', () => { ...mockGlobalState.sourcerer.sourcererScopes, [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], - loading: false, selectedDataViewId: id, selectedPatterns: patternListNoSignals.slice(0, 2), }, @@ -629,6 +650,7 @@ describe('timeline sourcerer', () => { }; beforeAll(() => { + (useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView); wrapper = mount( @@ -713,6 +735,7 @@ describe('timeline sourcerer', () => { }; store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + wrapper = mount( @@ -754,6 +777,7 @@ describe('Sourcerer integration tests', () => { const { storage } = createSecuritySolutionStorageMock(); beforeEach(() => { + (useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView); store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); jest.clearAllMocks(); jest.restoreAllMocks(); @@ -795,11 +819,15 @@ describe('No data', () => { const { storage } = createSecuritySolutionStorageMock(); beforeEach(() => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + indicesExist: false, + }); store = createStore(mockNoIndicesState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); jest.clearAllMocks(); jest.restoreAllMocks(); }); - test('Hide sourcerer', () => { + test('Hide sourcerer - default ', () => { const wrapper = mount( @@ -808,4 +836,123 @@ describe('No data', () => { expect(wrapper.find(`[data-test-subj="sourcerer-trigger"]`).exists()).toEqual(false); }); + test('Hide sourcerer - detections ', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="sourcerer-trigger"]`).exists()).toEqual(false); + }); + test('Hide sourcerer - timeline ', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).exists()).toEqual(true); + }); +}); + +describe('Update available', () => { + const { storage } = createSecuritySolutionStorageMock(); + const state2 = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [ + mockGlobalState.sourcerer.defaultDataView, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '1234', + title: 'auditbeat-*', + patternList: ['auditbeat-*'], + }, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '12347', + title: 'packetbeat-*', + patternList: ['packetbeat-*'], + }, + ], + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.timeline]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], + loading: false, + patternList, + selectedDataViewId: null, + selectedPatterns: ['myFakebeat-*'], + missingPatterns: ['myFakebeat-*'], + }, + }, + }, + }; + + let wrapper: ReactWrapper; + + beforeEach(() => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + activePatterns: ['myFakebeat-*'], + }); + store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + wrapper = mount( + + + + ); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Show Update available label', () => { + expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-badge"]`).exists()).toBeTruthy(); + }); + + test('Show correct tooltip', () => { + expect(wrapper.find(`[data-test-subj="sourcerer-tooltip"]`).prop('content')).toEqual( + 'myFakebeat-*' + ); + }); + + test('Show UpdateDefaultDataViewModal', () => { + wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + + wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + + expect(wrapper.find(`UpdateDefaultDataViewModal`).prop('isShowing')).toEqual(true); + }); + + test('Show Add index pattern in UpdateDefaultDataViewModal', () => { + wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + + wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + + expect(wrapper.find(`button[data-test-subj="sourcerer-update-data-view"]`).text()).toEqual( + 'Add index pattern' + ); + }); + + test('Set all the index patterns from legacy timeline to sourcerer, after clicking on "Add index pattern"', async () => { + wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + + wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + + wrapper.find(`button[data-test-subj="sourcerer-update-data-view"]`).simulate('click'); + + await waitFor(() => wrapper.update()); + expect(mockDispatch).toHaveBeenCalledWith( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: 'security-solution', + selectedPatterns: ['myFakebeat-*'], + shouldValidateSelectedPatterns: false, + }) + ); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx index 89bbeef72a21c..2ffb0670c4edc 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx @@ -13,13 +13,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiForm, + EuiOutsideClickDetector, EuiPopover, EuiPopoverTitle, EuiSpacer, EuiSuperSelect, - EuiToolTip, } from '@elastic/eui'; -import deepEqual from 'fast-deep-equal'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -27,18 +26,13 @@ import * as i18n from './translations'; import { sourcererActions, sourcererModel, sourcererSelectors } from '../../store/sourcerer'; import { useDeepEqualSelector } from '../../hooks/use_selector'; import { SourcererScopeName } from '../../store/sourcerer/model'; -import { checkIfIndicesExist } from '../../store/sourcerer/helpers'; import { usePickIndexPatterns } from './use_pick_index_patterns'; -import { - FormRow, - getDataViewSelectOptions, - getTooltipContent, - PopoverContent, - ResetButton, - StyledBadge, - StyledButton, - StyledFormRow, -} from './helpers'; +import { FormRow, PopoverContent, ResetButton, StyledButton, StyledFormRow } from './helpers'; +import { TemporarySourcerer } from './temporary'; +import { UpdateDefaultDataViewModal } from './update_default_data_view_modal'; +import { useSourcererDataView } from '../../containers/sourcerer'; +import { useUpdateDataView } from './use_update_data_view'; +import { Trigger } from './trigger'; interface SourcererComponentProps { scope: sourcererModel.SourcererScopeName; @@ -54,13 +48,24 @@ export const Sourcerer = React.memo(({ scope: scopeId } defaultDataView, kibanaDataViews, signalIndexName, - sourcererScope: { selectedDataViewId, selectedPatterns, loading }, - sourcererDataView, + sourcererScope: { + selectedDataViewId, + selectedPatterns, + missingPatterns: sourcererMissingPatterns, + }, } = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId)); - const indicesExist = useMemo( - () => checkIfIndicesExist({ scopeId, signalIndexName, sourcererDataView }), - [scopeId, signalIndexName, sourcererDataView] + + const { activePatterns, indicesExist, loading } = useSourcererDataView(scopeId); + const [missingPatterns, setMissingPatterns] = useState( + activePatterns && activePatterns.length > 0 + ? sourcererMissingPatterns.filter((p) => activePatterns.includes(p)) + : [] ); + useEffect(() => { + if (activePatterns && activePatterns.length > 0) { + setMissingPatterns(sourcererMissingPatterns.filter((p) => activePatterns.includes(p))); + } + }, [activePatterns, sourcererMissingPatterns]); const [isOnlyDetectionAlertsChecked, setIsOnlyDetectionAlertsChecked] = useState( isTimelineSourcerer && selectedPatterns.join() === signalIndexName @@ -68,15 +73,15 @@ export const Sourcerer = React.memo(({ scope: scopeId } const isOnlyDetectionAlerts: boolean = isDetectionsSourcerer || (isTimelineSourcerer && isOnlyDetectionAlertsChecked); - const [isPopoverOpen, setPopoverIsOpen] = useState(false); - const [dataViewId, setDataViewId] = useState(selectedDataViewId ?? defaultDataView.id); + const [dataViewId, setDataViewId] = useState(selectedDataViewId); const { + allOptions, + dataViewSelectOptions, isModified, onChangeCombo, renderOption, - selectableOptions, selectedOptions, setIndexPatternsByDataView, } = usePickIndexPatterns({ @@ -84,10 +89,12 @@ export const Sourcerer = React.memo(({ scope: scopeId } defaultDataViewId: defaultDataView.id, isOnlyDetectionAlerts, kibanaDataViews, + missingPatterns, scopeId, selectedPatterns, signalIndexName, }); + const onCheckboxChanged = useCallback( (e) => { setIsOnlyDetectionAlertsChecked(e.target.checked); @@ -96,20 +103,26 @@ export const Sourcerer = React.memo(({ scope: scopeId } }, [defaultDataView.id, setIndexPatternsByDataView] ); - const isSavingDisabled = useMemo(() => selectedOptions.length === 0, [selectedOptions]); + const [expandAdvancedOptions, setExpandAdvancedOptions] = useState(false); + const [isShowingUpdateModal, setIsShowingUpdateModal] = useState(false); const setPopoverIsOpenCb = useCallback(() => { setPopoverIsOpen((prevState) => !prevState); setExpandAdvancedOptions(false); // we always want setExpandAdvancedOptions collapsed by default when popover opened }, []); const onChangeDataView = useCallback( - (newSelectedDataView: string, newSelectedPatterns: string[]) => { + ( + newSelectedDataView: string, + newSelectedPatterns: string[], + shouldValidateSelectedPatterns?: boolean + ) => { dispatch( sourcererActions.setSelectedDataView({ id: scopeId, selectedDataViewId: newSelectedDataView, selectedPatterns: newSelectedPatterns, + shouldValidateSelectedPatterns, }) ); }, @@ -128,11 +141,14 @@ export const Sourcerer = React.memo(({ scope: scopeId } setDataViewId(defaultDataView.id); setIndexPatternsByDataView(defaultDataView.id); setIsOnlyDetectionAlertsChecked(false); + setMissingPatterns([]); }, [defaultDataView.id, setIndexPatternsByDataView]); const handleSaveIndices = useCallback(() => { const patterns = selectedOptions.map((so) => so.label); - onChangeDataView(dataViewId, patterns); + if (dataViewId != null) { + onChangeDataView(dataViewId, patterns); + } setPopoverIsOpen(false); }, [onChangeDataView, dataViewId, selectedOptions]); @@ -140,183 +156,220 @@ export const Sourcerer = React.memo(({ scope: scopeId } setPopoverIsOpen(false); setExpandAdvancedOptions(false); }, []); - const trigger = useMemo( - () => ( - - {i18n.DATA_VIEW} - {isModified === 'modified' && {i18n.MODIFIED_BADGE_TITLE}} - {isModified === 'alerts' && ( - - {i18n.ALERTS_BADGE_TITLE} - - )} - - ), - [isTimelineSourcerer, loading, setPopoverIsOpenCb, isModified] - ); - const dataViewSelectOptions = useMemo( - () => - getDataViewSelectOptions({ - dataViewId, - defaultDataView, - isModified: isModified === 'modified', - isOnlyDetectionAlerts, - kibanaDataViews, - }), - [dataViewId, defaultDataView, isModified, isOnlyDetectionAlerts, kibanaDataViews] - ); + // deprecated timeline index pattern handlers + const onContinueUpdateDeprecated = useCallback(() => { + setIsShowingUpdateModal(false); + const patterns = selectedPatterns.filter((pattern) => + defaultDataView.patternList.includes(pattern) + ); + onChangeDataView(defaultDataView.id, patterns); + setPopoverIsOpen(false); + }, [defaultDataView.id, defaultDataView.patternList, onChangeDataView, selectedPatterns]); + + const onUpdateDeprecated = useCallback(() => { + // are all the patterns in the default? + if (missingPatterns.length === 0) { + onContinueUpdateDeprecated(); + } else { + // open modal + setIsShowingUpdateModal(true); + } + }, [missingPatterns, onContinueUpdateDeprecated]); + + const [isTriggerDisabled, setIsTriggerDisabled] = useState(false); + + const onOpenAndReset = useCallback(() => { + setPopoverIsOpen(true); + resetDataSources(); + }, [resetDataSources]); + + const updateDataView = useUpdateDataView(onOpenAndReset); + const onUpdateDataView = useCallback(async () => { + const isUiSettingsSuccess = await updateDataView(missingPatterns); + setIsShowingUpdateModal(false); + setPopoverIsOpen(false); + + if (isUiSettingsSuccess) { + onChangeDataView( + defaultDataView.id, + // to be at this stage, activePatterns is defined, the ?? selectedPatterns is to make TS happy + activePatterns ?? selectedPatterns, + false + ); + setIsTriggerDisabled(true); + } + }, [ + activePatterns, + defaultDataView.id, + missingPatterns, + onChangeDataView, + selectedPatterns, + updateDataView, + ]); useEffect(() => { - setDataViewId((prevSelectedOption) => - selectedDataViewId != null && !deepEqual(selectedDataViewId, prevSelectedOption) - ? selectedDataViewId - : prevSelectedOption - ); + setDataViewId(selectedDataViewId); }, [selectedDataViewId]); - const tooltipContent = useMemo( - () => - getTooltipContent({ - isOnlyDetectionAlerts, - isPopoverOpen, - selectedPatterns, - signalIndexName, - }), - [isPopoverOpen, isOnlyDetectionAlerts, signalIndexName, selectedPatterns] - ); - - const buttonWithTooptip = useMemo(() => { - return tooltipContent ? ( - - {trigger} - - ) : ( - trigger - ); - }, [trigger, tooltipContent]); + const onOutsideClick = useCallback(() => { + setDataViewId(selectedDataViewId); + setMissingPatterns(sourcererMissingPatterns); + }, [selectedDataViewId, sourcererMissingPatterns]); const onExpandAdvancedOptionsClicked = useCallback(() => { setExpandAdvancedOptions((prevState) => !prevState); }, []); - return indicesExist ? ( + // always show sourcerer in timeline + return indicesExist || scopeId === SourcererScopeName.timeline ? ( + } closePopover={handleClosePopOver} + data-test-subj={isTimelineSourcerer ? 'timeline-sourcerer-popover' : 'sourcerer-popover'} display="block" - repositionOnScroll + isOpen={isPopoverOpen} ownFocus + repositionOnScroll > - - - <>{i18n.SELECT_DATA_VIEW} - - {isOnlyDetectionAlerts && ( - - )} - - - {isTimelineSourcerer && ( - - - - )} - - - + + + <>{i18n.SELECT_DATA_VIEW} + + {isOnlyDetectionAlerts && ( + - - - - - - {i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE} - - {expandAdvancedOptions && } - - - + )} + + {isModified === 'deprecated' || isModified === 'missingPatterns' ? ( + <> + + setIsShowingUpdateModal(false)} + onContinue={onContinueUpdateDeprecated} + onUpdate={onUpdateDataView} + /> + + ) : ( + + <> + {isTimelineSourcerer && ( + + + + )} + {dataViewId && ( + + + + )} - {!isDetectionsSourcerer && ( - - - - - {i18n.INDEX_PATTERNS_RESET} - - - - + + {i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE} + + {expandAdvancedOptions && } + + - {i18n.SAVE_INDEX_PATTERNS} - - - - + isDisabled={isOnlyDetectionAlerts} + onChange={onChangeCombo} + options={allOptions} + placeholder={i18n.PICK_INDEX_PATTERNS} + renderOption={renderOption} + selectedOptions={selectedOptions} + /> + + + {!isDetectionsSourcerer && ( + + + + + {i18n.INDEX_PATTERNS_RESET} + + + + + {i18n.SAVE_INDEX_PATTERNS} + + + + + )} + + + )} - - - + + ) : null; }); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/refresh_button.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/refresh_button.tsx new file mode 100644 index 0000000000000..c30c6aa2dea9c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/refresh_button.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 { EuiButton } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; + +import { RELOAD_PAGE_TITLE } from './translations'; + +const StyledRefreshButton = styled(EuiButton)` + float: right; +`; + +export const RefreshButton = React.memo(() => { + const onPageRefresh = useCallback(() => { + document.location.reload(); + }, []); + return ( + + {RELOAD_PAGE_TITLE} + + ); +}); + +RefreshButton.displayName = 'RefreshButton'; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/temporary.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/temporary.tsx new file mode 100644 index 0000000000000..36fae76c7739b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/temporary.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiCallOut, + EuiLink, + EuiText, + EuiTextColor, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiToolTip, + EuiIcon, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; +import * as i18n from './translations'; +import { Blockquote, ResetButton } from './helpers'; + +interface Props { + activePatterns?: string[]; + indicesExist: boolean; + isModified: 'deprecated' | 'missingPatterns'; + missingPatterns: string[]; + onClick: () => void; + onClose: () => void; + onUpdate: () => void; + selectedPatterns: string[]; +} + +const translations = { + deprecated: { + title: i18n.CALL_OUT_DEPRECATED_TITLE, + update: i18n.UPDATE_INDEX_PATTERNS, + }, + missingPatterns: { + title: i18n.CALL_OUT_MISSING_PATTERNS_TITLE, + update: i18n.ADD_INDEX_PATTERN, + }, +}; + +export const TemporarySourcerer = React.memo( + ({ + activePatterns, + indicesExist, + isModified, + onClose, + onClick, + onUpdate, + selectedPatterns, + missingPatterns, + }) => { + const trigger = useMemo( + () => ( + + {translations[isModified].update} + + ), + [indicesExist, isModified, onUpdate] + ); + const buttonWithTooltip = useMemo( + () => + !indicesExist ? ( + + {trigger} + + ) : ( + trigger + ), + [indicesExist, trigger] + ); + + const deadPatterns = + activePatterns && activePatterns.length > 0 + ? selectedPatterns.filter((p) => !activePatterns.includes(p)) + : []; + + return ( + <> + + + + +

+ {activePatterns && activePatterns.length > 0 ? ( + 0 ? ( + !activePatterns.includes(p)) + .join(', '), + }} + /> + } + > + + + ) : null, + callout:

{activePatterns.join(', ')}
, + }} + /> + ) : ( + {selectedPatterns.join(', ')}, + }} + /> + )} + + {isModified === 'deprecated' && ( + {i18n.TOGGLE_TO_NEW_SOURCERER}
, + }} + /> + )} + {isModified === 'missingPatterns' && ( + <> + {missingPatterns.join(', ')}, + }} + /> + {i18n.TOGGLE_TO_NEW_SOURCERER}, + }} + /> + + )} +

+ + + + + + {i18n.INDEX_PATTERNS_CLOSE} + + + {buttonWithTooltip} + + + ); + } +); + +TemporarySourcerer.displayName = 'TemporarySourcerer'; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts index fcf465ebfc9ef..2d8e506f39437 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts @@ -11,6 +11,20 @@ export const CALL_OUT_TITLE = i18n.translate('xpack.securitySolution.indexPatter defaultMessage: 'Data view cannot be modified on this page', }); +export const CALL_OUT_DEPRECATED_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.callOutDeprecxatedTitle', + { + defaultMessage: 'This timeline uses a legacy data view selector', + } +); + +export const CALL_OUT_MISSING_PATTERNS_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.callOutMissingPatternsTitle', + { + defaultMessage: 'This timeline is out of date with the Security Data View', + } +); + export const CALL_OUT_TIMELINE_TITLE = i18n.translate( 'xpack.securitySolution.indexPatterns.callOutTimelineTitle', { @@ -18,9 +32,42 @@ export const CALL_OUT_TIMELINE_TITLE = i18n.translate( } ); +export const TOGGLE_TO_NEW_SOURCERER = i18n.translate( + 'xpack.securitySolution.indexPatterns.toggleToNewSourcerer.link', + { + defaultMessage: 'here', + } +); + export const DATA_VIEW = i18n.translate('xpack.securitySolution.indexPatterns.dataViewLabel', { defaultMessage: 'Data view', }); + +export const UPDATE_DATA_VIEW = i18n.translate( + 'xpack.securitySolution.indexPatterns.updateDataView', + { + defaultMessage: + 'Would you like to add this index pattern to Security Data View? Otherwise, we can recreate the data view without the missing index patterns.', + } +); + +export const UPDATE_SECURITY_DATA_VIEW = i18n.translate( + 'xpack.securitySolution.indexPatterns.updateSecurityDataView', + { + defaultMessage: 'Update Security Data View', + } +); + +export const CONTINUE_WITHOUT_ADDING = i18n.translate( + 'xpack.securitySolution.indexPatterns.continue', + { + defaultMessage: 'Continue without adding', + } +); +export const ADD_INDEX_PATTERN = i18n.translate('xpack.securitySolution.indexPatterns.add', { + defaultMessage: 'Add index pattern', +}); + export const MODIFIED_BADGE_TITLE = i18n.translate( 'xpack.securitySolution.indexPatterns.modifiedBadgeTitle', { @@ -35,6 +82,13 @@ export const ALERTS_BADGE_TITLE = i18n.translate( } ); +export const DEPRECATED_BADGE_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.updateAvailableBadgeTitle', + { + defaultMessage: 'Update available', + } +); + export const SECURITY_DEFAULT_DATA_VIEW_LABEL = i18n.translate( 'xpack.securitySolution.indexPatterns.securityDefaultDataViewLabel', { @@ -97,6 +151,14 @@ export const DISABLED_INDEX_PATTERNS = i18n.translate( } ); +export const DISABLED_SOURCERER = i18n.translate('xpack.securitySolution.sourcerer.disabled', { + defaultMessage: 'The updates to the Data view require a page reload to take effect.', +}); + +export const UPDATE_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.update', { + defaultMessage: 'Update and recreate data view', +}); + export const INDEX_PATTERNS_RESET = i18n.translate( 'xpack.securitySolution.indexPatterns.resetButton', { @@ -104,6 +166,22 @@ export const INDEX_PATTERNS_RESET = i18n.translate( } ); +export const INDEX_PATTERNS_CLOSE = i18n.translate( + 'xpack.securitySolution.indexPatterns.closeButton', + { + defaultMessage: 'Close', + } +); + +export const INACTIVE_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.inactive', { + defaultMessage: 'Inactive index patterns', +}); + +export const NO_DATA = i18n.translate('xpack.securitySolution.indexPatterns.noData', { + defaultMessage: + "The index pattern on this timeline doesn't match any data streams, indices, or index aliases.", +}); + export const PICK_INDEX_PATTERNS = i18n.translate( 'xpack.securitySolution.indexPatterns.pickIndexPatternsCombo', { @@ -117,3 +195,24 @@ export const ALERTS_CHECKBOX_LABEL = i18n.translate( defaultMessage: 'Show only detection alerts', } ); + +export const SUCCESS_TOAST_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.successToastTitle', + { + defaultMessage: 'One or more settings require you to reload the page to take effect', + } +); + +export const RELOAD_PAGE_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.reloadPageTitle', + { + defaultMessage: 'Reload page', + } +); + +export const FAILURE_TOAST_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.failureToastTitle', + { + defaultMessage: 'Unable to update data view', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/trigger.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/trigger.tsx new file mode 100644 index 0000000000000..a464036f3b138 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/trigger.tsx @@ -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 React, { FC, memo, useMemo } from 'react'; +import { EuiToolTip } from '@elastic/eui'; +import * as i18n from './translations'; +import { getTooltipContent, StyledBadge, StyledButton } from './helpers'; +import { ModifiedTypes } from './use_pick_index_patterns'; + +interface Props { + activePatterns?: string[]; + disabled: boolean; + isModified: ModifiedTypes; + isOnlyDetectionAlerts: boolean; + isPopoverOpen: boolean; + isTimelineSourcerer: boolean; + loading: boolean; + onClick: () => void; + selectedPatterns: string[]; + signalIndexName: string | null; +} +export const TriggerComponent: FC = ({ + activePatterns, + disabled, + isModified, + isOnlyDetectionAlerts, + isPopoverOpen, + isTimelineSourcerer, + loading, + onClick, + selectedPatterns, + signalIndexName, +}) => { + const badge = useMemo(() => { + switch (isModified) { + case 'modified': + return {i18n.MODIFIED_BADGE_TITLE}; + case 'alerts': + return ( + + {i18n.ALERTS_BADGE_TITLE} + + ); + case 'deprecated': + return ( + + {i18n.DEPRECATED_BADGE_TITLE} + + ); + case 'missingPatterns': + return ( + + {i18n.DEPRECATED_BADGE_TITLE} + + ); + case '': + default: + return null; + } + }, [isModified]); + + const trigger = useMemo( + () => ( + + {i18n.DATA_VIEW} + {!disabled && badge} + + ), + [disabled, badge, isTimelineSourcerer, loading, onClick] + ); + + const tooltipContent = useMemo( + () => + disabled + ? i18n.DISABLED_SOURCERER + : getTooltipContent({ + isOnlyDetectionAlerts, + isPopoverOpen, + // if activePatterns, use because we are in the temporary sourcerer state + selectedPatterns: activePatterns ?? selectedPatterns, + signalIndexName, + }), + [ + activePatterns, + disabled, + isOnlyDetectionAlerts, + isPopoverOpen, + selectedPatterns, + signalIndexName, + ] + ); + + return tooltipContent ? ( + + {trigger} + + ) : ( + trigger + ); +}; + +export const Trigger = memo(TriggerComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/update_default_data_view_modal.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/update_default_data_view_modal.tsx new file mode 100644 index 0000000000000..78fc6f82fa748 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/update_default_data_view_modal.tsx @@ -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. + */ + +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiText, + EuiTextColor, +} from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import * as i18n from './translations'; +import { Blockquote, ResetButton } from './helpers'; + +interface Props { + isShowing: boolean; + missingPatterns: string[]; + onClose: () => void; + onContinue: () => void; + onUpdate: () => void; +} +const MyEuiModal = styled(EuiModal)` + .euiModal__flex { + width: 60vw; + } + .euiCodeBlock { + height: auto !important; + max-width: 718px; + } + z-index: 99999999; +`; + +export const UpdateDefaultDataViewModal = React.memo( + ({ isShowing, onClose, onContinue, onUpdate, missingPatterns }) => + isShowing ? ( + + + +

{i18n.UPDATE_SECURITY_DATA_VIEW}

+
+
+ + + +

+ {missingPatterns.join(', ')}, + }} + /> + {i18n.UPDATE_DATA_VIEW} +

+
+
+ + + + {i18n.CONTINUE_WITHOUT_ADDING} + + + + + {i18n.ADD_INDEX_PATTERN} + + + +
+
+ ) : null +); + +UpdateDefaultDataViewModal.displayName = 'UpdateDefaultDataViewModal'; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx index 2ed2319499398..d7b094ab27b14 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx @@ -6,29 +6,31 @@ */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; +import { EuiComboBoxOptionOption, EuiSuperSelectOption } from '@elastic/eui'; import { getScopePatternListSelection } from '../../store/sourcerer/helpers'; import { sourcererModel } from '../../store/sourcerer'; -import { getPatternListWithoutSignals } from './helpers'; +import { getDataViewSelectOptions, getPatternListWithoutSignals } from './helpers'; import { SourcererScopeName } from '../../store/sourcerer/model'; interface UsePickIndexPatternsProps { - dataViewId: string; + dataViewId: string | null; defaultDataViewId: string; isOnlyDetectionAlerts: boolean; kibanaDataViews: sourcererModel.SourcererModel['kibanaDataViews']; + missingPatterns: string[]; scopeId: sourcererModel.SourcererScopeName; selectedPatterns: string[]; signalIndexName: string | null; } -export type ModifiedTypes = 'modified' | 'alerts' | ''; +export type ModifiedTypes = 'modified' | 'alerts' | 'deprecated' | 'missingPatterns' | ''; interface UsePickIndexPatterns { + allOptions: Array>; + dataViewSelectOptions: Array>; isModified: ModifiedTypes; onChangeCombo: (newSelectedDataViewId: Array>) => void; renderOption: ({ value }: EuiComboBoxOptionOption) => React.ReactElement; - selectableOptions: Array>; selectedOptions: Array>; setIndexPatternsByDataView: (newSelectedDataViewId: string, isAlerts?: boolean) => void; } @@ -45,6 +47,7 @@ export const usePickIndexPatterns = ({ defaultDataViewId, isOnlyDetectionAlerts, kibanaDataViews, + missingPatterns, scopeId, selectedPatterns, signalIndexName, @@ -54,42 +57,44 @@ export const usePickIndexPatterns = ({ [signalIndexName] ); - const { patternList, selectablePatterns } = useMemo(() => { + const { allPatterns, selectablePatterns } = useMemo<{ + allPatterns: string[]; + selectablePatterns: string[]; + }>(() => { if (isOnlyDetectionAlerts && signalIndexName) { return { - patternList: [signalIndexName], + allPatterns: [signalIndexName], selectablePatterns: [signalIndexName], }; } const theDataView = kibanaDataViews.find((dataView) => dataView.id === dataViewId); - return theDataView != null - ? scopeId === sourcererModel.SourcererScopeName.default - ? { - patternList: getPatternListWithoutSignals( - theDataView.title - .split(',') - // remove duplicates patterns from selector - .filter((pattern, i, self) => self.indexOf(pattern) === i), - signalIndexName - ), - selectablePatterns: getPatternListWithoutSignals( - theDataView.patternList, - signalIndexName - ), - } - : { - patternList: theDataView.title - .split(',') - // remove duplicates patterns from selector - .filter((pattern, i, self) => self.indexOf(pattern) === i), - selectablePatterns: theDataView.patternList, - } - : { patternList: [], selectablePatterns: [] }; + + if (theDataView == null) { + return { + allPatterns: [], + selectablePatterns: [], + }; + } + + const titleAsList = [...new Set(theDataView.title.split(','))]; + + return scopeId === sourcererModel.SourcererScopeName.default + ? { + allPatterns: getPatternListWithoutSignals(titleAsList, signalIndexName), + selectablePatterns: getPatternListWithoutSignals( + theDataView.patternList, + signalIndexName + ), + } + : { + allPatterns: titleAsList, + selectablePatterns: theDataView.patternList, + }; }, [dataViewId, isOnlyDetectionAlerts, kibanaDataViews, scopeId, signalIndexName]); - const selectableOptions = useMemo( - () => patternListToOptions(patternList, selectablePatterns), - [patternList, selectablePatterns] + const allOptions = useMemo( + () => patternListToOptions(allPatterns, selectablePatterns), + [allPatterns, selectablePatterns] ); const [selectedOptions, setSelectedOptions] = useState>>( isOnlyDetectionAlerts ? alertsOptions : patternListToOptions(selectedPatterns) @@ -111,37 +116,50 @@ export const usePickIndexPatterns = ({ ); const defaultSelectedPatternsAsOptions = useMemo( - () => getDefaultSelectedOptionsByDataView(dataViewId), + () => (dataViewId != null ? getDefaultSelectedOptionsByDataView(dataViewId) : []), [dataViewId, getDefaultSelectedOptionsByDataView] ); - const [isModified, setIsModified] = useState<'modified' | 'alerts' | ''>(''); + const [isModified, setIsModified] = useState( + dataViewId == null ? 'deprecated' : missingPatterns.length > 0 ? 'missingPatterns' : '' + ); const onSetIsModified = useCallback( - (patterns?: string[]) => { + (patterns: string[], id: string | null) => { + if (id == null) { + return setIsModified('deprecated'); + } + if (missingPatterns.length > 0) { + return setIsModified('missingPatterns'); + } if (isOnlyDetectionAlerts) { return setIsModified('alerts'); } - const modifiedPatterns = patterns != null ? patterns : selectedPatterns; const isPatternsModified = - defaultSelectedPatternsAsOptions.length !== modifiedPatterns.length || + defaultSelectedPatternsAsOptions.length !== patterns.length || !defaultSelectedPatternsAsOptions.every((option) => - modifiedPatterns.find((pattern) => option.value === pattern) + patterns.find((pattern) => option.value === pattern) ); return setIsModified(isPatternsModified ? 'modified' : ''); }, - [defaultSelectedPatternsAsOptions, isOnlyDetectionAlerts, selectedPatterns] + [defaultSelectedPatternsAsOptions, isOnlyDetectionAlerts, missingPatterns.length] ); - // when scope updates, check modified to set/remove alerts label useEffect(() => { setSelectedOptions( scopeId === SourcererScopeName.detections ? alertsOptions : patternListToOptions(selectedPatterns) ); - onSetIsModified(selectedPatterns); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [scopeId, selectedPatterns]); + }, [selectedPatterns, scopeId]); + // when scope updates, check modified to set/remove alerts label + useEffect(() => { + onSetIsModified( + selectedOptions.map(({ label }) => label), + dataViewId + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataViewId, missingPatterns, scopeId, selectedOptions]); const onChangeCombo = useCallback((newSelectedOptions) => { setSelectedOptions(newSelectedOptions); @@ -156,11 +174,26 @@ export const usePickIndexPatterns = ({ setSelectedOptions(getDefaultSelectedOptionsByDataView(newSelectedDataViewId, isAlerts)); }; + const dataViewSelectOptions = useMemo( + () => + dataViewId != null + ? getDataViewSelectOptions({ + dataViewId, + defaultDataViewId, + isModified: isModified === 'modified', + isOnlyDetectionAlerts, + kibanaDataViews, + }) + : [], + [dataViewId, defaultDataViewId, isModified, isOnlyDetectionAlerts, kibanaDataViews] + ); + return { + allOptions, + dataViewSelectOptions, isModified, onChangeCombo, renderOption, - selectableOptions, selectedOptions, setIndexPatternsByDataView, }; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/use_update_data_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_update_data_view.test.tsx new file mode 100644 index 0000000000000..4ec39a60a97b9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_update_data_view.test.tsx @@ -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 { renderHook } from '@testing-library/react-hooks'; +import { useUpdateDataView } from './use_update_data_view'; +import { useKibana } from '../../lib/kibana'; +import * as i18n from './translations'; +const mockAddSuccess = jest.fn(); +const mockAddError = jest.fn(); +const mockSet = jest.fn(); +const mockPatterns = ['packetbeat-*', 'winlogbeat-*']; +jest.mock('../../hooks/use_app_toasts', () => { + const original = jest.requireActual('../../hooks/use_app_toasts'); + + return { + ...original, + useAppToasts: () => ({ + addSuccess: mockAddSuccess, + addError: mockAddError, + }), + }; +}); +jest.mock('../../lib/kibana'); +jest.mock('../../../../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../../../../src/plugins/kibana_react/public'); + + return { + ...original, + toMountPoint: jest.fn(), + }; +}); +describe('use_update_data_view', () => { + const mockError = jest.fn(); + beforeEach(() => { + (useKibana as jest.Mock).mockImplementation(() => ({ + services: { + uiSettings: { + get: () => mockPatterns, + set: mockSet.mockResolvedValue(true), + }, + }, + })); + jest.clearAllMocks(); + }); + + test('Successful uiSettings updates with correct index pattern, and shows success toast', async () => { + const { result } = renderHook(() => useUpdateDataView(mockError)); + const updateDataView = result.current; + const isUiSettingsSuccess = await updateDataView(['missing-*']); + expect(mockSet.mock.calls[0][1]).toEqual([...mockPatterns, 'missing-*'].sort()); + expect(isUiSettingsSuccess).toEqual(true); + expect(mockAddSuccess).toHaveBeenCalled(); + }); + + test('Failed uiSettings update returns false and shows error toast', async () => { + (useKibana as jest.Mock).mockImplementation(() => ({ + services: { + uiSettings: { + get: () => mockPatterns, + set: mockSet.mockResolvedValue(false), + }, + }, + })); + const { result } = renderHook(() => useUpdateDataView(mockError)); + const updateDataView = result.current; + const isUiSettingsSuccess = await updateDataView(['missing-*']); + expect(mockSet.mock.calls[0][1]).toEqual([...mockPatterns, 'missing-*'].sort()); + expect(isUiSettingsSuccess).toEqual(false); + expect(mockAddError).toHaveBeenCalled(); + expect(mockAddError.mock.calls[0][0]).toEqual(new Error(i18n.FAILURE_TOAST_TITLE)); + }); + + test('Failed uiSettings throws error and shows error toast', async () => { + (useKibana as jest.Mock).mockImplementation(() => ({ + services: { + uiSettings: { + get: jest.fn().mockImplementation(() => { + throw new Error('Uh oh bad times over here'); + }), + set: mockSet.mockResolvedValue(true), + }, + }, + })); + const { result } = renderHook(() => useUpdateDataView(mockError)); + const updateDataView = result.current; + const isUiSettingsSuccess = await updateDataView(['missing-*']); + expect(isUiSettingsSuccess).toEqual(false); + expect(mockAddError).toHaveBeenCalled(); + expect(mockAddError.mock.calls[0][0]).toEqual(new Error('Uh oh bad times over here')); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/use_update_data_view.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_update_data_view.tsx new file mode 100644 index 0000000000000..68193942ea257 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_update_data_view.tsx @@ -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 React, { useCallback } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '../../lib/kibana'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { ensurePatternFormat } from '../../store/sourcerer/helpers'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import * as i18n from './translations'; +import { RefreshButton } from './refresh_button'; +import { useAppToasts } from '../../hooks/use_app_toasts'; + +export const useUpdateDataView = ( + onOpenAndReset: () => void +): ((missingPatterns: string[]) => Promise) => { + const { uiSettings } = useKibana().services; + const { addSuccess, addError } = useAppToasts(); + return useCallback( + async (missingPatterns: string[]): Promise => { + const asyncSearch = async (): Promise<[boolean, Error | null]> => { + try { + const defaultPatterns = uiSettings.get(DEFAULT_INDEX_KEY); + const uiSettingsIndexPattern = [...defaultPatterns, ...missingPatterns]; + const isSuccess = await uiSettings.set( + DEFAULT_INDEX_KEY, + ensurePatternFormat(uiSettingsIndexPattern) + ); + return [isSuccess, null]; + } catch (e) { + return [false, e]; + } + }; + const [isUiSettingsSuccess, possibleError] = await asyncSearch(); + if (isUiSettingsSuccess) { + addSuccess({ + color: 'success', + title: toMountPoint(i18n.SUCCESS_TOAST_TITLE), + text: toMountPoint(), + iconType: undefined, + toastLifeTimeMs: 600000, + }); + return true; + } + addError(possibleError !== null ? possibleError : new Error(i18n.FAILURE_TOAST_TITLE), { + title: i18n.FAILURE_TOAST_TITLE, + toastMessage: ( + <> + + {i18n.TOGGLE_TO_NEW_SOURCERER} + + ), + }} + /> + + ) as unknown as string, + }); + return false; + }, + [addError, addSuccess, onOpenAndReset, uiSettings] + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx index 7c87aa19484bc..0f7e93f1befca 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx @@ -64,7 +64,7 @@ export const useSetInitialStateFromUrl = () => { dispatch( sourcererActions.setSelectedDataView({ id: scope, - selectedDataViewId: sourcererState[scope]?.id ?? '', + selectedDataViewId: sourcererState[scope]?.id ?? null, selectedPatterns: sourcererState[scope]?.selectedPatterns ?? [], }) ) 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/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index 3311207eb1420..c493cb528d09a 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -5,12 +5,16 @@ * 2.0. */ -import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { matchPath } from 'react-router-dom'; import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; -import { SelectedDataView, SourcererScopeName } from '../../store/sourcerer/model'; +import { + SelectedDataView, + SourcererDataView, + SourcererScopeName, +} from '../../store/sourcerer/model'; import { useUserInfo } from '../../../detections/components/user_info'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { @@ -28,6 +32,7 @@ import { checkIfIndicesExist, getScopePatternListSelection } from '../../store/s import { useAppToasts } from '../../hooks/use_app_toasts'; import { postSourcererDataView } from './api'; import { useDataView } from '../source/use_data_view'; +import { useFetchIndex } from '../source'; export const useInitSourcerer = ( scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default @@ -37,11 +42,14 @@ export const useInitSourcerer = ( const initialTimelineSourcerer = useRef(true); const initialDetectionSourcerer = useRef(true); const { loading: loadingSignalIndex, isSignalIndexExists, signalIndexName } = useUserInfo(); - const getDefaultDataViewSelector = useMemo( - () => sourcererSelectors.defaultDataViewSelector(), + + const getDataViewsSelector = useMemo( + () => sourcererSelectors.getSourcererDataViewsSelector(), [] ); - const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector); + const { defaultDataView, signalIndexName: signalIndexNameSourcerer } = useDeepEqualSelector( + (state) => getDataViewsSelector(state) + ); const { addError } = useAppToasts(); @@ -59,12 +67,6 @@ export const useInitSourcerer = ( } }, [addError, defaultDataView.error]); - const getSignalIndexNameSelector = useMemo( - () => sourcererSelectors.signalIndexNameSelector(), - [] - ); - const signalIndexNameSourcerer = useDeepEqualSelector(getSignalIndexNameSelector); - const getTimelineSelector = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const activeTimeline = useDeepEqualSelector((state) => getTimelineSelector(state, TimelineId.active) @@ -256,14 +258,26 @@ export const EXCLUDE_ELASTIC_CLOUD_INDEX = '-*elastic-cloud-logs-*'; export const useSourcererDataView = ( scopeId: SourcererScopeName = SourcererScopeName.default ): SelectedDataView => { - const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); + const { getDataViewsSelector, getSourcererDataViewSelector, getScopeSelector } = useMemo( + () => ({ + getDataViewsSelector: sourcererSelectors.getSourcererDataViewsSelector(), + getSourcererDataViewSelector: sourcererSelectors.sourcererDataViewSelector(), + getScopeSelector: sourcererSelectors.scopeIdSelector(), + }), + [] + ); const { signalIndexName, - sourcererDataView: selectedDataView, - sourcererScope: { selectedPatterns: scopeSelectedPatterns, loading }, - }: sourcererSelectors.SourcererScopeSelector = useDeepEqualSelector((state) => - sourcererScopeSelector(state, scopeId) - ); + selectedDataView, + sourcererScope: { missingPatterns, selectedPatterns: scopeSelectedPatterns, loading }, + }: sourcererSelectors.SourcererScopeSelector = useDeepEqualSelector((state) => { + const sourcererScope = getScopeSelector(state, scopeId); + return { + ...getDataViewsSelector(state), + selectedDataView: getSourcererDataViewSelector(state, sourcererScope.selectedDataViewId), + sourcererScope, + }; + }); const selectedPatterns = useMemo( () => @@ -273,40 +287,69 @@ export const useSourcererDataView = ( [scopeSelectedPatterns] ); + const [legacyPatterns, setLegacyPatterns] = useState([]); + + const [indexPatternsLoading, fetchIndexReturn] = useFetchIndex(legacyPatterns); + + const legacyDataView: Omit & { id: string | null } = useMemo( + () => ({ + ...fetchIndexReturn, + runtimeMappings: {}, + title: '', + id: selectedDataView?.id ?? null, + loading: indexPatternsLoading, + patternList: fetchIndexReturn.indexes, + indexFields: fetchIndexReturn.indexPatterns + .fields as SelectedDataView['indexPattern']['fields'], + }), + [fetchIndexReturn, indexPatternsLoading, selectedDataView] + ); + + useEffect(() => { + if (selectedDataView == null || missingPatterns.length > 0) { + // old way of fetching indices, legacy timeline + setLegacyPatterns(selectedPatterns); + } else { + setLegacyPatterns([]); + } + }, [missingPatterns, selectedDataView, selectedPatterns]); + + const sourcererDataView = useMemo( + () => + selectedDataView == null || missingPatterns.length > 0 ? legacyDataView : selectedDataView, + [legacyDataView, missingPatterns.length, selectedDataView] + ); + const indicesExist = useMemo( - () => checkIfIndicesExist({ scopeId, signalIndexName, sourcererDataView: selectedDataView }), - [scopeId, signalIndexName, selectedDataView] + () => + checkIfIndicesExist({ + scopeId, + signalIndexName, + patternList: sourcererDataView.patternList, + }), + [scopeId, signalIndexName, sourcererDataView] ); return useMemo( () => ({ - browserFields: selectedDataView.browserFields, - dataViewId: selectedDataView.id, - docValueFields: selectedDataView.docValueFields, + browserFields: sourcererDataView.browserFields, + dataViewId: sourcererDataView.id, + docValueFields: sourcererDataView.docValueFields, indexPattern: { - fields: selectedDataView.indexFields, + fields: sourcererDataView.indexFields, title: selectedPatterns.join(','), }, indicesExist, - loading: loading || selectedDataView.loading, - runtimeMappings: selectedDataView.runtimeMappings, + loading: loading || sourcererDataView.loading, + runtimeMappings: sourcererDataView.runtimeMappings, // all active & inactive patterns in DATA_VIEW - patternList: selectedDataView.title.split(','), - // selected patterns in DATA_VIEW + patternList: sourcererDataView.title.split(','), + // selected patterns in DATA_VIEW including filter selectedPatterns: selectedPatterns.sort(), + // if we have to do an update to data view, tell us which patterns are active + ...(legacyPatterns.length > 0 ? { activePatterns: sourcererDataView.patternList } : {}), }), - [ - selectedDataView.browserFields, - selectedDataView.id, - selectedDataView.docValueFields, - selectedDataView.indexFields, - selectedDataView.loading, - selectedDataView.runtimeMappings, - selectedDataView.title, - selectedPatterns, - indicesExist, - loading, - ] + [sourcererDataView, selectedPatterns, indicesExist, loading, legacyPatterns.length] ); }; 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/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 0f814d758e7f5..2de29a8c3acf8 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -1955,7 +1955,7 @@ export const mockTimelineModel: TimelineModel = { columns: mockTimelineModelColumns, defaultColumns: mockTimelineModelColumns, dataProviders: [], - dataViewId: '', + dataViewId: null, dateRange: { end: '2020-03-18T13:52:38.929Z', start: '2020-03-18T13:46:38.929Z', @@ -2092,7 +2092,7 @@ export const defaultTimelineProps: CreateTimelineProps = { queryMatch: { field: '_id', operator: ':', value: '1' }, }, ], - dataViewId: '', + dataViewId: null, dateRange: { end: '2018-11-05T19:03:25.937Z', start: '2018-11-05T18:58:25.937Z' }, deletedEventIds: [], description: '', diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts b/x-pack/plugins/security_solution/public/common/store/reducer.test.ts index 1cbf08c354b33..e46a4a532d701 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.test.ts @@ -8,7 +8,7 @@ import { parseExperimentalConfigValue } from '../../../common/experimental_features'; import { SecuritySubPlugins } from '../../app/types'; import { createInitialState } from './reducer'; -import { mockSourcererState } from '../mock'; +import { mockIndexPattern, mockSourcererState } from '../mock'; import { useSourcererDataView } from '../containers/sourcerer'; import { useDeepEqualSelector } from '../hooks/use_selector'; import { renderHook } from '@testing-library/react-hooks'; @@ -19,6 +19,12 @@ jest.mock('../lib/kibana', () => ({ get: jest.fn(() => ({ uiSettings: { get: () => ({ from: 'now-24h', to: 'now' }) } })), }, })); +jest.mock('../containers/source', () => ({ + useFetchIndex: () => [ + false, + { indexes: [], indicesExist: true, indexPatterns: mockIndexPattern }, + ], +})); describe('createInitialState', () => { describe('sourcerer -> default -> indicesExist', () => { @@ -40,20 +46,24 @@ describe('createInitialState', () => { (useDeepEqualSelector as jest.Mock).mockClear(); }); - test('indicesExist should be TRUE if configIndexPatterns is NOT empty', async () => { + test('indicesExist should be TRUE if patternList is NOT empty', async () => { const { result } = renderHook(() => useSourcererDataView()); expect(result.current.indicesExist).toEqual(true); }); - test('indicesExist should be FALSE if configIndexPatterns is empty', () => { + test('indicesExist should be FALSE if patternList is empty', () => { const state = createInitialState(mockPluginState, { ...defaultState, defaultDataView: { ...defaultState.defaultDataView, - id: '', - title: '', patternList: [], }, + kibanaDataViews: [ + { + ...defaultState.defaultDataView, + patternList: [], + }, + ], }); (useDeepEqualSelector as jest.Mock).mockImplementation((cb) => cb(state)); const { result } = renderHook(() => useSourcererDataView()); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts index aa0689de9cca3..6a3d3e71f3750 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts @@ -6,9 +6,8 @@ */ import actionCreatorFactory from 'typescript-fsa'; -import { TimelineEventsType } from '../../../../common/types/timeline'; -import { SourcererDataView, SourcererScopeName } from './model'; +import { SelectedDataView, SourcererDataView, SourcererScopeName } from './model'; import { SecurityDataView } from '../../containers/sourcerer/api'; const actionCreator = actionCreatorFactory('x-pack/security_solution/local/sourcerer'); @@ -39,8 +38,8 @@ export const setSourcererScopeLoading = actionCreator<{ export interface SelectedDataViewPayload { id: SourcererScopeName; - selectedDataViewId: string; - selectedPatterns: string[]; - eventType?: TimelineEventsType; + selectedDataViewId: SelectedDataView['dataViewId']; + selectedPatterns: SelectedDataView['selectedPatterns']; + shouldValidateSelectedPatterns?: boolean; } export const setSelectedDataView = actionCreator('SET_SELECTED_DATA_VIEW'); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts index 5945b453673c3..672ecb575ce79 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts @@ -69,7 +69,7 @@ describe('sourcerer store helpers', () => { selectedPatterns: ['auditbeat-*'], }; it('sets selectedPattern', () => { - const result = validateSelectedPatterns(mockGlobalState.sourcerer, payload); + const result = validateSelectedPatterns(mockGlobalState.sourcerer, payload, true); expect(result).toEqual({ [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], @@ -78,10 +78,14 @@ describe('sourcerer store helpers', () => { }); }); it('sets to default when empty array is passed and scope is default', () => { - const result = validateSelectedPatterns(mockGlobalState.sourcerer, { - ...payload, - selectedPatterns: [], - }); + const result = validateSelectedPatterns( + mockGlobalState.sourcerer, + { + ...payload, + selectedPatterns: [], + }, + true + ); expect(result).toEqual({ [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], @@ -90,11 +94,15 @@ describe('sourcerer store helpers', () => { }); }); it('sets to default when empty array is passed and scope is detections', () => { - const result = validateSelectedPatterns(mockGlobalState.sourcerer, { - ...payload, - id: SourcererScopeName.detections, - selectedPatterns: [], - }); + const result = validateSelectedPatterns( + mockGlobalState.sourcerer, + { + ...payload, + id: SourcererScopeName.detections, + selectedPatterns: [], + }, + true + ); expect(result).toEqual({ [SourcererScopeName.detections]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.detections], @@ -103,22 +111,21 @@ describe('sourcerer store helpers', () => { }, }); }); - it('sets to default when empty array is passed and scope is timeline', () => { - const result = validateSelectedPatterns(mockGlobalState.sourcerer, { - ...payload, - id: SourcererScopeName.timeline, - selectedPatterns: [], - }); + it('sets to empty when empty array is passed and scope is timeline', () => { + const result = validateSelectedPatterns( + mockGlobalState.sourcerer, + { + ...payload, + id: SourcererScopeName.timeline, + selectedPatterns: [], + }, + true + ); expect(result).toEqual({ [SourcererScopeName.timeline]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], selectedDataViewId: dataView.id, - selectedPatterns: [ - signalIndexName, - ...mockGlobalState.sourcerer.defaultDataView.patternList.filter( - (p) => p !== signalIndexName - ), - ].sort(), + selectedPatterns: [], }, }); }); @@ -132,11 +139,15 @@ describe('sourcerer store helpers', () => { defaultDataView: dataViewNoSignals, kibanaDataViews: [dataViewNoSignals], }; - const result = validateSelectedPatterns(stateNoSignals, { - ...payload, - id: SourcererScopeName.timeline, - selectedPatterns: [`${mockGlobalState.sourcerer.signalIndexName}`], - }); + const result = validateSelectedPatterns( + stateNoSignals, + { + ...payload, + id: SourcererScopeName.timeline, + selectedPatterns: [`${mockGlobalState.sourcerer.signalIndexName}`], + }, + true + ); expect(result).toEqual({ [SourcererScopeName.timeline]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], @@ -147,19 +158,23 @@ describe('sourcerer store helpers', () => { }); describe('handles missing dataViewId, 7.16 -> 8.0', () => { it('selectedPatterns.length > 0 & all selectedPatterns exist in defaultDataView, set dataViewId to defaultDataView.id', () => { - const result = validateSelectedPatterns(mockGlobalState.sourcerer, { - ...payload, - id: SourcererScopeName.timeline, - selectedDataViewId: '', - selectedPatterns: [ - mockGlobalState.sourcerer.defaultDataView.patternList[3], - mockGlobalState.sourcerer.defaultDataView.patternList[4], - ], - }); + const result = validateSelectedPatterns( + mockGlobalState.sourcerer, + { + ...payload, + id: SourcererScopeName.timeline, + selectedDataViewId: null, + selectedPatterns: [ + mockGlobalState.sourcerer.defaultDataView.patternList[3], + mockGlobalState.sourcerer.defaultDataView.patternList[4], + ], + }, + true + ); expect(result).toEqual({ [SourcererScopeName.timeline]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], - selectedDataViewId: dataView.id, + selectedDataViewId: null, selectedPatterns: [ mockGlobalState.sourcerer.defaultDataView.patternList[3], mockGlobalState.sourcerer.defaultDataView.patternList[4], @@ -167,16 +182,20 @@ describe('sourcerer store helpers', () => { }, }); }); - it('selectedPatterns.length > 0 & a pattern in selectedPatterns does not exist in defaultDataView, set dataViewId to null', () => { - const result = validateSelectedPatterns(mockGlobalState.sourcerer, { - ...payload, - id: SourcererScopeName.timeline, - selectedDataViewId: '', - selectedPatterns: [ - mockGlobalState.sourcerer.defaultDataView.patternList[3], - 'journalbeat-*', - ], - }); + it('selectedPatterns.length > 0 & some selectedPatterns do not exist in defaultDataView, set dataViewId to null', () => { + const result = validateSelectedPatterns( + mockGlobalState.sourcerer, + { + ...payload, + id: SourcererScopeName.timeline, + selectedDataViewId: null, + selectedPatterns: [ + mockGlobalState.sourcerer.defaultDataView.patternList[3], + 'journalbeat-*', + ], + }, + true + ); expect(result).toEqual({ [SourcererScopeName.timeline]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], @@ -185,6 +204,7 @@ describe('sourcerer store helpers', () => { mockGlobalState.sourcerer.defaultDataView.patternList[3], 'journalbeat-*', ], + missingPatterns: ['journalbeat-*'], }, }); }); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts index 689bf1c4502d8..7f176b0efaca4 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts @@ -34,54 +34,58 @@ export const getScopePatternListSelection = ( } }; +export const ensurePatternFormat = (patternList: string[]): string[] => + [ + ...new Set( + patternList.reduce((acc: string[], pattern: string) => [...pattern.split(','), ...acc], []) + ), + ].sort(); + export const validateSelectedPatterns = ( state: SourcererModel, - payload: SelectedDataViewPayload + payload: SelectedDataViewPayload, + shouldValidateSelectedPatterns: boolean ): Partial => { const { id, ...rest } = payload; - let dataView = state.kibanaDataViews.find((p) => p.id === rest.selectedDataViewId); + const dataView = state.kibanaDataViews.find((p) => p.id === rest.selectedDataViewId); // dedupe because these could come from a silly url or pre 8.0 timeline - const dedupePatterns = [...new Set(rest.selectedPatterns)]; - let selectedPatterns = - dataView != null + const dedupePatterns = ensurePatternFormat(rest.selectedPatterns); + let missingPatterns: string[] = []; + // check for missing patterns against default data view only + if (dataView == null || dataView.id === state.defaultDataView.id) { + const dedupeAllDefaultPatterns = ensurePatternFormat( + (dataView ?? state.defaultDataView).title.split(',') + ); + missingPatterns = dedupePatterns.filter( + (pattern) => !dedupeAllDefaultPatterns.includes(pattern) + ); + } + const selectedPatterns = + // shouldValidateSelectedPatterns is false when upgrading from + // legacy pre-8.0 timeline index patterns to data view. + shouldValidateSelectedPatterns && dataView != null && missingPatterns.length === 0 ? dedupePatterns.filter( (pattern) => - // Typescript is being mean and telling me dataView could be undefined here - // so redoing the dataView != null check (dataView != null && dataView.patternList.includes(pattern)) || // this is a hack, but sometimes signal index is deleted and is getting regenerated. it gets set before it is put in the dataView state.signalIndexName == null || state.signalIndexName === pattern ) - : // 7.16 -> 8.0 this will get hit because dataView == null + : // don't remove non-existing patterns, they were saved in the first place in timeline + // but removed from the security data view + // or its a legacy pre-8.0 timeline dedupePatterns; - if (selectedPatterns.length > 0 && dataView == null) { - // we have index patterns, but not a data view id - // find out if we have these index patterns in the defaultDataView - const areAllPatternsInDefault = selectedPatterns.every( - (pattern) => state.defaultDataView.title.indexOf(pattern) > -1 - ); - if (areAllPatternsInDefault) { - dataView = state.defaultDataView; - selectedPatterns = selectedPatterns.filter( - (pattern) => dataView != null && dataView.patternList.includes(pattern) - ); - } - } - // TO DO: Steph/sourcerer If dataView is still undefined here, create temporary dataView - // and prompt user to go create this dataView - // currently UI will take the undefined dataView and default to defaultDataView anyways - // this is a "strategically merged" bug ;) - // https://github.com/elastic/security-team/issues/1921 - return { [id]: { ...state.sourcererScopes[id], ...rest, selectedDataViewId: dataView?.id ?? null, selectedPatterns, - ...(isEmpty(selectedPatterns) + missingPatterns, + // if in timeline, allow for empty in case pattern was deleted + // need flow for this + ...(isEmpty(selectedPatterns) && id !== SourcererScopeName.timeline ? { selectedPatterns: getScopePatternListSelection( dataView ?? state.defaultDataView, @@ -97,17 +101,17 @@ export const validateSelectedPatterns = ( }; interface CheckIfIndicesExistParams { + patternList: sourcererModel.SourcererDataView['patternList']; scopeId: sourcererModel.SourcererScopeName; signalIndexName: string | null; - sourcererDataView: sourcererModel.SourcererDataView; } export const checkIfIndicesExist = ({ + patternList, scopeId, signalIndexName, - sourcererDataView, }: CheckIfIndicesExistParams) => scopeId === SourcererScopeName.detections - ? sourcererDataView.patternList.includes(`${signalIndexName}`) + ? patternList.includes(`${signalIndexName}`) : scopeId === SourcererScopeName.default - ? sourcererDataView.patternList.filter((i) => i !== signalIndexName).length > 0 - : sourcererDataView.patternList.length > 0; + ? patternList.filter((i) => i !== signalIndexName).length > 0 + : patternList.length > 0; diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts index a22a04d025d19..61377662fa812 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts @@ -29,10 +29,15 @@ export interface SourcererScope { id: SourcererScopeName; /** is an update being made to the sourcerer data view */ loading: boolean; - /** selected data view id */ - selectedDataViewId: string; + /** selected data view id, null if it is legacy index patterns*/ + selectedDataViewId: string | null; /** selected patterns within the data view */ selectedPatterns: string[]; + /** if has length, + * id === SourcererScopeName.timeline + * selectedDataViewId === null OR defaultDataView.id + * saved timeline has pattern that is not in the default */ + missingPatterns: string[]; } export type SourcererScopeById = Record; @@ -54,6 +59,7 @@ export interface KibanaDataView { * DataView from Kibana + timelines/index_fields enhanced field data */ export interface SourcererDataView extends KibanaDataView { + id: string; /** we need this for @timestamp data */ browserFields: BrowserFields; /** we need this for @timestamp data */ @@ -75,7 +81,7 @@ export interface SourcererDataView extends KibanaDataView { */ export interface SelectedDataView { browserFields: SourcererDataView['browserFields']; - dataViewId: SourcererDataView['id']; + dataViewId: string | null; // null if legacy pre-8.0 timeline docValueFields: SourcererDataView['docValueFields']; /** * DataViewBase with enhanced index fields used in timelines @@ -88,8 +94,10 @@ export interface SelectedDataView { /** all active & inactive patterns from SourcererDataView['title'] */ patternList: string[]; runtimeMappings: SourcererDataView['runtimeMappings']; - /** all selected patterns from SourcererScope['selectedPatterns'] */ - selectedPatterns: string[]; + /** all selected patterns from SourcererScope['selectedPatterns'] */ + selectedPatterns: SourcererScope['selectedPatterns']; + // active patterns when dataViewId == null + activePatterns?: string[]; } /** @@ -97,7 +105,7 @@ export interface SelectedDataView { */ export interface SourcererModel { /** default security-solution data view */ - defaultDataView: SourcererDataView & { error?: unknown }; + defaultDataView: SourcererDataView & { id: string; error?: unknown }; /** all Kibana data views, including security-solution */ kibanaDataViews: SourcererDataView[]; /** security solution signals index name */ @@ -115,8 +123,9 @@ export type SourcererUrlState = Partial<{ export const initSourcererScope: Omit = { loading: false, - selectedDataViewId: '', + selectedDataViewId: null, selectedPatterns: [], + missingPatterns: [], }; export const initDataView = { browserFields: EMPTY_BROWSER_FIELDS, diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts index e1747a6786cdb..648ba354f29d9 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts @@ -72,13 +72,17 @@ export const sourcererReducer = reducerWithInitialState(initialSourcererState) }), }, })) - .case(setSelectedDataView, (state, payload) => ({ - ...state, - sourcererScopes: { - ...state.sourcererScopes, - ...validateSelectedPatterns(state, payload), - }, - })) + .case(setSelectedDataView, (state, payload) => { + const { shouldValidateSelectedPatterns = true, ...patternsInfo } = payload; + + return { + ...state, + sourcererScopes: { + ...state.sourcererScopes, + ...validateSelectedPatterns(state, patternsInfo, shouldValidateSelectedPatterns), + }, + }; + }) .case(setDataView, (state, dataView) => ({ ...state, ...(dataView.id === state.defaultDataView.id diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts index b72d7bfde2dcc..8c0b1ecf6f627 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts @@ -26,8 +26,11 @@ export const sourcererDefaultDataViewSelector = ({ sourcerer, }: State): SourcererModel['defaultDataView'] => sourcerer.defaultDataView; -export const dataViewSelector = ({ sourcerer }: State, id: string): SourcererDataView => - sourcerer.kibanaDataViews.find((dataView) => dataView.id === id) ?? sourcerer.defaultDataView; +export const dataViewSelector = ( + { sourcerer }: State, + id: string | null +): SourcererDataView | undefined => + sourcerer.kibanaDataViews.find((dataView) => dataView.id === id); export const sourcererScopeIdSelector = ( { sourcerer }: State, @@ -54,29 +57,48 @@ export const sourcererDataViewSelector = () => createSelector(dataViewSelector, (dataView) => dataView); export interface SourcererScopeSelector extends Omit { - sourcererDataView: SourcererDataView; + selectedDataView: SourcererDataView | undefined; sourcererScope: SourcererScope; } -export const getSourcererScopeSelector = () => { +export const getSourcererDataViewsSelector = () => { const getKibanaDataViewsSelector = kibanaDataViewsSelector(); const getDefaultDataViewSelector = defaultDataViewSelector(); const getSignalIndexNameSelector = signalIndexNameSelector(); - const getSourcererDataViewSelector = sourcererDataViewSelector(); - const getScopeSelector = scopeIdSelector(); - - return (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => { + return (state: State): Omit => { const kibanaDataViews = getKibanaDataViewsSelector(state); const defaultDataView = getDefaultDataViewSelector(state); const signalIndexName = getSignalIndexNameSelector(state); - const scope = getScopeSelector(state, scopeId); - const sourcererDataView = getSourcererDataViewSelector(state, scope.selectedDataViewId); return { defaultDataView, kibanaDataViews, signalIndexName, - sourcererDataView, + }; + }; +}; + +/** + * Attn Future Developer + * Access sourcererScope.selectedPatterns from + * hook useSourcererDataView in `common/containers/sourcerer/index` + * in order to get exclude patterns for searches + * Access sourcererScope.selectedPatterns + * from this function for display purposes only + * */ +export const getSourcererScopeSelector = () => { + const getDataViewsSelector = getSourcererDataViewsSelector(); + const getSourcererDataViewSelector = sourcererDataViewSelector(); + const getScopeSelector = scopeIdSelector(); + + return (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => { + const dataViews = getDataViewsSelector(state); + const scope = getScopeSelector(state, scopeId); + const selectedDataView = getSourcererDataViewSelector(state, scope.selectedDataViewId); + + return { + ...dataViews, + selectedDataView, sourcererScope: scope, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 73af793275122..a7d443acc3daf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -140,7 +140,7 @@ describe('alert actions', () => { ], defaultColumns: defaultHeaders, dataProviders: [], - dataViewId: '', + dataViewId: null, dateRange: { end: '2018-11-05T19:03:25.937Z', start: '2018-11-05T18:58:25.937Z', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx index 13e93604863b4..aab6cabdb3a93 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx @@ -7,7 +7,7 @@ import { ExistsFilter, Filter } from '@kbn/es-query'; import { - buildAlertsRuleIdFilter, + buildAlertsFilter, buildAlertStatusesFilter, buildAlertStatusFilter, buildThreatMatchFilter, @@ -18,21 +18,21 @@ jest.mock('./actions'); describe('alerts default_config', () => { describe('buildAlertsRuleIdFilter', () => { test('given a rule id this will return an array with a single filter', () => { - const filters: Filter[] = buildAlertsRuleIdFilter('rule-id-1'); + const filters: Filter[] = buildAlertsFilter('rule-id-1'); const expectedFilter: Filter = { meta: { alias: null, negate: false, disabled: false, type: 'phrase', - key: 'kibana.alert.rule.uuid', + key: 'kibana.alert.rule.rule_id', params: { query: 'rule-id-1', }, }, query: { match_phrase: { - 'kibana.alert.rule.uuid': 'rule-id-1', + 'kibana.alert.rule.rule_id': 'rule-id-1', }, }, }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index a5947e45ed0f0..663d133f04b1c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -6,21 +6,13 @@ */ import { - ALERT_DURATION, - ALERT_RULE_PRODUCER, - ALERT_START, + ALERT_BUILDING_BLOCK_TYPE, ALERT_WORKFLOW_STATUS, - ALERT_UUID, - ALERT_RULE_UUID, - ALERT_RULE_NAME, - ALERT_RULE_CATEGORY, - ALERT_RULE_SEVERITY, - ALERT_RULE_RISK_SCORE, + ALERT_RULE_RULE_ID, } from '@kbn/rule-data-utils/technical_field_names'; import type { Filter } from '@kbn/es-query'; -import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; -import { ColumnHeaderOptions, RowRendererId } from '../../../../common/types/timeline'; +import { RowRendererId } from '../../../../common/types/timeline'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; @@ -34,12 +26,12 @@ export const buildAlertStatusFilter = (status: Status): Filter[] => { should: [ { term: { - 'kibana.alert.workflow_status': status, + [ALERT_WORKFLOW_STATUS]: status, }, }, { term: { - 'kibana.alert.workflow_status': 'in-progress', + [ALERT_WORKFLOW_STATUS]: 'in-progress', }, }, ], @@ -47,7 +39,7 @@ export const buildAlertStatusFilter = (status: Status): Filter[] => { } : { term: { - 'kibana.alert.workflow_status': status, + [ALERT_WORKFLOW_STATUS]: status, }, }; @@ -58,7 +50,7 @@ export const buildAlertStatusFilter = (status: Status): Filter[] => { negate: false, disabled: false, type: 'phrase', - key: 'kibana.alert.workflow_status', + key: ALERT_WORKFLOW_STATUS, params: { query: status, }, @@ -76,7 +68,7 @@ export const buildAlertStatusesFilter = (statuses: Status[]): Filter[] => { bool: { should: statuses.map((status) => ({ term: { - 'kibana.alert.workflow_status': status, + [ALERT_WORKFLOW_STATUS]: status, }, })), }, @@ -94,8 +86,15 @@ export const buildAlertStatusesFilter = (statuses: Status[]): Filter[] => { ]; }; -export const buildAlertsRuleIdFilter = (ruleId: string | null): Filter[] => - ruleId +/** + * Builds Kuery filter for fetching alerts for a specific rule. Takes the rule's + * static id, i.e. `rule.ruleId` (not rule.id), so that alerts for _all + * historical instances_ of the rule are returned. + * + * @param ruleStaticId Rule's static id: `rule.ruleId` + */ +export const buildAlertsFilter = (ruleStaticId: string | null): Filter[] => + ruleStaticId ? [ { meta: { @@ -103,14 +102,14 @@ export const buildAlertsRuleIdFilter = (ruleId: string | null): Filter[] => negate: false, disabled: false, type: 'phrase', - key: 'kibana.alert.rule.uuid', + key: ALERT_RULE_RULE_ID, params: { - query: ruleId, + query: ruleStaticId, }, }, query: { match_phrase: { - 'kibana.alert.rule.uuid': ruleId, + [ALERT_RULE_RULE_ID]: ruleStaticId, }, }, }, @@ -127,10 +126,10 @@ export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean): negate: true, disabled: false, type: 'exists', - key: 'kibana.alert.building_block_type', + key: ALERT_BUILDING_BLOCK_TYPE, value: 'exists', }, - query: { exists: { field: 'kibana.alert.building_block_type' } }, + query: { exists: { field: ALERT_BUILDING_BLOCK_TYPE } }, }, ]; @@ -183,121 +182,3 @@ export const requiredFieldsForActions = [ 'host.os.family', 'event.code', ]; - -// TODO: Once we are past experimental phase this code should be removed -export const buildAlertStatusFilterRuleRegistry = (status: Status): Filter[] => { - const combinedQuery = - status === 'acknowledged' - ? { - bool: { - should: [ - { - term: { - [ALERT_WORKFLOW_STATUS]: status, - }, - }, - { - term: { - [ALERT_WORKFLOW_STATUS]: 'in-progress', - }, - }, - ], - }, - } - : { - term: { - [ALERT_WORKFLOW_STATUS]: status, - }, - }; - - return [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: ALERT_WORKFLOW_STATUS, - params: { - query: status, - }, - }, - query: combinedQuery, - }, - ]; -}; - -// TODO: Once we are past experimental phase this code should be removed -export const buildAlertStatusesFilterRuleRegistry = (statuses: Status[]): Filter[] => { - const combinedQuery = { - bool: { - should: statuses.map((status) => ({ - term: { - [ALERT_WORKFLOW_STATUS]: status, - }, - })), - }, - }; - - return [ - { - meta: { - alias: null, - negate: false, - disabled: false, - }, - query: combinedQuery, - }, - ]; -}; - -export const buildShowBuildingBlockFilterRuleRegistry = ( - showBuildingBlockAlerts: boolean -): Filter[] => - showBuildingBlockAlerts - ? [] - : [ - { - meta: { - alias: null, - negate: true, - disabled: false, - type: 'exists', - key: 'kibana.alert.building_block_type', - value: 'exists', - }, - query: { exists: { field: 'kibana.alert.building_block_type' } }, - }, - ]; - -export const requiredFieldMappingsForActionsRuleRegistry = { - '@timestamp': '@timestamp', - 'event.kind': 'event.kind', - 'rule.severity': ALERT_RULE_SEVERITY, - 'rule.risk_score': ALERT_RULE_RISK_SCORE, - 'alert.uuid': ALERT_UUID, - 'alert.start': ALERT_START, - 'event.action': 'event.action', - 'alert.workflow_status': ALERT_WORKFLOW_STATUS, - 'alert.duration.us': ALERT_DURATION, - 'rule.uuid': ALERT_RULE_UUID, - 'rule.name': ALERT_RULE_NAME, - 'rule.category': ALERT_RULE_CATEGORY, - producer: ALERT_RULE_PRODUCER, - tags: 'tags', -}; - -export const alertsHeadersRuleRegistry: ColumnHeaderOptions[] = Object.entries( - requiredFieldMappingsForActionsRuleRegistry -).map(([alias, field]) => ({ - columnHeaderType: defaultColumnHeaderType, - displayAsText: alias, - id: field, -})); - -export const alertsDefaultModelRuleRegistry: SubsetTimelineModel = { - ...timelineDefaults, - columns: alertsHeadersRuleRegistry, - showCheckboxes: true, - excludedRowRendererIds: Object.values(RowRendererId), -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index bbab423738ca0..256a063c44158 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -40,9 +40,7 @@ import { updateAlertStatusAction } from './actions'; import { AditionalFiltersAction, AlertsUtilityBar } from './alerts_utility_bar'; import { alertsDefaultModel, - alertsDefaultModelRuleRegistry, buildAlertStatusFilter, - buildAlertStatusFilterRuleRegistry, requiredFieldsForActions, } from './default_config'; import { buildTimeRangeFilter } from './helpers'; @@ -106,8 +104,6 @@ export const AlertsTableComponent: React.FC = ({ const kibana = useKibana(); const [, dispatchToaster] = useStateToaster(); const { addWarning } = useAppToasts(); - // TODO: Once we are past experimental phase this code should be removed - const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); const ACTION_BUTTON_COUNT = 4; const getGlobalQuery = useCallback( @@ -247,14 +243,9 @@ export const AlertsTableComponent: React.FC = ({ refetchQuery: inputsModel.Refetch, { status, selectedStatus }: UpdateAlertsStatusProps ) => { - // TODO: Once we are past experimental phase this code should be removed - const currentStatusFilter = ruleRegistryEnabled - ? buildAlertStatusFilterRuleRegistry(status) - : buildAlertStatusFilter(status); - await updateAlertStatusAction({ query: showClearSelectionAction - ? getGlobalQuery(currentStatusFilter)?.filterQuery + ? getGlobalQuery(buildAlertStatusFilter(status))?.filterQuery : undefined, alertIds: Object.keys(selectedEventIds), selectedStatus, @@ -273,7 +264,6 @@ export const AlertsTableComponent: React.FC = ({ showClearSelectionAction, onAlertStatusUpdateSuccess, onAlertStatusUpdateFailure, - ruleRegistryEnabled, ] ); @@ -327,24 +317,16 @@ export const AlertsTableComponent: React.FC = ({ ); const defaultFiltersMemo = useMemo(() => { - // TODO: Once we are past experimental phase this code should be removed - const alertStatusFilter = ruleRegistryEnabled - ? buildAlertStatusFilterRuleRegistry(filterGroup) - : buildAlertStatusFilter(filterGroup); + const alertStatusFilter = buildAlertStatusFilter(filterGroup); if (isEmpty(defaultFilters)) { return alertStatusFilter; } else if (defaultFilters != null && !isEmpty(defaultFilters)) { return [...defaultFilters, ...alertStatusFilter]; } - }, [defaultFilters, filterGroup, ruleRegistryEnabled]); + }, [defaultFilters, filterGroup]); const { filterManager } = useKibana().services.data.query; - // TODO: Once we are past experimental phase this code should be removed - const defaultTimelineModel = ruleRegistryEnabled - ? alertsDefaultModelRuleRegistry - : alertsDefaultModel; - const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); useEffect(() => { @@ -359,7 +341,7 @@ export const AlertsTableComponent: React.FC = ({ : c ), documentType: i18n.ALERTS_DOCUMENT_TYPE, - excludedRowRendererIds: defaultTimelineModel.excludedRowRendererIds as RowRendererId[], + excludedRowRendererIds: alertsDefaultModel.excludedRowRendererIds as RowRendererId[], filterManager, footerText: i18n.TOTAL_COUNT_OF_ALERTS, id: timelineId, @@ -370,7 +352,7 @@ export const AlertsTableComponent: React.FC = ({ showCheckboxes: true, }) ); - }, [dispatch, defaultTimelineModel, filterManager, tGridEnabled, timelineId]); + }, [dispatch, filterManager, tGridEnabled, timelineId]); const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); @@ -383,7 +365,7 @@ export const AlertsTableComponent: React.FC = ({ additionalFilters={additionalFiltersComponent} currentFilter={filterGroup} defaultCellActions={defaultCellActions} - defaultModel={defaultTimelineModel} + defaultModel={alertsDefaultModel} end={to} entityType="events" hasAlertsCrud={hasIndexWrite && hasIndexMaintenance} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index 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/query_preview/custom_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx deleted file mode 100644 index 2e6991f87ec5a..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx +++ /dev/null @@ -1,128 +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 { mount } from 'enzyme'; - -import * as i18n from '../rule_preview/translations'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { TestProviders } from '../../../../common/mock'; -import { PreviewCustomQueryHistogram } from './custom_histogram'; - -jest.mock('../../../../common/containers/use_global_time'); - -describe('PreviewCustomQueryHistogram', () => { - const mockSetQuery = jest.fn(); - - beforeEach(() => { - (useGlobalTime as jest.Mock).mockReturnValue({ - from: '2020-07-07T08:20:18.966Z', - isInitializing: false, - to: '2020-07-08T08:20:18.966Z', - setQuery: mockSetQuery, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('it renders loader when isLoading is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); - expect( - wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') - ).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING); - }); - - test('it configures data and subtitle', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); - expect( - wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') - ).toEqual(i18n.QUERY_PREVIEW_TITLE(9154)); - expect(wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).props().data).toEqual( - [ - { - key: 'hits', - value: [ - { - g: 'All others', - x: 1602247050000, - y: 2314, - }, - { - g: 'All others', - x: 1602247162500, - y: 3471, - }, - { - g: 'All others', - x: 1602247275000, - y: 3369, - }, - ], - }, - ] - ); - }); - - test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { - const mockRefetch = jest.fn(); - - mount( - - - - ); - - expect(mockSetQuery).toHaveBeenCalledWith({ - id: 'queryPreviewCustomHistogramQuery', - inspect: { dsl: ['some dsl'], response: ['query response'] }, - loading: false, - refetch: mockRefetch, - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx deleted file mode 100644 index 5392b08889128..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useMemo } from 'react'; - -import * as i18n from '../rule_preview/translations'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { getHistogramConfig } from '../rule_preview/helpers'; -import { - ChartSeriesConfigs, - ChartSeriesData, - ChartData, -} from '../../../../common/components/charts/common'; -import { InspectResponse } from '../../../../../public/types'; -import { inputsModel } from '../../../../common/store'; -import { PreviewHistogram } from './histogram'; - -export const ID = 'queryPreviewCustomHistogramQuery'; - -interface PreviewCustomQueryHistogramProps { - to: string; - from: string; - isLoading: boolean; - data: ChartData[]; - totalCount: number; - inspect: InspectResponse; - refetch: inputsModel.Refetch; -} - -export const PreviewCustomQueryHistogram = ({ - to, - from, - data, - totalCount, - inspect, - refetch, - isLoading, -}: PreviewCustomQueryHistogramProps) => { - const { setQuery, isInitializing } = useGlobalTime(); - - useEffect((): void => { - if (!isLoading && !isInitializing) { - setQuery({ id: ID, inspect, loading: isLoading, refetch }); - } - }, [setQuery, inspect, isLoading, isInitializing, refetch]); - - const barConfig = useMemo( - (): ChartSeriesConfigs => getHistogramConfig(to, from, true), - [from, to] - ); - - const subtitle = useMemo( - (): string => - isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount), - [isLoading, totalCount] - ); - - const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]); - - return ( - - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx deleted file mode 100644 index df32223fc7ec3..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx +++ /dev/null @@ -1,152 +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 { mount } from 'enzyme'; - -import * as i18n from '../rule_preview/translations'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { TestProviders } from '../../../../common/mock'; -import { PreviewEqlQueryHistogram } from './eql_histogram'; - -jest.mock('../../../../common/containers/use_global_time'); - -describe('PreviewEqlQueryHistogram', () => { - const mockSetQuery = jest.fn(); - - beforeEach(() => { - (useGlobalTime as jest.Mock).mockReturnValue({ - from: '2020-07-07T08:20:18.966Z', - isInitializing: false, - to: '2020-07-08T08:20:18.966Z', - setQuery: mockSetQuery, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('it renders loader when isLoading is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); - expect( - wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') - ).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING); - }); - - test('it configures data and subtitle', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); - expect( - wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') - ).toEqual(i18n.QUERY_PREVIEW_TITLE(9154)); - expect(wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).props().data).toEqual([ - { - key: 'hits', - value: [ - { - g: 'All others', - x: 1602247050000, - y: 2314, - }, - { - g: 'All others', - x: 1602247162500, - y: 3471, - }, - { - g: 'All others', - x: 1602247275000, - y: 3369, - }, - ], - }, - ]); - }); - - test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { - const mockRefetch = jest.fn(); - - mount( - - - - ); - - expect(mockSetQuery).toHaveBeenCalledWith({ - id: 'queryEqlPreviewHistogramQuery', - inspect: { dsl: ['some dsl'], response: ['query response'] }, - loading: false, - refetch: mockRefetch, - }); - }); - - test('it displays histogram', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); - expect( - wrapper.find('[data-test-subj="sharedPreviewQueryNoHistogramAvailable"]').exists() - ).toBeFalsy(); - expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx deleted file mode 100644 index eae2a593d5f25..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useMemo } from 'react'; - -import * as i18n from '../rule_preview/translations'; -import { getHistogramConfig } from '../rule_preview/helpers'; -import { - ChartSeriesData, - ChartSeriesConfigs, - ChartData, -} from '../../../../common/components/charts/common'; -import { InspectQuery } from '../../../../common/store/inputs/model'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { inputsModel } from '../../../../common/store'; -import { PreviewHistogram } from './histogram'; - -export const ID = 'queryEqlPreviewHistogramQuery'; - -interface PreviewEqlQueryHistogramProps { - to: string; - from: string; - totalCount: number; - isLoading: boolean; - data: ChartData[]; - inspect: InspectQuery; - refetch: inputsModel.Refetch; -} - -export const PreviewEqlQueryHistogram = ({ - from, - to, - totalCount, - data, - inspect, - refetch, - isLoading, -}: PreviewEqlQueryHistogramProps) => { - const { setQuery, isInitializing } = useGlobalTime(); - - useEffect((): void => { - if (!isInitializing) { - setQuery({ id: ID, inspect, loading: false, refetch }); - } - }, [setQuery, inspect, isInitializing, refetch]); - - const barConfig = useMemo((): ChartSeriesConfigs => getHistogramConfig(to, from), [from, to]); - - const subtitle = useMemo( - (): string => - isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount), - [isLoading, totalCount] - ); - - const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]); - - return ( - - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx deleted file mode 100644 index 500a7f3d0e3db..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx +++ /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 React from 'react'; -import { mount } from 'enzyme'; - -import { TestProviders } from '../../../../common/mock'; -import { PreviewHistogram } from './histogram'; -import { getHistogramConfig } from '../rule_preview/helpers'; - -describe('PreviewHistogram', () => { - test('it renders loading icon if "isLoading" is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders chart if "isLoading" is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx deleted file mode 100644 index 3391ed1c5560a..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx +++ /dev/null @@ -1,75 +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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui'; -import styled from 'styled-components'; - -import { BarChart } from '../../../../common/components/charts/barchart'; -import { Panel } from '../../../../common/components/panel'; -import { HeaderSection } from '../../../../common/components/header_section'; -import { ChartSeriesData, ChartSeriesConfigs } from '../../../../common/components/charts/common'; - -const LoadingChart = styled(EuiLoadingChart)` - display: block; - margin: 0 auto; -`; - -interface PreviewHistogramProps { - id: string; - data: ChartSeriesData[]; - dataTestSubj?: string; - barConfig: ChartSeriesConfigs; - title: string; - subtitle: string; - disclaimer: string; - isLoading: boolean; -} - -export const PreviewHistogram = ({ - id, - data, - dataTestSubj, - barConfig, - title, - subtitle, - disclaimer, - isLoading, -}: PreviewHistogramProps) => { - return ( - <> - - - - - - - {isLoading ? ( - - ) : ( - - )} - - - <> - - -

{disclaimer}

-
- -
-
-
- - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx deleted file mode 100644 index f14bd5f7354d9..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx +++ /dev/null @@ -1,502 +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 { of } from 'rxjs'; -import { ThemeProvider } from 'styled-components'; -import { mount } from 'enzyme'; - -import { TestProviders } from '../../../../common/mock'; -import { useKibana } from '../../../../common/lib/kibana'; -import { PreviewQuery } from './'; -import { getMockEqlResponse } from '../../../../common/hooks/eql/eql_search_response.mock'; -import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; -import { useEqlPreview } from '../../../../common/hooks/eql/'; -import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; -import type { FilterMeta } from '@kbn/es-query'; - -const mockTheme = getMockTheme({ - eui: { - euiSuperDatePickerWidth: '180px', - }, -}); - -jest.mock('../../../../common/lib/kibana'); -jest.mock('../../../../common/containers/matrix_histogram'); -jest.mock('../../../../common/hooks/eql/'); - -describe('PreviewQuery', () => { - beforeEach(() => { - useKibana().services.notifications.toasts.addError = jest.fn(); - - useKibana().services.notifications.toasts.addWarning = jest.fn(); - - (useMatrixHistogram as jest.Mock).mockReturnValue([ - false, - { - inspect: { dsl: [], response: [] }, - totalCount: 1, - refetch: jest.fn(), - data: [], - buckets: [], - }, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - ]); - - (useEqlPreview as jest.Mock).mockReturnValue([ - false, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - { - inspect: { dsl: [], response: [] }, - totalCount: 1, - refetch: jest.fn(), - data: [], - buckets: [], - }, - ]); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('it renders timeframe select and preview button on render', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewSelect"]').exists()).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders preview button disabled if "isDisabled" is true', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeTruthy(); - }); - - test('it renders preview button disabled if "query" is undefined', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeTruthy(); - }); - - test('it renders preview button enabled if query exists', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeFalsy(); - }); - - test('it renders preview button enabled if no query exists but filters do exist', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeFalsy(); - }); - - test('it renders query histogram when rule type is query and preview button clicked', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders noise warning when rule type is query, timeframe is last hour and hit average is greater than 1/hour', async () => { - const wrapper = mount( - - - - ); - - (useMatrixHistogram as jest.Mock).mockReturnValue([ - false, - { - inspect: { dsl: [], response: [] }, - totalCount: 2, - refetch: jest.fn(), - data: [], - buckets: [], - }, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - ]); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy(); - }); - - test('it renders query histogram when rule type is saved_query and preview button clicked', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders eql histogram when preview button clicked and rule type is eql', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeTruthy(); - }); - - test('it renders noise warning when rule type is eql, timeframe is last hour and hit average is greater than 1/hour', async () => { - const wrapper = mount( - - - - ); - - (useEqlPreview as jest.Mock).mockReturnValue([ - false, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - { - inspect: { dsl: [], response: [] }, - totalCount: 2, - refetch: jest.fn(), - data: [], - buckets: [], - }, - ]); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy(); - }); - - test('it renders threshold histogram when preview button clicked, rule type is threshold, and threshold field is defined', () => { - const wrapper = mount( - - - - ); - - (useMatrixHistogram as jest.Mock).mockReturnValue([ - false, - { - inspect: { dsl: [], response: [] }, - totalCount: 500, - refetch: jest.fn(), - data: [], - buckets: [{ key: 'siem-kibana', doc_count: 500 }], - }, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - ]); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders noise warning when rule type is threshold, and threshold field is defined, timeframe is last hour and hit average is greater than 1/hour', async () => { - const wrapper = mount( - - - - ); - - (useMatrixHistogram as jest.Mock).mockReturnValue([ - false, - { - inspect: { dsl: [], response: [] }, - totalCount: 500, - refetch: jest.fn(), - data: [], - buckets: [ - { key: 'siem-kibana', doc_count: 200 }, - { key: 'siem-windows', doc_count: 300 }, - ], - }, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - ]); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy(); - }); - - test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty array', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty string', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it hides histogram when timeframe changes', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - - wrapper - .find('[data-test-subj="queryPreviewTimeframeSelect"] select') - .at(0) - .simulate('change', { target: { value: 'd' } }); - - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx deleted file mode 100644 index e7cc34ef49bef..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx +++ /dev/null @@ -1,362 +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, { Fragment, useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; -import { Unit } from '@elastic/datemath'; -import styled from 'styled-components'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSelect, - EuiFormRow, - EuiButton, - EuiCallOut, - EuiText, - EuiSpacer, -} from '@elastic/eui'; -import { debounce } from 'lodash/fp'; - -import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; -import * as i18n from '../rule_preview/translations'; -import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; -import { MatrixHistogramType } from '../../../../../common/search_strategy'; -import { FieldValueQueryBar } from '../query_bar'; -import { PreviewEqlQueryHistogram } from './eql_histogram'; -import { useEqlPreview } from '../../../../common/hooks/eql/'; -import { PreviewThresholdQueryHistogram } from './threshold_histogram'; -import { formatDate } from '../../../../common/components/super_date_picker'; -import { State, queryPreviewReducer } from './reducer'; -import { isNoisy } from '../rule_preview/helpers'; -import { PreviewCustomQueryHistogram } from './custom_histogram'; -import { FieldValueThreshold } from '../threshold_input'; - -const Select = styled(EuiSelect)` - width: ${({ theme }) => theme.eui.euiSuperDatePickerWidth}; -`; - -const PreviewButton = styled(EuiButton)` - margin-left: 0; -`; - -export const initialState: State = { - timeframeOptions: [], - showHistogram: false, - timeframe: 'h', - warnings: [], - queryFilter: undefined, - toTime: '', - fromTime: '', - queryString: '', - language: 'kuery', - filters: [], - thresholdFieldExists: false, - showNonEqlHistogram: false, -}; - -export type Threshold = FieldValueThreshold | undefined; - -interface PreviewQueryProps { - dataTestSubj: string; - idAria: string; - query: FieldValueQueryBar | undefined; - index: string[]; - ruleType: Type; - threshold: Threshold; - isDisabled: boolean; -} - -export const PreviewQuery = ({ - ruleType, - dataTestSubj, - idAria, - query, - index, - threshold, - isDisabled, -}: PreviewQueryProps) => { - const [ - eqlQueryLoading, - startEql, - { - totalCount: eqlQueryTotal, - data: eqlQueryData, - refetch: eqlQueryRefetch, - inspect: eqlQueryInspect, - }, - ] = useEqlPreview(); - - const [ - { - thresholdFieldExists, - showNonEqlHistogram, - timeframeOptions, - showHistogram, - timeframe, - warnings, - queryFilter, - toTime, - fromTime, - queryString, - }, - dispatch, - ] = useReducer(queryPreviewReducer(), { - ...initialState, - toTime: formatDate('now-1h'), - fromTime: formatDate('now'), - }); - const [ - isMatrixHistogramLoading, - { inspect, totalCount: matrixHistTotal, refetch, data: matrixHistoData, buckets }, - startNonEql, - ] = useMatrixHistogram({ - errorMessage: i18n.QUERY_PREVIEW_ERROR, - endDate: fromTime, - startDate: toTime, - filterQuery: queryFilter, - indexNames: index, - includeMissingData: false, - histogramType: MatrixHistogramType.events, - stackByField: 'event.category', - threshold: ruleType === 'threshold' ? threshold : undefined, - skip: true, - }); - - const setQueryInfo = useCallback( - (queryBar: FieldValueQueryBar | undefined, indices: string[], type: Type): void => { - dispatch({ - type: 'setQueryInfo', - queryBar, - index: indices, - ruleType: type, - }); - }, - [dispatch] - ); - - const debouncedSetQueryInfo = useRef(debounce(500, setQueryInfo)); - - const setTimeframeSelect = useCallback( - (selection: Unit): void => { - dispatch({ - type: 'setTimeframeSelect', - timeframe: selection, - }); - }, - [dispatch] - ); - - const setRuleTypeChange = useCallback( - (type: Type): void => { - dispatch({ - type: 'setResetRuleTypeChange', - ruleType: type, - }); - }, - [dispatch] - ); - - const setWarnings = useCallback( - (yikes: string[]): void => { - dispatch({ - type: 'setWarnings', - warnings: yikes, - }); - }, - [dispatch] - ); - - const setNoiseWarning = useCallback((): void => { - dispatch({ - type: 'setNoiseWarning', - }); - }, [dispatch]); - - const setShowHistogram = useCallback( - (show: boolean): void => { - dispatch({ - type: 'setShowHistogram', - show, - }); - }, - [dispatch] - ); - - const setThresholdValues = useCallback( - (thresh: Threshold, type: Type): void => { - dispatch({ - type: 'setThresholdQueryVals', - threshold: thresh, - ruleType: type, - }); - }, - [dispatch] - ); - - useEffect(() => { - debouncedSetQueryInfo.current(query, index, ruleType); - }, [index, query, ruleType]); - - useEffect((): void => { - setThresholdValues(threshold, ruleType); - }, [setThresholdValues, threshold, ruleType]); - - useEffect((): void => { - setRuleTypeChange(ruleType); - }, [ruleType, setRuleTypeChange]); - - useEffect((): void => { - switch (ruleType) { - case 'eql': - if (isNoisy(eqlQueryTotal, timeframe)) { - setNoiseWarning(); - } - break; - case 'threshold': - const totalHits = thresholdFieldExists ? buckets.length : matrixHistTotal; - if (isNoisy(totalHits, timeframe)) { - setNoiseWarning(); - } - break; - default: - if (isNoisy(matrixHistTotal, timeframe)) { - setNoiseWarning(); - } - } - }, [ - timeframe, - matrixHistTotal, - eqlQueryTotal, - ruleType, - setNoiseWarning, - thresholdFieldExists, - buckets.length, - ]); - - const handlePreviewEqlQuery = useCallback( - (to: string, from: string): void => { - startEql({ - index, - query: queryString, - from, - to, - interval: timeframe, - }); - }, - [startEql, index, queryString, timeframe] - ); - - const handleSelectPreviewTimeframe = useCallback( - ({ target: { value } }: React.ChangeEvent): void => { - setTimeframeSelect(value as Unit); - }, - [setTimeframeSelect] - ); - - const handlePreviewClicked = useCallback((): void => { - const to = formatDate('now'); - const from = formatDate(`now-1${timeframe}`); - - setWarnings([]); - setShowHistogram(true); - - if (ruleType === 'eql') { - handlePreviewEqlQuery(to, from); - } else { - startNonEql(to, from); - } - }, [setWarnings, setShowHistogram, ruleType, handlePreviewEqlQuery, startNonEql, timeframe]); - - const previewButtonDisabled = useMemo(() => { - return ( - isMatrixHistogramLoading || - eqlQueryLoading || - isDisabled || - query == null || - (query != null && query.query.query === '' && query.filters.length === 0) - ); - }, [eqlQueryLoading, isDisabled, isMatrixHistogramLoading, query]); - - return ( - <> - - - -