diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index 7eafc66465bc7..8121405e5ae24 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -73,11 +73,7 @@ def agentProcess(Map params = [:]) { ]) { task { if (config.needBuild) { - if (!config.isXpack) { - kibanaPipeline.buildOss() - } else { - kibanaPipeline.buildXpack() - } + kibanaPipeline.buildKibana() } for(def i = 0; i < config.agentExecutions; i++) { diff --git a/.ci/Jenkinsfile_security_cypress b/.ci/Jenkinsfile_security_cypress index 811af44d1ca56..d48b9965919dc 100644 --- a/.ci/Jenkinsfile_security_cypress +++ b/.ci/Jenkinsfile_security_cypress @@ -16,7 +16,7 @@ kibanaPipeline(timeoutMinutes: 180) { def job = 'xpack-securityCypress' workers.ci(name: job, size: 'l', ramDisk: true) { - kibanaPipeline.bash('test/scripts/jenkins_xpack_build_kibana.sh', 'Build Default Distributable') + kibanaPipeline.bash('test/scripts/jenkins_build_kibana.sh', 'Build Distributable') kibanaPipeline.functionalTestProcess(job, 'test/scripts/jenkins_security_solution_cypress_chrome.sh')() // Temporarily disabled to figure out test flake // kibanaPipeline.functionalTestProcess(job, 'test/scripts/jenkins_security_solution_cypress_firefox.sh')() diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index d56ec61314ac7..dc3a3cde7d658 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -37,12 +37,8 @@ kibanaPipeline(timeoutMinutes: 210) { ]) task { - kibanaPipeline.buildOss(6) + kibanaPipeline.buildKibana(16) tasks.ossCiGroups() - } - - task { - kibanaPipeline.buildXpack(10, true) tasks.xpackCiGroups() tasks.xpackCiGroupDocker() } diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index d334d7979ed59..60af851ff2dc6 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -10,15 +10,15 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # Fetch Node.js rules http_archive( name = "build_bazel_rules_nodejs", - sha256 = "65067dcad93a61deb593be7d3d9a32a4577d09665536d8da536d731da5cd15e2", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.4.2/rules_nodejs-3.4.2.tar.gz"], + sha256 = "10f534e1c80f795cffe1f2822becd4897754d18564612510c59b3c73544ae7c6", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.5.0/rules_nodejs-3.5.0.tar.gz"], ) # Now that we have the rules let's import from them to complete the work load("@build_bazel_rules_nodejs//:index.bzl", "check_rules_nodejs_version", "node_repositories", "yarn_install") # Assure we have at least a given rules_nodejs version -check_rules_nodejs_version(minimum_version_string = "3.4.2") +check_rules_nodejs_version(minimum_version_string = "3.5.0") # Setup the Node.js toolchain for the architectures we want to support # diff --git a/docs/concepts/index-patterns.asciidoc b/docs/concepts/index-patterns.asciidoc index 158fa6282e6fa..03bad72a317c6 100644 --- a/docs/concepts/index-patterns.asciidoc +++ b/docs/concepts/index-patterns.asciidoc @@ -10,10 +10,9 @@ or all indices that contain your data. It can also point to a You’ll learn how to: -* Create an index pattern -* Explore and configure the data fields +* Create index patterns * Set the default index pattern -* Delete an index pattern +* Delete index patterns [float] [[index-patterns-read-only-access]] @@ -133,77 +132,23 @@ To exclude a cluster, use `cluster_*:logstash-*,cluster_one:-*`. Once an index pattern is configured using the {ccs} syntax, all searches and aggregations using that index pattern in {kib} take advantage of {ccs}. - -[float] -[[reload-fields]] -=== Explore and configure the data fields - -To explore and configure the data fields in your index pattern, open the main menu, then click -*Stack Management > Index Patterns*. Each field has a {ref}/mapping.html[mapping], -which indicates the type of data the field contains in {es}, -such as strings or boolean values. The field mapping also determines -how you can use the field, such as whether it can be searched or aggregated. - -When a new field is added to the index, the index pattern field list is updated -the next time the index pattern is loaded, for example, when you load the page or -move between {kib} apps. - -[role="screenshot"] -image:management/index-patterns/images/new-index-pattern.png["Create index pattern"] - -[float] -=== Format the display of common field types - -Whenever possible, {kib} uses the same field type for display as -{es}. However, some field types that {es} supports are not available -in {kib}. Using field formatters, you can manually change the field type in {kib} to display your data the way you prefer -to see it, regardless of how it is stored in {es}. - -For example, if you store -date values in {es}, you can use a {kib} field formatter to change the display to mm/dd/yyyy format. -{kib} has field formatters for -<>, -<>, -<>, -and <>. - -To customize the displayed field name provided by {es}, you can -use *Custom Label* . - -A popularity counter keeps track of the fields you use most often. -The top five most popular fields and their values are displayed in <>. - -To edit the field display, click the edit icon -(image:management/index-patterns/images/edit_icon.png[]) in the index pattern detail view. - -[role="screenshot"] -image:management/index-patterns/images/edit-field-format.png["Edit field format"] - [float] -[[default-index-pattern]] -=== Set the default index pattern +[[delete-index-pattern]] +=== Delete index patterns -The first index pattern you create is automatically designated as the default pattern, -but you can set any index pattern as the default. The default index pattern is automatically selected when you first open <> or create a visualization from scratch. +When you delete an index pattern, you are unable to recover the associated field formatters, scripted fields, source filters, +and field popularity data. Deleting an index pattern does not remove any indices or data documents from {es}. -. In *Index patterns*, click the index pattern name. -. Click the star icon (image:management/index-patterns/images/star.png[Star icon]). +WARNING: Deleting an index pattern breaks all visualizations, saved searches, and other saved objects that reference the index pattern. -[float] -[[delete-index-pattern]] -=== Delete an index pattern +. Open the main menu, then click *Stack Management > Index Patterns*. -This action removes the pattern from the list of saved objects in {kib}. -You will not be able to recover field formatters, scripted fields, source filters, -and field popularity data associated with the index pattern. Deleting an -index pattern does not remove any indices or data documents from {es}. +. Click the index pattern you want to delete. -WARNING: Deleting an index pattern breaks all visualizations, saved searches, and other saved objects that reference the pattern. - -. In *Index patterns*, click the index pattern name. -. Click the delete icon (image:management/index-patterns/images/delete.png[Delete icon]). +. Delete (image:management/index-patterns/images/delete.png[Delete icon]) the index pattern. [float] +[[reload-fields]] === What’s next -* Learn about <> and how to create data on the fly. +Learn how to <> in your index patterns. diff --git a/docs/concepts/index.asciidoc b/docs/concepts/index.asciidoc index 74e5bd4d4fb2f..cb37dceb53564 100644 --- a/docs/concepts/index.asciidoc +++ b/docs/concepts/index.asciidoc @@ -49,10 +49,9 @@ that accesses the {kib} API. {kib} uses the index pattern to show you a list of fields, such as `event.duration`. You can customize the display name and format for each field. -For example, you can tell Kibana to display `event.duration` in seconds. +For example, you can tell {kib} to display `event.duration` in seconds. {kib} has <> for strings, -dates, geopoints, -and numbers. +dates, geopoints, and numbers. [float] [[kibana-concepts-searching-your-data]] diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index cba5b9bfadf98..92dc2a1a24377 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -74,6 +74,7 @@ yarn kbn watch-bazel - @kbn/config-schema - @kbn/crypto - @kbn/dev-utils +- @kbn/docs-utils - @kbn/es - @kbn/eslint-import-resolver-kibana - @kbn/eslint-plugin-eslint @@ -81,10 +82,14 @@ yarn kbn watch-bazel - @kbn/legacy-logging - @kbn/logging - @kbn/securitysolution-constants -- @kbn/securitysolution-utils +- @kbn/securitysolution-es-utils +- kbn/securitysolution-io-ts-alerting-types +- kbn/securitysolution-io-ts-list-types +- kbn/securitysolution-io-ts-types - @kbn/securitysolution-io-ts-utils +- @kbn/securitysolution-utils - @kbn/std +- @kbn/telemetry-utils - @kbn/tinymath - @kbn/utility-types - @kbn/utils - 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 ac625095da2a4..78d2d8daa3d45 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,7 +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 canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: 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: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | - diff --git a/docs/discover/images/add-field-to-pattern.png b/docs/discover/images/add-field-to-pattern.png new file mode 100644 index 0000000000000..84dfcb0745c69 Binary files /dev/null and b/docs/discover/images/add-field-to-pattern.png differ diff --git a/docs/discover/images/hello-field.png b/docs/discover/images/hello-field.png new file mode 100644 index 0000000000000..07d97e054d7ec Binary files /dev/null and b/docs/discover/images/hello-field.png differ diff --git a/docs/management/field-formatters/color-formatter.asciidoc b/docs/management/field-formatters/color-formatter.asciidoc index d9ba5e9be1165..488fb37153799 100644 --- a/docs/management/field-formatters/color-formatter.asciidoc +++ b/docs/management/field-formatters/color-formatter.asciidoc @@ -1,10 +1,5 @@ -The `Color` field formatter enables you to specify colors with specific ranges of values for a numeric field. +The *Color* field formatter enables you to specify colors with ranges of values for a number field. -When you select the `Color` field formatter, Kibana displays the *Range*, *Font Color*, *Background Color*, and -*Example* fields. - -Click the *Add Color* button to add a range of values to associate with a particular color. You can click in the *Font -Color* and *Background Color* fields to display a color picker. You can also enter a specific hex code value in the -field. The effect of your current color choices are displayed in the *Example* field. +When you select the *Color* formatter, click *Add Color*, then specify the *Range*, *Text color*, and *Background color*. image::images/colorformatter.png[] diff --git a/docs/management/field-formatters/duration-formatter.asciidoc b/docs/management/field-formatters/duration-formatter.asciidoc index 36a73f61f6227..873a4ac94c291 100644 --- a/docs/management/field-formatters/duration-formatter.asciidoc +++ b/docs/management/field-formatters/duration-formatter.asciidoc @@ -1,4 +1,4 @@ -The `Duration` field formatter can display the numeric value of a field in the following increments: +The *Duration* field formatter displays the numeric value of a field in the following increments: * Picoseconds * Nanoseconds @@ -12,4 +12,4 @@ The `Duration` field formatter can display the numeric value of a field in the f * Months * Years -You can specify these increments with up to 20 decimal places for both input and output formats. +You can specify these increments with up to 20 decimal places for input and output formats. diff --git a/docs/management/field-formatters/string-formatter.asciidoc b/docs/management/field-formatters/string-formatter.asciidoc index ed3aa45873284..f32eee7dc8396 100644 --- a/docs/management/field-formatters/string-formatter.asciidoc +++ b/docs/management/field-formatters/string-formatter.asciidoc @@ -1,11 +1,20 @@ -The `String` field formatter can apply the following transformations to the field's contents: +The *String* field formatter enables you to apply transforms to the field. + +Supported transformations include: * Convert to lowercase + * Convert to uppercase + * Convert to title case -* Apply the short dots transformation, which replaces the content before a `.` character with the first character of -that content, as in the following example: + +* Apply the short dots transformation, which replaces the content before the `.` character with the first character of +the content. For example: [horizontal] *Original*:: *Becomes* `com.organizations.project.ClassName`:: `c.o.p.ClassName` + +* Base64 decode + +* URL param decode diff --git a/docs/management/field-formatters/url-formatter.asciidoc b/docs/management/field-formatters/url-formatter.asciidoc index 41d4f75603dc6..8b0e43c9f2496 100644 --- a/docs/management/field-formatters/url-formatter.asciidoc +++ b/docs/management/field-formatters/url-formatter.asciidoc @@ -1,33 +1,32 @@ -The `Url` field formatter can take on the following types: +You can specify the following types to the `Url` field formatter: -* The *Link* type turn the contents of the field into an URL. -* The *Image* type can be used to specify an image directory where a specified image is located. -* The *Audio* type can be used to specify an audio directory where a specified audio file is located. +* *Link* — Converts the contents of the field into an URL. You can specify the width and height of the image, while keeping the aspect ratio. +When the image is smaller than the specified paramters, the image is unable to upscale. +* *Image* — Specifies the image directory. +* *Audio* — Specify the audio directory. -For an *Image* type you can specify width and height attributes. These will be used to set the max width / max height of the image, while keeping the aspect ratio. Image will not be upscaled if it's smaller than the provided size parameters. - -You can customize either type of URL field formats with templates. A _URL template_ enables you to add specific values -to a partial URL. Use the string `{{value}}` to add the contents of the field to a fixed URL. +To customize URL field formats, use templates. An *URL template* enables you to add values +to a partial URL. To add the contents of the field to a fixed URL, use the `{{value}}` string. For example, when: * A field contains a user ID -* That field uses the `Url` field formatter +* A field uses the `Url` field formatter * The URI template is `http://company.net/profiles?user_id={­{value}­}` The resulting URL replaces `{{value}}` with the user ID from the field. The `{{value}}` template string URL-encodes the contents of the field. When a field encoded into a URL contains -non-ASCII characters, these characters are replaced with a `%` character and the appropriate hexadecimal code. For +non-ASCII characters, the characters are replaced with a `%` character and the appropriate hexadecimal code. For example, field contents `users/admin` result in the URL template adding `users%2Fadmin`. -When the formatter type is set to *Image*, the `{{value}}` template string specifies the name of an image at the +When the formatter type is *Image*, the `{{value}}` template string specifies the name of an image at the specified URI. -When the formatter type is set to *Audio*, the `{{value}}` template string specifies the name of an audio file at the specified URI. +When the formatter type is *Audio*, the `{{value}}` template string specifies the name of an audio file at the specified URI. -In order to pass unescaped values directly to the URL, use the `{{rawValue}}` string. +To pass unescaped values directly to the URL, use the `{{rawValue}}` string. -A _Label Template_ enables you to specify a text string that displays instead of the raw URL. You can use the +A *Label template* enables you to specify a text string that appears instead of the raw URL. You can use the `{{value}}` template string normally in label templates. You can also use the `{{url}}` template string to display the formatted URL. diff --git a/docs/management/images/colorformatter.png b/docs/management/images/colorformatter.png index df5dc34dd31e5..3c2cfad62d76a 100644 Binary files a/docs/management/images/colorformatter.png and b/docs/management/images/colorformatter.png differ diff --git a/docs/management/manage-index-patterns.asciidoc b/docs/management/manage-index-patterns.asciidoc new file mode 100644 index 0000000000000..94870733174ad --- /dev/null +++ b/docs/management/manage-index-patterns.asciidoc @@ -0,0 +1,264 @@ +[[managing-index-patterns]] +== Manage index pattern data fields + +To customize the data fields in your index pattern, you can add runtime fields to the existing documents, add scrited fields to compute data on the fly, and change how {kib} displays the data fields. + +[float] +[[runtime-fields]] +=== Explore your data with runtime fields + +Runtime fields are fields that you add to documents after you've ingested, and are evaluated at query time. With runtime fields, you allow for a smaller index and faster ingest time so that you can use less resources and reduce your operating costs. You can use runtime fields anywhere index patterns are used. + +When you use runtime fields, you can: + +* Define fields for a specific use without modifying the underlying schema. + +* Override the returned values from index fields. + +* Start working on your data without first understanding the structure. + +* Add fields to existing documents without reindexing your data. + +* Explore runtime field data in *Discover*. + +* Create visualizations with runtime field data using *Lens*, *Maps*, and *TSVB*. + +WARNING: Runtime fields can impact {kib} performance. When you run a query, {es} uses the fields you index first to shorten the response time. +Index the fields that you commonly search for and filter on, such as `timestamp`, then use runtime fields to limit the number of fields {es} uses to calculate values. + +For more information, refer to {ref}/runtime.html[Runtime fields]. + +[float] +[[create-runtime-fields]] +==== Create runtime fields + +Create runtime fields in your index patterns, or create runtime fields in *Discover* and *Lens*. + +. Open the main menu, then click *Stack Management > Index Patterns*. + +. Select the index pattern you want to add the runtime field to, then click *Add field*. + +. Enter a *Name* for the runtime field, then select the field *Type*. + +. Select *Set value*, then define the field value by emitting a single value using the {ref}/modules-scripting-painless.html[Painless scripting language]. ++ +The script must match the field *Type*, or the script fails. + +. Click *Create field*. +//+ +//For information on how to create runtime fields in *Discover*, refer to <>. ++ +For information on how to create runtime fields in *Lens*, refer to <>. + +[float] +[[runtime-field-examples]] +==== Runtime field examples + +Try the runtime field examples on your own using the *Sample web logs* data index pattern. + +[float] +[[simple-hello-world-example]] +==== Return a keyword value + +To return `Hello World!` value: + +[source,text] +---- +emit("Hello World!"); +---- + +[float] +[[perform-a-calculation-on-a-single-field]] +===== Perform a calculation on a single field + +Calculate kilobytes from bytes: + +[source,text] +---- +emit(doc['bytes'].value / 1024) +---- + +[float] +[[return-substring]] +===== Return a substring + +Return the string that appears after the last slash in the URL: + +[source,text] +---- +def path = doc["url.keyword"].value; +if (path != null) { + int lastSlashIndex = path.lastIndexOf('/'); + if (lastSlashIndex > 0) { + emit(path.substring(lastSlashIndex+1)); + return; + } +} +emit(""); +---- + +[float] +[[replace-nulls-with-blanks]] +===== Replace nulls with blanks + +Replace null values with none values: + +[source,text] +---- +def source = doc['referer'].value; +if (source != null) { + emit(source); + return; +} +else { + emit("None"); +} +---- + +Specify operating system condition: + +[source,text] +---- +def source = doc['machine.os.keyword'].value; +if (source != "") { + emit(source); +} +else { + emit("None"); +} +---- + +[float] +[[manage-runtime-fields]] +==== Manage runtime fields + +Edit the settings for runtime fields, or remove runtime fields from index patterns. + +. Open the main menu, then click *Stack Management > Index Patterns*. + +. Select the index pattern that contains the runtime field you want to manage, then open the runtime field edit options or delete the runtime field. + +[float] +[[scripted-fields]] +=== Add scripted fields to index patterns + +deprecated::[7.13,Use {ref}/runtime.html[runtime fields] instead of scripted fields. Runtime fields support Painless scripts and provide greater flexibility.] + +Scripted fields compute data on the fly from the data in your {es} indices. The data is shown on +the Discover tab as part of the document data, and you can use scripted fields in your visualizations. You query scripted fields with the <>, and can filter them using the filter bar. The scripted field values are computed at query time, so they aren't indexed and cannot be searched using the {kib} default +query language. + +WARNING: Computing data on the fly with scripted fields can be very resource intensive and can have a direct impact on +{kib} performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are +buggy, you'll get exceptions whenever you try to view the dynamically generated data. + +When you define a scripted field in {kib}, you have a choice of the {ref}/modules-scripting-expression.html[Lucene expressions] or the +{ref}/modules-scripting-painless.html[Painless] scripting language. + +You can reference any single value numeric field in your expressions, for example: + +---- +doc['field_name'].value +---- + +For more information on scripted fields and additional examples, refer to +https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless in {kib} scripted fields] + +[float] +[[create-scripted-field]] +==== Create scripted fields + +Create and add scripted fields to your index patterns. + +. Open the main menu, then click *Stack Management > Index Patterns*. + +. Select the index pattern you want to add a scripted field to. + +. Select the *Scripted fields* tab, then click *Add scripted field*. + +. Enter a *Name* for the scripted field, then enter the *Script* you want to use to compute a value on the fly from your index data. + +. Click *Create field*. + +For more information about scripted fields in {es}, refer to {ref}/modules-scripting.html[Scripting]. + +[float] +[[update-scripted-field]] +==== Manage scripted fields + +. Open the main menu, then click *Stack Management > Index Patterns*. + +. Select the index pattern that contains the scripted field you want to manage. + +. Select the *Scripted fields* tab, then open the scripted field edit options or delete the scripted field. + +WARNING: Built-in validation is unsupported for scripted fields. When your scripts contain errors, you receive +exceptions when you view the dynamically generated data. + +[float] +[[managing-fields]] +=== Format data fields + +{kib} uses the same field types as {es}, however, some {es} field types are unsupported in {kib}. +To customize how {kib} displays data fields, use the formatting options. + +. Open the main menu, then click *Stack Management > Index Patterns*. + +. Click the index pattern that contains the field you want to change. + +. Find the field, then open the edit options (image:management/index-patterns/images/edit_icon.png[Data field edit icon]). + +. Select *Set custom label*, then enter a *Custom label* for the field. + +. Select *Set format*, then enter the *Format* for the field. + +[float] +[[string-field-formatters]] +==== String field formatters + +String fields support *String* and *Url* formatters. + +include::field-formatters/string-formatter.asciidoc[] + +include::field-formatters/url-formatter.asciidoc[] + +[float] +[[field-formatters-date]] +==== Date field formatters + +Date fields support *Date*, *String*, and *Url* formatters. + +The *Date* formatter enables you to choose the display format of date stamps using the https://momentjs.com/[moment.js] +standard format definitions. + +include::field-formatters/string-formatter.asciidoc[] + +include::field-formatters/url-formatter.asciidoc[] + +[float] +[[field-formatters-geopoint]] +==== Geographic point field formatters + +Geographic point fields support the *String* formatter. + +include::field-formatters/string-formatter.asciidoc[] + +[float] +[[field-formatters-numeric]] +==== Number field formatters + +Numeric fields support *Bytes*, *Color*, *Duration*, *Histogram*, *Number*, *Percentage*, *String*, and *Url* formatters. + +The *Bytes*, *Number*, and *Percentage* formatters enable you to choose the display formats of numbers in the field using +the <> syntax that {kib} maintains. + +The *Histogram* formatter is used only for the {ref}/histogram.html[histogram field type]. When you use the *Histogram* formatter, +you can apply the *Bytes*, *Number*, or *Percentage* format to aggregated data. + +include::field-formatters/url-formatter.asciidoc[] + +include::field-formatters/string-formatter.asciidoc[] + +include::field-formatters/duration-formatter.asciidoc[] + +include::field-formatters/color-formatter.asciidoc[] \ No newline at end of file diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc deleted file mode 100644 index 505f6853c7906..0000000000000 --- a/docs/management/managing-fields.asciidoc +++ /dev/null @@ -1,134 +0,0 @@ -[[managing-fields]] -== Field management - -Whenever possible, -{kib} uses the same field type for display as {es}. However, a few field types -{es} supports are not available in {kib}. Use field formatters to customize how your -fields are displayed in Kibana, regardless of how they are stored in {es}. - -Kibana provides these field formatters: - -* <> -* <> -* <> -* <> - -To format a field: - -. Open the main menu, and click *Stack Management > Index Patterns*. -. Click the index pattern that contains the field you want to format. -. Find the field you want to format and click the edit icon (image:management/index-patterns/images/edit_icon.png[]). -. Enter a custom label for the field, if needed. -. Select a format and fill in the details. -+ -[role="screenshot"] -image:management/index-patterns/images/edit-field-format.png["Edit field format"] - - - -[[field-formatters-string]] -=== String field formatters - -String fields support the `String` and `Url` formatters. - -include::field-formatters/string-formatter.asciidoc[] - -include::field-formatters/url-formatter.asciidoc[] - -[[field-formatters-date]] -=== Date field formatters - -Date fields support the `Date`, `Url`, and `String` formatters. - -The `Date` formatter enables you to choose the display format of date stamps using the https://momentjs.com/[moment.js] -standard format definitions. - -include::field-formatters/string-formatter.asciidoc[] - -include::field-formatters/url-formatter.asciidoc[] - -[[field-formatters-geopoint]] -=== Geographic point field formatters - -Geographic point fields support the `String` formatter. - -include::field-formatters/string-formatter.asciidoc[] - -[[field-formatters-numeric]] -=== Numeric field formatters - -Numeric fields support the `Url`, `Bytes`, `Duration`, `Number`, `Percentage`, `Histogram`, `String`, and `Color` formatters. - -The `Bytes`, `Number`, and `Percentage` formatters enable you to choose the display formats of numbers in this field using -the <> syntax that {kib} maintains. - -The `Histogram` formatter is only used for the {ref}/histogram.html[histogram field type]. When using the `Histogram` formatter, -you can apply the `Number`, `Bytes`, or `Percentage` format to the aggregated data. - -`Number`, and `Percentage` formatters enable you to choose the display formats of numbers in this field using -the <> syntax that {kib} maintains. - -include::field-formatters/url-formatter.asciidoc[] - -include::field-formatters/string-formatter.asciidoc[] - -include::field-formatters/duration-formatter.asciidoc[] - -include::field-formatters/color-formatter.asciidoc[] - -[[scripted-fields]] -=== Scripted fields -deprecated::[7.13,Use {ref}/runtime.html[runtime fields] instead of scripted fields. Runtime fields support Painless scripts and provide greater flexibility.] - -Scripted fields compute data on the fly from the data in your {es} indices. The data is shown on -the Discover tab as part of the document data, and you can use scripted fields in your visualizations. You query scripted fields with the <>, and can filter them using the filter bar. The scripted field values are computed at query time, so they aren't indexed and cannot be searched using the {kib} default -query language. - -WARNING: Computing data on the fly with scripted fields can be very resource intensive and can have a direct impact on -{kib} performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are -buggy, you'll get exceptions whenever you try to view the dynamically generated data. - -When you define a scripted field in {kib}, you have a choice of the {ref}/modules-scripting-expression.html[Lucene expressions] or the -{ref}/modules-scripting-painless.html[Painless] scripting language. - -You can reference any single value numeric field in your expressions, for example: - ----- -doc['field_name'].value ----- - -For more information on scripted fields and additional examples, refer to -https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless in {kib} scripted fields] - -[float] -[[create-scripted-field]] -=== Create a scripted field - -. Open the main menu, then click *Stack Management > Index Patterns*. -. Select the index pattern you want to add a scripted field to. -. Go to the *Scripted fields* tab for the index pattern, then click *Add scripted field*. -. Enter a name for the scripted field. -. Enter the expression that you want to use to compute a value on the fly from your index data. -. Click *Create field*. - -For more information about scripted fields in {es}, see -{ref}/modules-scripting.html[Scripting]. - -[float] -[[update-scripted-field]] -=== Update a scripted field - -. Click the *Scripted fields* tab for the index pattern. -. Click the *Edit* button for the scripted field you want to change. -. Make your changes, then click *Save field*. - -WARNING: Built-in validation is unsupported for scripted fields. If your scripts are buggy, you'll get -exceptions whenever you try to view the dynamically generated data. - -[float] -[[delete-scripted-field]] -=== Delete a scripted field - -. Click the *Scripted fields* tab for the index pattern. -. Click *Delete* for the scripted field you want to remove. -. Click *Delete* on the confirmation window. diff --git a/docs/maps/trouble-shooting.asciidoc b/docs/maps/trouble-shooting.asciidoc index 0e1ed0b9e1bec..a58e8ac8902b8 100644 --- a/docs/maps/trouble-shooting.asciidoc +++ b/docs/maps/trouble-shooting.asciidoc @@ -26,7 +26,7 @@ image::maps/images/inspector.png[] * Verify your geospatial data is correctly mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. ** Run `GET myIndexPatternTitle/_field_caps?fields=myGeoFieldName` in <>, replacing `myIndexPatternTitle` and `myGeoFieldName` with your index pattern title and geospatial field name. ** Ensure response specifies `type` as `geo_point` or `geo_shape`. -* Verify your geospatial data is correctly mapped in your <>. +* Verify your geospatial data is correctly mapped in your <>. ** Open your index pattern in <>. ** Ensure your geospatial field type is `geo_point` or `geo_shape`. ** Ensure your geospatial field is searchable and aggregatable. diff --git a/docs/maps/vector-tooltips.asciidoc b/docs/maps/vector-tooltips.asciidoc index b0498c9088e4e..2dda35aa28f76 100644 --- a/docs/maps/vector-tooltips.asciidoc +++ b/docs/maps/vector-tooltips.asciidoc @@ -18,7 +18,7 @@ image::maps/images/multifeature_tooltip.png[] ==== Format tooltips You can format the attributes in a tooltip by adding <> to your -Kibana index pattern. You can use field formatters to round numbers, provide units, +index pattern. You can use field formatters to round numbers, provide units, and even display images in your tooltip. [float] diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 4aedb0f516b20..a14bda2bf5a98 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -279,26 +279,32 @@ This content has moved. Refer to <>. [role="exclude",id="ingest-node-pipelines"] == Ingest Node Pipelines -This content has moved. See {ref}/ingest.html[Ingest pipelines]. +This content has moved. Refer to {ref}/ingest.html[Ingest pipelines]. [role="exclude",id="create-panels-with-timelion"] == Timelion -This content has moved. refer to <>. +This content has moved. Refer to <>. [role="exclude",id="space-rbac-tutorial"] == Tutorial: Use role-based access control to customize Kibana spaces -This content has moved. refer to <>. +This content has moved. Refer to <>. [role="exclude",id="search"] == Search your data -This content has moved. refer to <>. +This content has moved. Refer to <>. [role="exclude",id="discover-document-context"] == View surrounding documents -This content has moved. refer to <>. +This content has moved. Refer to <>. + +[role="exclude",id="field-formatters-string"] +== String field formatters + +This content has moved. Refer to <>. + diff --git a/docs/user/alerting/images/rule-details-alerts-inactive.png b/docs/user/alerting/images/rule-details-alerts-inactive.png index f84910ae0dcdc..fc82cf465ebb2 100644 Binary files a/docs/user/alerting/images/rule-details-alerts-inactive.png and b/docs/user/alerting/images/rule-details-alerts-inactive.png differ diff --git a/docs/user/dashboard/images/lens_advanced_2_1.png b/docs/user/dashboard/images/lens_advanced_2_1.png index 5090f0d3b2841..dab32369d71a1 100644 Binary files a/docs/user/dashboard/images/lens_advanced_2_1.png and b/docs/user/dashboard/images/lens_advanced_2_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_2_1_1.png b/docs/user/dashboard/images/lens_advanced_2_1_1.png deleted file mode 100644 index f4d9ca488782e..0000000000000 Binary files a/docs/user/dashboard/images/lens_advanced_2_1_1.png and /dev/null differ diff --git a/docs/user/dashboard/images/lens_advanced_3_1.gif b/docs/user/dashboard/images/lens_advanced_2_2.gif similarity index 100% rename from docs/user/dashboard/images/lens_advanced_3_1.gif rename to docs/user/dashboard/images/lens_advanced_2_2.gif diff --git a/docs/user/dashboard/images/lens_advanced_2_2.png b/docs/user/dashboard/images/lens_advanced_2_2.png index 820bc3bd4dfa9..1d88bcd238ca3 100644 Binary files a/docs/user/dashboard/images/lens_advanced_2_2.png and b/docs/user/dashboard/images/lens_advanced_2_2.png differ diff --git a/docs/user/dashboard/images/lens_advanced_2_2_1.png b/docs/user/dashboard/images/lens_advanced_2_2_1.png index 3044f1070367d..c3fb697666b46 100644 Binary files a/docs/user/dashboard/images/lens_advanced_2_2_1.png and b/docs/user/dashboard/images/lens_advanced_2_2_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_3_3.png b/docs/user/dashboard/images/lens_advanced_2_3.png similarity index 100% rename from docs/user/dashboard/images/lens_advanced_3_3.png rename to docs/user/dashboard/images/lens_advanced_2_3.png diff --git a/docs/user/dashboard/images/lens_advanced_3_1.png b/docs/user/dashboard/images/lens_advanced_3_1.png new file mode 100644 index 0000000000000..1473b203924a3 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_3_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_3_1_1.png b/docs/user/dashboard/images/lens_advanced_3_1_1.png deleted file mode 100644 index c3fb697666b46..0000000000000 Binary files a/docs/user/dashboard/images/lens_advanced_3_1_1.png and /dev/null differ diff --git a/docs/user/dashboard/images/lens_advanced_3_2.png b/docs/user/dashboard/images/lens_advanced_3_2.png index 20da2ed706dfd..15f2f0228a0fc 100644 Binary files a/docs/user/dashboard/images/lens_advanced_3_2.png and b/docs/user/dashboard/images/lens_advanced_3_2.png differ diff --git a/docs/user/dashboard/images/lens_advanced_4_1.png b/docs/user/dashboard/images/lens_advanced_4_1.png index 43c8db213d482..50d1affa268dd 100644 Binary files a/docs/user/dashboard/images/lens_advanced_4_1.png and b/docs/user/dashboard/images/lens_advanced_4_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_4_2.png b/docs/user/dashboard/images/lens_advanced_4_2.png deleted file mode 100644 index 4b3e98910e7b7..0000000000000 Binary files a/docs/user/dashboard/images/lens_advanced_4_2.png and /dev/null differ diff --git a/docs/user/dashboard/images/lens_advanced_5_1.png b/docs/user/dashboard/images/lens_advanced_5_1.png new file mode 100644 index 0000000000000..5090f0d3b2841 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_5_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_5_2.png b/docs/user/dashboard/images/lens_advanced_5_2.png new file mode 100644 index 0000000000000..820bc3bd4dfa9 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_5_2.png differ diff --git a/docs/user/dashboard/images/lens_advanced_5_2_1.png b/docs/user/dashboard/images/lens_advanced_5_2_1.png new file mode 100644 index 0000000000000..3044f1070367d Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_5_2_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_6_1.png b/docs/user/dashboard/images/lens_advanced_6_1.png new file mode 100644 index 0000000000000..5d5cefa472a13 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_6_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_7_1.png b/docs/user/dashboard/images/lens_advanced_7_1.png new file mode 100644 index 0000000000000..3d66d5d7e4579 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_7_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_result.png b/docs/user/dashboard/images/lens_advanced_result.png index 19963d87c8e1c..8cf087f936abd 100644 Binary files a/docs/user/dashboard/images/lens_advanced_result.png and b/docs/user/dashboard/images/lens_advanced_result.png differ diff --git a/docs/user/dashboard/images/manage-runtime-field.gif b/docs/user/dashboard/images/manage-runtime-field.gif new file mode 100644 index 0000000000000..c6ecf0caf8180 Binary files /dev/null and b/docs/user/dashboard/images/manage-runtime-field.gif differ diff --git a/docs/user/dashboard/images/runtime-field-menu.png b/docs/user/dashboard/images/runtime-field-menu.png new file mode 100644 index 0000000000000..891de38bb6833 Binary files /dev/null and b/docs/user/dashboard/images/runtime-field-menu.png differ diff --git a/docs/user/dashboard/lens-advanced.asciidoc b/docs/user/dashboard/lens-advanced.asciidoc index 6b090f6017f5d..e49db0c0d026d 100644 --- a/docs/user/dashboard/lens-advanced.asciidoc +++ b/docs/user/dashboard/lens-advanced.asciidoc @@ -1,11 +1,11 @@ [[create-a-dashboard-of-panels-with-ecommerce-data]] -== Tutorial: Create a dashboard of panels with ecommerce sales data +== Time series analysis with Lens -You collected sales data from your store, and you want to visualize and analyze the data on a dashboard. +The tutorial uses sample data from the perspective of a shop owner looking +at sales trends, but this type of dashboard works on any type of data. To create dashboard panels of the data, open the *Lens* visualization builder, then create the visualization panels that best display the data. - -When you've completed the tutorial, you'll have a dashboard that provides you with a complete overview of your ecommerce sales data. +Before using this tutorial, you should be familiar with the <>. [role="screenshot"] image::images/lens_advanced_result.png[Dashboard view] @@ -14,36 +14,52 @@ image::images/lens_advanced_result.png[Dashboard view] [[add-the-data-and-create-the-dashboard-advanced]] === Add the data and create the dashboard -To create visualizations of the data from your store, add the data set, then create the dashboard. +If you are working with your own data, you should already have an <>. +To install the sample sales data: . From the {kib} *Home* page, click *Try our sample data*. . From *Sample eCommerce orders*, click *Add data*. +Then create a new dashboard: + . Open the main menu, then click *Dashboard*. . On the *Dashboards* page, click *Create dashboard*. +. Set the <> to *Last 30 days*. + [float] [[open-and-set-up-lens-advanced]] === Open and set up Lens -Open the *Lens* editor, then make sure the correct fields appear. +*Lens* is designed to help you quickly build visualizations for your dashboard, as shown in <>, while providing support for advanced usage as well. -. From the dashboard, click *Create panel*. +Open the *Lens* editor, then make sure the correct fields appear. -. On the *New visualization* window, click *Lens*. -+ -[role="screenshot"] -image::images/lens_end_to_end_1_1.png[New visualization popover] +. From the dashboard, click *Create visualization*. -. Make sure the *kibana_sample_data_ecommerce_* index appears. +. Make sure the *kibana_sample_data_ecommerce* index appears. [discrete] -[[view-the-number-of-transactions-per-day]] -=== View the number of transactions per hour +[[custom-time-interval]] +=== View a date histogram with a custom time interval + +It is common to use the automatic date histogram interval, but sometimes you want a larger or smaller +interval. *Lens* only lets you choose the minimum time interval, not the exact time interval, for +performance reasons. The performance limit is controlled by the <> +advanced setting and the overall time range. To see hourly sales over a 30 day time period, choose +one of these options: -To determine the number of orders made every hour, create a bar chart, then add the chart to the dashboard. +* View less than 30 days at a time, then use the time picker to select each day separately. + +* Increase `histogram:maxBars` from 100 to at least 720, which the number of hours in 30 days. +This affects all visualizations and can reduce performance. + +* If approximation is okay, use the *Normalize unit* option. This can convert *Average sales per 12 hours* +into *Average sales per 12 hours (per hour)* by dividing the number of hours. + +For the sample data, approximation is okay. To use the *Normalize unit* option: . Set the <> to *Last 30 days*. @@ -75,183 +91,258 @@ image::images/lens_advanced_1_2.png[Orders per day] . Click *Save and return*. [discrete] -[[view-the-cumulative-number-of-products-sold-over-time]] -=== View the cumulative number of products sold on weekends +[[add-a-data-layer-advanced]] +=== Monitor multiple series within a date histogram -To determine the number of orders made only on Saturday and Sunday, create an area chart, then add it to the dashboard. +It is often required to monitor multiple series within a time interval. These series can be have similar configurations with few changes between one and another. +*Lens* copies a function when you drag and drop it to the *Drop a field or click to add* +field within the same group, or when you drag and drop to the *Duplicate* field on a different group. +You can also drag and drop using your keyboard. For more information, refer to <>. -. Open *Lens*. +To quickly create many copies of a percentile metric that shows distribution of price over time: -. From the *Chart Type* dropdown, select *Area*. +. From the *Chart Type* dropdown, select *Line*. + [role="screenshot"] -image::images/lens_advanced_2_1_1.png[Chart type menu with Area selected] +image::images/lens_advanced_2_1.png[Chart type menu with Line selected] -. Configure the cumulative sum of the store orders. - -.. From the *Available fields* list, drag and drop *Records* to the visualization builder. +. From the *Available fields* list, drag and drop *products.price* to the visualization builder. -.. From the editor, click *Count of Records*. +. Create the 95th percentile. -.. From *Select a function*, click *Cumulative sum*. +.. In the editor, click *Median of products.price*. -.. In the *Display name* field, enter `Cumulative orders during weekend days`, then click *Close*. +.. From *Select a function*, click *Percentile*. -. Filter the results to display the data for only Saturday and Sunday. +.. In the *Display name* field, enter `95th`, then click *Close*. -.. From the editor, click the *Drop a field or click to add* field for *Break down by*. +. To create the 90th percentile, duplicate the `95th` percentile. -.. From *Select a function*, click *Filters*. +.. Drag and drop *95th* to *Drop a field or click to add*. -.. Click *All records*. +.. Click *95th [1]*, then enter `90` in the *Percentile* field. -.. In the *KQL* field, enter `day_of_week : "Saturday" or day_of_week : "Sunday"`, then press Return. -+ -The <> displays all documents where `day_of_week` matches `Saturday` or `Sunday`. +.. In the *Display name* field enter `90th`, then click *Close*. + [role="screenshot"] -image::images/lens_advanced_2_1.png[Filter aggregation to filter weekend days] +image::images/lens_advanced_2_2.gif[Easily duplicate the items with drag and drop] -. To hide the legend, open the *Legend* menu, then click *Hide*. +.. Repeat the duplication steps to create the `50th` and `10th` percentile, naming them accordingly. + +. To change the left axis label, open the *Left Axis* menu, then enter `Percentiles for product prices` in the *Axis name* field. + [role="screenshot"] -image::images/lens_advanced_2_2_1.png[Legend menu] +image::images/lens_advanced_2_2_1.png[Left Axis menu] + -You have an area chart that shows you how many orders your store received during the weekend. +You have a line chart that shows you the price distribution of products sold over time. + [role="screenshot"] -image::images/lens_advanced_2_2.png[Line chart with cumulative sum of orders made on the weekend] +image::images/lens_advanced_2_3.png[Percentiles for product prices chart] -. Click *Save and return*. +. Add the filter for the redirect codes. [discrete] -[[add-a-data-layer-advanced]] -=== Create multiple key percentiles of product prices - -To view the price distribution of products sold over time, create a percentile chart, then add it to the dashboard. - -. Open *Lens*. - -. From the *Chart Type* dropdown, select *Line*. - -. From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. - -* Drag and drop *products.price* to the *Vertical axis* field. +[[add-a-data-layer]] +==== Multiple chart types or index patterns in one visualization -* Drag and drop *order_date* to the *Horizontal axis* field. +You can add multiple metrics to a single chart type, but if you want to overlay +multiple chart types or index patterns, use a second layer. When building layered charts, +it is important to match the data on the horizontal axis so that it uses the same +scale. To add a line chart layer on top of an existing chart: -. Create the 95th percentile. +To compare product prices with customers traffic: -.. In the editor, click *Median of products.price*. +. From the *Available fields* list, drag and drop *products.price* to the visualization builder. -.. From *Select a function*, click *Percentile*. +.. In the *KQL* field, enter `response.keyword>=500 AND response.keyword<600`. -.. In the *Display name* field, enter `95th`, then click *Close*. +.. From *Select a function*, click *Average*. -. To create the 90th percentile, duplicate the `95th` percentile. +.. In the *Display name* field, enter `Average of prices`, then click *Close*. -.. Drag and drop *95th* to *Drop a field or click to add*. +. From the *Chart Type* dropdown, select *Area*. -.. Click *95th [1]*, then enter `90` in the *Percentile* field. +. Create a new layer to overlay with custom traffic. -.. In the *Display name* field enter `90th`, then click *Close*. +. In the editor, click *+*. + [role="screenshot"] -image::images/lens_advanced_3_1.gif[Easily duplicate the items with drag and drop] - -. Create the 50th percentile. - -.. Drag and drop *90th* to *Drop a field or click to add*. +image::images/lens_advanced_3_1.png[Add new layer button] -.. Click *90th [1]*, then enter `50` in the *Percentile* field. +. From the *Available fields* list, drag and drop *customer_id* to the *Vertical Axis* of the newly created layer. -.. In the *Display name* field enter `50th`, then click *Close*. +.. In the editor, click *Unique count of customer_id*. -. Create the 10th percentile. +.. In the *Display name* field, enter `Unique customers`, then click *Close*. -.. Drag and drop *50th* to *Drop a field or click to add*. +. In the *Series color* field, enter *#D36086*, then click *Close*. -.. Click *50th [1]*, then enter `10` in the *Percentile* field. +. For *Axis side*, click *Right*, then click *Close*. -.. In the *Display name* field enter `10th`, then click *Close*. +. From the *Available fields* list, drag and drop *order_date* to the *Horizontal Axis* of the newly created layer. -. To change the left axis label, open the *Left Axis* menu, then enter `Percentiles for product prices` in the *Axis name* field. +. From the new layer editor, click the *Chart type* dropdown, then click the line chart. + [role="screenshot"] -image::images/lens_advanced_3_1_1.png[Left Axis menu] -+ -You have a line chart that shows you the price distribution of products sold over time. -+ -[role="screenshot"] -image::images/lens_advanced_3_3.png[Percentiles for product prices chart] +image::images/lens_advanced_3_2.png[Change layer type] + +The visualization is done, but the legend uses a lot of space. Change the legend position to the top of the chart. + +. From the *Legend* dropdown, select the top position. . Click *Save and return*. [discrete] -[[add-the-response-code-filters-advanced]] -=== View the moving average of inventory prices +[[percentage-stacked-area]] +=== Compare the change in percentage over time -To view and analyze the prices of shoes, accessories, and clothing in the store inventory, create a line chart. +By default, *Lens* shows *date histograms* using a stacked chart visualization, which helps understand how distinct sets of documents perform over time. Sometimes it is useful to understand how the distributions of these sets change over time. +Combine *filters* and *date histogram* functions to see the change over time in specific +sets of documents. To view this as a percentage, use a *stacked percentage* bar or area chart. -. Open *Lens*. +To see sales change of product by type over time: -. From the *Chart Type* dropdown, select *Line*. +. From the *Available fields* list, drag and drop *Records* to the visualization builder. -. From the *Available fields* list, drag and drop *products.price* to the visualization builder. +. Click *Bar vertical stacked*, then select *Area percentage*. -. In the editor, click the *Drop a field or click to add* field for *Break down by*. +For each category type that you want to break down, create a filter. + +. In the editor, click the *Drop a field or click to add* field for *Break down by*. . From *Select a function*, click *Filters*. -. Add a filter for shoes. +. Add the filter for the clothing category. .. Click *All records*. -.. In the *KQL* field, enter `category.keyword : *Shoes*`. +.. In the *KQL* field, enter `category.keyword : *Clothing`. + +.. In the *Label* field, enter `Clothing`, then press Return. + +. Add the filter for the shoes category. + +.. Click *Add a filter*. + +.. In the *KQL* field, enter `category.keyword : *Shoes`. .. In the *Label* field, enter `Shoes`, then press Return. -. Add a filter for accessories. +. Add the filter for the accessories category. .. Click *Add a filter*. -.. In the *KQL* field, enter `category.keyword : *Accessories*`. +.. In the *KQL* field, enter `category.keyword : *Accessories`. .. In the *Label* field, enter `Accessories`, then press Return. -. Add a filter for clothing. +Change the legend position to the top of the chart. -.. Click *Add a filter*. +. From the *Legend* dropdown, select the top position. -.. In the *KQL* field, enter `category.keyword : *Clothing*`. ++ +[role="screenshot"] +image::images/lens_advanced_4_1.png[Prices share by category] -.. In the *Label* field, enter `Clothing`, then press Return. + Click *Save and return*. + +[discrete] +[[view-the-cumulative-number-of-products-sold-on-weekends]] +=== View the cumulative number of products sold on weekends + +To determine the number of orders made only on Saturday and Sunday, create an area chart, then add it to the dashboard. + +. Open *Lens*. + +. From the *Chart Type* dropdown, select *Area*. + +. Configure the cumulative sum of the store orders. + +.. From the *Available fields* list, drag and drop *Records* to the visualization builder. + +.. From the editor, click *Count of Records*. + +.. From *Select a function*, click *Cumulative sum*. + +.. In the *Display name* field, enter `Cumulative orders during weekend days`, then click *Close*. + +. Filter the results to display the data for only Saturday and Sunday. + +.. From the editor, click the *Drop a field or click to add* field for *Break down by*. + +.. From *Select a function*, click *Filters*. + +.. Click *All records*. + +.. In the *KQL* field, enter `day_of_week : "Saturday" or day_of_week : "Sunday"`, then press Return. ++ +The <> displays all documents where `day_of_week` matches `Saturday` or `Sunday`. ++ +[role="screenshot"] +image::images/lens_advanced_5_1.png[Filter aggregation to filter weekend days] + +. To hide the legend, open the *Legend* menu, then click *Hide*. ++ +[role="screenshot"] +image::images/lens_advanced_5_2_1.png[Legend menu] ++ +You have an area chart that shows you how many orders your store received during the weekend. -. Click *Close* +. Click *Bar vertical stacked*, then select *Area*. + [role="screenshot"] -image::images/lens_advanced_4_1.png[Median prices chart for different categories] +image::images/lens_advanced_5_2.png[Line chart with cumulative sum of orders made on the weekend] + +. Click *Save and return*. [discrete] -[[add-the-moving-average]] -==== Add the moving average +[[view-customers-over-time-by-continents]] +=== View table of customers by category over time + +Tables are an alternative type of visualization for time series, useful when you want to read the actual values. +You can build a date histogram table, and group the customer count metric by category, like the continent registered in their profile. + +In *Lens* you can split the metric in a table leveraging the *Columns* field, where each data value from the aggregation is used as column of the table and the relative metric value is shown. + +To build a date histogram table: + +. Open *Lens*. + +. From the *Chart Type* dropdown, select *Table*. -To focus on the general trends rather than on the peaks in the data, add the moving average, then add the visualization to the dashboard. +.. From the *Available fields* list, drag and drop *customer_id* to the *Metrics* field of the editor. -. In the editor, click the *Median of products.price*. +.. From the editor, click *Unique count of customer_id*. -. From *Select a function*, click *Moving average*. +.. In the *Display name* field, enter `Customers`, then click *Close*. -. In the *Window size* field, enter `7`, then click *Close*. +.. From the *Available fields* list, drag and drop *order_date* to the *Rows* field of the editor. + +.. From the editor *Rows*, click the *order_date* field just dropped. + +. Select *Customize time interval*. + +. Change the *Minimum interval* to `1 days`, then click *Close*. + +.. In the *Display name* field, enter `Sale`, then click *Close*. + +To split the customers count by continent: + +. From the *Available fields* list, drag and drop *geoip.continent_name* to the *Columns* field of the editor. + [role="screenshot"] -image::images/lens_advanced_4_2.png[Moving average prices chart for different categories] +image::images/lens_advanced_6_1.png[Table with daily customers by continent configuration] . Click *Save and return*. [discrete] === Save the dashboard +By default the dashboard attempts to match the palette across panels, but in this case there's no need for that, so it can be disabled. + +[role="screenshot"] +image::images/lens_advanced_7_1.png[Disable palette sync in dashboard] + Now that you have a complete overview of your ecommerce sales data, save the dashboard. . In the toolbar, click *Save*. diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_1.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_1.png deleted file mode 100644 index 1c752972791af..0000000000000 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_1.png and /dev/null differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2.png index de719c852dfde..ea58057433a19 100644 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2.png and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2.png differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2_1.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2_1.png index 9408cd736b27e..fa404d50b4dd1 100644 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2_1.png and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2_1.png differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_3.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_3.png index 4302c95eb8a25..b069acae43163 100644 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_3.png and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_3.png differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1.png deleted file mode 100644 index c08a865e52137..0000000000000 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1.png and /dev/null differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_1.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_1.png index 48712ec5ef224..e996b58520d41 100644 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_1.png and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_1.png differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_2.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_2.png new file mode 100644 index 0000000000000..a524dd1e456f6 Binary files /dev/null and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_2.png differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_4_3.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_4_3.png index b70910fdd2500..2cd75d2797a98 100644 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_4_3.png and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_4_3.png differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_6_2.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_6_2.png deleted file mode 100644 index e95b3d9a97b8d..0000000000000 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_6_2.png and /dev/null differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_7_1.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_7_1.png deleted file mode 100644 index e63f7e77f1e9f..0000000000000 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_7_1.png and /dev/null differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_dashboard.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_dashboard.png index 9bce65401f4c9..9e9a5ed3d758e 100644 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_dashboard.png and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_dashboard.png differ diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 58476bcae87df..94c9db1462760 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -75,6 +75,31 @@ Drag and drop the fields on to the visualization builder, then [role="screenshot"] image::images/lens_value_labels_xychart_toggle.png[Lens Bar chart value labels menu] +[float] +[[add-fields-in-lens]] +===== Add fields + +Add and define fields to the index pattern that you want to visualize using the {ref}/modules-scripting-painless.html[Painless scripting language]. +The fields that you are add are saved to the index pattern and appear in all visualizations, saved searches, and saved objects that use the index pattern. + +. Click *...*, then select *Add field to index pattern*. ++ +[role="screenshot"] +image:images/runtime-field-menu.png[Dropdown menu located next to index pattern field with items for adding and managing fields, width=50%] + +. Enter a *Name* for the field, then select the field *Type*. + +. Select *Set value*, then define the field value by emitting a single value using the {ref}/modules-scripting-painless.html[Painless scripting language]. + +. Click *Save*. ++ +To manage the field, click the field, then click *Edit index pattern field* or *Remove index pattern field*. ++ +[role="screenshot"] +image:images/manage-runtime-field.gif[Field menu to edit or remove field from index pattern, width=50%] + +For more information about adding fields to index patterns and Painless scripting language examples, refer to <>. + [float] [[drag-and-drop-keyboard-navigation]] ===== Create visualization panels with keyboard navigation diff --git a/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc index 22483b2801848..1375aa8d5934e 100644 --- a/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc +++ b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc @@ -1,10 +1,10 @@ [[create-a-dashboard-of-panels-with-web-server-data]] -== Tutorial: Create a dashboard of panels with web server data +== Build your first dashboard -You collected data from your web server, and you want to visualize and analyze the data on a dashboard. To create dashboard panels of the data, open the *Lens* visualization builder, then -create the visualization panels that best display the data. - -When you've completed the tutorial, you'll have a dashboard that provides you with a complete overview of your web server data. +Learn the most common ways to build a dashboard from your own data. +The tutorial will use sample data from the perspective of an analyst looking +at website logs, but this type of dashboard works on any type of data. +Before using this tutorial, you should be familiar with the <>. [role="screenshot"] image::images/lens_end_to_end_dashboard.png[Final dashboard vis] @@ -13,46 +13,78 @@ image::images/lens_end_to_end_dashboard.png[Final dashboard vis] [[add-the-data-and-create-the-dashboard]] === Add the data and create the dashboard -To create visualizations of the data from the web server, add the data set, then create the dashboard. +Install the sample web logs data that you'll use to create your dashboard. -. From the {kib} *Home* page, click *Try our sample data*. +. On the {kib} *Home* page, click *Try our sample data*. . From *Sample web logs*, click *Add data*. -. Open the main menu, click *Dashboard*. +Then create a new dashboard: + +. Open the main menu, then click *Dashboard*. . Click *Create dashboard*. +. Set the <> to *Last 90 days*. + [float] [[open-and-set-up-lens]] -=== Open and set up Lens +=== Open Lens and get familiar with the data -With *Lens*, you identify the data fields you want to visualize, drag and drop the fields, then watch as -*Lens* uses heuristics to apply the fields and create a visualization for you. +. On the dashboard, click *Create visualization*. -. From the dashboard, click *Create panel*. - -. On the *New visualization* window, click *Lens*. +. Make sure the *kibana_sample_data_logs* index appears. You might need to select +a different index pattern from the dropdown: + [role="screenshot"] -image::images/lens_end_to_end_1_1.png[New visualization popover] +image::images/lens_end_to_end_1_2.png[Lens index pattern selector, width=50%] + +This tutorial uses `Records`, timestamp`, `bytes`, `clientip`, and `referer.keyword`. +To see the most frequent values of a field, click the field name to view a summary. + +The main elements of *Lens* are named: -. Make sure the *kibana_sample_data_logs* index appears. +Workspace panel:: Displays your visualization. You can drag and drop onto this area. +Dimensions:: Each dimension is a function with extra options. Dimensions are grouped +for each visualization type, for example the *Vertical axis* is a group that allows +multiple dimensions. Each dimension starts empty with the label *Drop a field or click to add*. +Functions:: There are two main types of functions: *buckets* and *metrics*, which are +equivalent to what {es} provides. [discrete] [[view-the-number-of-website-visitors]] -=== View the number of website visitors +=== Create your first visualization -To determine how many users have visited your website within the last 90 days, create a metric visualization, then add it to the dashboard. +Every time you build a visualization in *Lens*, you need to: -. Set the <> to *Last 90 days*. +* *Choose your visualization.* Do you know the type of visualization you'd like to use? +If you do, select the type before dragging any fields. If you don't, you can change the +visualization type after configuring your functions. + +* *Choose your field.* Do you know the dimension group you want to use the field in? If you do, +drag and drop the field from the field list to your chosen dimension and Lens will pick a function for you. +If you don't, drag and drop the field onto the workspace panel. Skip this step if you are +using the *Filters* function. + +* *Edit and delete.* To change the function or styling options, click the dimension to open +the configuration panel. To delete a specific dimension, close the configuration panel and click +the delete button. To reset the entire visualization, click *Reset layer*. + +To put this into practice, pick a field you want to analyze, such as `clientip`. If you want +to analyze only this field, you can use *Metric* to show a big number. +The only number function that you can use with `clientip` is *Unique count*. +*Unique count*, also known as cardinality, approximates the number of unique values +of the `clientip` field. -. From the *Chart Type* dropdown, select *Metric*. +. To select the visualization type, click *Bar vertical stacked* to open the chart type dropdown, then select *Metric*. + [role="screenshot"] -image::images/lens_end_to_end_1_2_1.png[Chart Type dropdown with Metric selected] +image::images/lens_end_to_end_1_2_1.png[Chart Type dropdown with Metric selected, width=50%] -. From the *Available fields* list, drag and drop *clientip* to the visualization builder. +. From the *Available fields* list, drag and drop `clientip` to the workspace panel. +Lens selects *Unique count* because it is the only numeric function +that works for IP addresses. You can also drag and drop `clientip` onto +the empty dimension for the same result. + [role="screenshot"] image::images/lens_end_to_end_1_3.png[Changed type and dropped clientip field] @@ -68,294 +100,220 @@ image::images/lens_end_to_end_1_4.png[Flyout config open] . Click *Save and return*. -[discrete] -[[view-the-distribution-of-visitors-by-operating-system]] -=== View the distribution of visitors by operating system - -To determine the operating systems you should continue to support, and the importance of mobile traffic from iOS devices, -create a donut chart that displays the top operating systems that your visitors used to access your website within the last 90 days. +. Customize the newly added panel: -. Open *Lens*, then set the <> to *Last 90 days*. +.. Drag the bottom corner of the panel until the metric takes up one quarter of the screen +width. The row for the metric will have 4 items on it later. -. From the *Chart Type* dropdown, select *Donut*. +.. The metric visualization has its own label, so you do not need to add a panel title. -. From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. +. Click *Save* on the dashboard menu -.. Drag and drop *clientip* to the *Size by* field. +. In the *Title* field, enter `Logs dashboard`. -.. Drag and drop *machine.os.keyword* to the *Slice by* field. -+ -[role="screenshot"] -image::images/lens_end_to_end_2_1_1.png[Donut chart with clientip and machine.os.keyword fields] - -. Change the color palette. - -.. In the editor, click *Top values of machine.os.keyword*. - -.. From the *Color palette* dropdown, select *Compatibility*. - -.. Click *Close*. -+ -[role="screenshot"] -image::images/lens_end_to_end_2_1.png[Donut chart with open config panel] +. Select *Store time with dashboard* box, then click *Save*. -. Click *Save and return*. +. After the dashboard refreshes, click *Edit* again. [discrete] [[mixed-multiaxis]] -=== View the average of bytes transfer per day +=== View a metric over time -To prevent potential server failures, and optimize the cost of website maintenance, create an area chart that displays the average of bytes transfer. To compare -the data to the number of visitors to your website, add a line chart layer. +*Lens* has two shortcuts that simplify viewing metrics over time. +If you drag and drop a numeric field to the workspace panel, *Lens* adds the default +time field from the index pattern. If the *Date histogram* function is being used, +quickly replace the time field by dragging and dropping on the workspace panel. -. Open *Lens*. +To visualize the `bytes` field over time without choosing a visualization type or function: -. From the *Available fields* list, drag and drop *bytes* to the visualization builder. +. From the *Available fields* list, drag and drop `bytes` onto the workspace panel to have *Lens* automatically +create a chart. *Lens* creates a bar chart with two dimensions, *timestamp* and *Median of bytes*. + +. *Lens* automatically chooses a date interval. To zoom in on the data you want to view, +click and drag your cursor across the bars. -. To zoom in on the data you want to view, click and drag your cursor across the bars. -+ [role="screenshot"] image::images/lens_end_to_end_3_1_1.gif[Zoom in on the data] -. Change the *timestamp* interval. - -.. In the editor, click *timestamp*. +To emphasize the change in *Median of bytes* over time, use a line chart. +To change the visualization type, use one of the following ways: -.. Select *Customize time interval*. +* From the *Suggestions*, click the line chart. +* Click *Bar vertical stacked*, then select *Line*. +* Click the chart type icon above *Horizontal axis*, then click the line icon. -.. Change the *Minimum interval* to `1 days`, then click *Close*. -+ -[role="screenshot"] -image::images/lens_end_to_end_3_1.png[Customize time interval] +Most users use the automatic time interval. You can increase and decrease +the minimum interval that *Lens* uses, but you cannot decrease the interval +below the {kib} advanced settings. To set the minimum time interval: -. From the *Chart Type* dropdown, select *Area*. +. In the editor, click *timestamp*. -[discrete] -[[add-a-data-layer]] -==== Add the line chart layer +. Click *How it works* to learn about the *Lens* minimum interval -To compare the average of bytes transfer to the number of users that visit your website, add a line chart layer. +. Select *Customize time interval*. -. In the editor, click *+*. +. Increase the *Minimum interval* to `1 days`, then click *Close*. + [role="screenshot"] -image::images/lens_end_to_end_3_2.png[Add new layer button] +image::images/lens_end_to_end_3_1.png[Customize time interval] -. From the new layer editor, click the *Chart type* dropdown, then click the line chart. +To save space on the dashboard, so to save space, hide the vertical and horizontal +axis labels. + +. Open the *Left axis* menu, then deselect *Show*. + [role="screenshot"] -image::images/lens_end_to_end_3_3.png[Change layer type] -+ -The chart type for the visualization changes to *Mixed XY*. - -. From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. - -.. Drag and drop *timestamp* to the *Horizontal axis* field. - -.. Drag and drop *clientip* to the *Vertical axis* field. - -. Change the *timestamp* interval. +image::images/lens_end_to_end_4_3.png[Turn off axis name] -.. In the editor, click *timestamp* in the line chart layer. +. Open the *Bottom axis* menu, then deselect *Show*. -.. Select *Customize time interval*. +. Click *Save and return* -.. Change the *Minimum interval* to `1 days`, then click *Close*. +. On the dashboard, move the panel so that it is in the same row as the *Metric* visualization panel. The two should +take up half the screen width. -. Change the *Unique count of clientip* label and color. +. Add a panel title to explain the panel, which is necessary because you removed the axis labels. -.. In the editor, click *Unique count of clientip*. +.. Open the panel menu and choose *Edit panel title*. -.. In the *Display name* field, enter `Unique visitors` in the line chart layer. +.. In the *Title* field, enter `Median of bytes`, then click *Save*. -.. In the *Series color* field, enter *#CA8EAE*, then click *Close*. +. In the toolbar, click *Save*. [discrete] -[[configure-the-multiaxis-chart]] -==== Configure the y-axes - -There is a significant difference between the *timestamp per day* and *Unique visitors* data, which makes the *Unique visitors* data difficult to read. To improve the readability, -display the *Unique visitors* data along a second y-axis, then change the formatting. When functions contain multiple formats, separate axes are created by default. +[[view-the-distribution-of-visitors-by-operating-system]] +=== View the top values of a field -. In the editor, click *Unique visitors* in the line chart layer. +The *Top values* function ranks the unique values of a field by another dimension. +The values are the most frequent when ranked by a *Count* dimension. +The values are the largest when ranked by a *Sum* dimension. -. For *Axis side*, click *Right*, then click *Close*. +When you drag and drop a text or IP address field onto the workspace panel, +*Lens* adds a *Top values* function ranked by *Count of records* to show the most frequent values. -[float] -[[change-the-visualization-type]] -==== Change the visualization type +For this tutorial, you have picked a field and function, but not a visualization type. +You want to see the most frequent values of `request.keyword` on your website, ranked by the unique visitors. +This means that you want to use *Top values of request.keyword* ranked by *Unique count of clientip*, instead of +being ranked by *Count of records*. -. In the editor, click *Average of bytes* in the area chart layer. +. From the *Available fields* list, drag and drop `clientip` onto the *Vertical axis*. +*Lens* chooses the function for you when you drop onto a dimension, which is *Unique count* here. +Do not drop the field into the main workspace because `clientip` will be added to the wrong axis. -. From the *Value format* dropdown, select *Bytes (1024)*, then click *Close*. +. Drag and drop `request.keyword` to the main workspace. *Lens* adds *Top values of request.keyword* +to the *Horizontal axis*. + [role="screenshot"] -image::images/lens_end_to_end_3_4.png[Multiaxis chart] - -[discrete] -[[lens-legend-position]] -==== Change the legend position +image::images/lens_end_to_end_2_1_1.png[Vertical bar chart with top values of request.keyword by most unique visitors] -The visualization is done, but the legend uses a lot of space. Change the legend position to the top of the chart. +This chart is hard to read because the `request.keyword` field contains long text. You could try +using one of the *Suggestions*, but the suggestions also have issues with long text. Instead, switch +to the *Table* visualization. -. From the *Legend* dropdown, select the top position. +Click *Bar vertical stacked*, then select *Table*. + [role="screenshot"] -image::images/lens_end_to_end_3_5.png[legend position] - -. Click *Save and return*. - -[discrete] -[[percentage-stacked-area]] -=== View the health of your website +image::images/lens_end_to_end_2_1_2.png[Table with top values of request.keyword by most unique visitors] -To detect unusual traffic, bad website links, and server errors, create a percentage stacked area chart that displays the associated response codes. +Next, customize the table. -. Open *Lens*. +. Click the *Top values of request.keyword* dimension. -. From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. +.. Increase the *Number of values*. The maximum allowed value is 1000. -.. Drag and drop *Records* to the *Vertical axis* field. +.. In the *Display name* field, enter `Page URL`, then click *Close*. -.. Drag and drop *@timestamp* to the *Horizontal axis* field. +. Click *Save and return*. -. From the *Chart Type* dropdown, select *Percentage bar*. +. Move the table panel so that it has its own row, but do not change the size. -. To remove the vertical axis label, click *Left axis*, then deselect *Show*. -+ -[role="screenshot"] -image::images/lens_end_to_end_4_3.png[Turn off axis name] +NOTE: You do not need a panel title because the table columns are clearly labeled. [discrete] -[[add-the-response-code-filters]] -==== Add the response code filters - -For each response code that you want to display, create a filter. - -. In the editor, click the *Drop a field or click to add* field for *Break down by*. - -. From *Select a function*, click *Filters*. - -. Add the filter for the successful response codes. +[[custom-ranges]] +=== Compare a subset of documents to all documents -.. Click *All records*. +To compare a field on subset of documents to all documents, you need to select two or more sets of documents that add up to 100%. +For this example, we are comparing documents where the `bytes` field is under 10 Kb to documents where `bytes` is over 10 Kb, +which are two sets that do not overlap. -.. In the *KQL* field, enter `response.keyword>=200 AND response.keyword<300`. +Use *Intervals* to select documents based on the number range of a field. Use *Filters* when your criteria +is not numeric, or when your query needs multiple clauses. -.. In the *Label* field, enter `2XX`, then press Return. -+ -[role="screenshot"] -image::images/lens_end_to_end_4_1.png[First filter in filters aggregation] +Use a proportion chart to display the values as a percentage of the sum of all values. Lens has 5 types of proportion charts: +pie, donut, treemap, percentage bar and percentage area. -. Add the filter for the redirect codes. +To determine if your users transfer more `bytes` from small files versus large files, +configure dimensions with *Intervals* and *Sum*, then switch to a pie chart to display as a percentage: -.. Click *Add a filter*. +. From the *Available fields* list, drag and drop `bytes` to *Vertical axis* in the editor. -.. In the *KQL* field, enter `response.keyword>=300 AND response.keyword<400`. +. Click *Median of bytes*, select *Sum*, then click *Close*. -.. In the *Label* field, enter `3XX`, then press Return. +. From the *Available fields* list, drag and drop `bytes` to *Break down by* in the editor, then specify the file size ranges. -. Add the filter for the client error codes. +.. In the editor, click *bytes*. -.. Click *Add a filter*. +.. Click *Create custom ranges*, enter the following, then press Return: -.. In the *KQL* field, enter `response.keyword>=400 AND response.keyword<500`. +* *Ranges* — `0` -> `10240` -.. In the *Label* field, enter `4XX`, then press Return. +* *Label* — `Below 10KB` -. Add the filter for the server error codes. +.. Click *Add range*, enter the following, then press Return: -.. Click *Add a filter*. +* *Ranges* — `10240` -> `+∞` -.. In the *KQL* field, enter `response.keyword>=500 AND response.keyword<600`. +* *Label* — `Above 10KB` ++ +[role="screenshot"] +image::images/lens_end_to_end_6_1.png[Custom ranges configuration] -.. In the *Label* field, enter `5XX`, then press Return. +.. From the *Value format* dropdown, select *Bytes (1024)*, then click *Close*. -. To change the color palette, select *Status* from the *Color palette* dropdown, then click *Close*. +. From the *Chart Type* dropdown, select *Pie*. . Click *Save and return*. [discrete] [[histogram]] -=== View the traffic for your website by the hour - -To find the best time to shut down your website for maintenance, create a histogram that displays the traffic for your website by the hour. - -. Open *Lens*. - -. From the *Available fields* list, drag and drop *bytes* to *Vertical axis* in the editor, then configure the options. +=== View a the distribution of a number field -.. Click *Average of bytes*. +Knowing the distribution of a number helps to find patterns. For example, you could +look at the website traffic per hour to find the best time to do routine maintenance. +Use *Intervals* to see an evenly spaced distribution of a number field. -.. From *Select a function*, click *Sum*. +. From the *Available fields* list, drag and drop `bytes` to *Vertical axis* in the editor. -.. In the *Display name* field, enter `Transferred bytes`. +. Click *Median of bytes*, then select *Sum*. -.. From the *Value format* dropdown, select `Bytes (1024)`, then click *Close*. +. In the *Display name* field, enter `Transferred bytes`. -. From the *Available fields* list, drag and drop *hour_of_day* to *Horizontal axis* in the editor, then configure the options. +. From the *Value format* dropdown, select `Bytes (1024)`, then click *Close*. -.. Click *hour_of_day*. +. From the *Available fields* list, drag and drop *hour_of_day* to *Horizontal axis* in the editor. -.. Click and slide the *Intervals granularity* slider until the horizontal axis displays hourly intervals. +. Click *hour_of_day*, and then slide the *Intervals granularity* slider until the horizontal axis displays hourly intervals. + [role="screenshot"] image::images/lens_end_to_end_5_2.png[Create custom ranges] . Click *Save and return*. -[discrete] -[[custom-ranges]] -=== View the percent of small versus large transferred files - -To determine if your users transfer more small files versus large files, create a pie chart that displays the percentage of each size. - -. Open *Lens*. - -. From the *Available fields* list, drag and drop *bytes* to *Vertical axis* in the editor, then configure the options. - -.. Click *Average of bytes*. - -.. From *Select a function*, click *Sum*, then click *Close*. - -. From the *Available fields* list, drag and drop *bytes* to *Break down by* in the editor, then specify the file size ranges. - -.. Click *bytes*. - -.. Click *Create custom ranges*, enter the following, then press Return: - -* *Ranges* — `0` -> `10240` - -* *Label* — `Below 10KB` - -.. Click *Add range*, enter the following, then press Return: - -* *Ranges* — `10240` -> `+∞` - -* *Label* — `Above 10KB` -+ -[role="screenshot"] -image::images/lens_end_to_end_6_1.png[Custom ranges configuration] - -.. From the *Value format* dropdown, select *Bytes (1024)*, then click *Close*. +. Decrease the panel size, then drag and drop it to the first row next to the `Median of bytes` panel. There +should be four panels in a row. -. From the *Chart Type* dropdown, select *Pie*. -+ -[role="screenshot"] -image::images/lens_end_to_end_6_2.png[Files size distribution] - -. Click *Save and return*. +. You do not need a panel title because the axis labels are self-explanatory. [discrete] [[treemap]] -=== View the top sources of website traffic - -To determine how users find out about your website and where your users are located, create a treemap that displays the percentage of users that -enter your website from specific social media websites, and the top countries where users are located. +=== Create a multi-level chart -. Open *Lens*. +*Lens* lets you use multiple functions in the data table and proportion charts. For example, +to create a chart that breaks down the traffic sources and user geography, use *Filters* and +*Top values*. -. From the *Chart Type* dropdown, select *Treemap*. +. Click *Bar vertical stacked*, then select *Treemap*. . From the *Available fields* list, drag and drop *Records* to the *Size by* field in the editor. @@ -377,21 +335,15 @@ enter your website from specific social media websites, and the top countries wh .. Click *Add a filter*, enter the following, then press Return: -* *KQL* — `NOT referer : *twitter* OR NOT referer: *facebook.com*` +* *KQL* — `NOT referer : *twitter.com* OR NOT referer: *facebook.com*` * *Label* — `Other` .. Click *Close*. -[discrete] -[[add-the-countries]] -==== Add the geographic data - -To determine the top countries where users are located, add the geographic data. +Add the next break down by geography: -Compare the top sources of website traffic data to the top three countries. - -. From the *Available fields* list, drag and drop *geo.src* to the visualization builder. +. From the *Available fields* list, drag and drop *geo.src* to the main workspace. . To change the *Group by* order, click and drag *Top values of geo.src* so that it appears first in the editor. + @@ -409,6 +361,12 @@ image::images/lens_end_to_end_7_3.png[Group other values as Other] . Click *Save and return*. +. Arrange the panel so that it is in the same row as the table. + +.. Click the gear icon and choose *Edit panel title*. + +.. Enter "Page views by location and referer" as the panel title, then click *Save*. + [discrete] === Save the dashboard @@ -417,3 +375,5 @@ Now that you have a complete overview of your web server data, save the dashboar . In the toolbar, click *Save*. . On the *Save dashboard* window, enter `Web server data`, then click *Save*. + +. If this was not the first time you saved the dashboard, click *Switch to view mode* diff --git a/docs/user/discover.asciidoc b/docs/user/discover.asciidoc index 6e3a7f697073d..ea413747a2aad 100644 --- a/docs/user/discover.asciidoc +++ b/docs/user/discover.asciidoc @@ -6,7 +6,7 @@ **_Gain insight to your data._** *Discover* enables you to quickly search and filter your data, get information -about structure of the fields, and visualize your data with *Lens* and *Maps*. +about the structure of the fields, and visualize your data with *Lens* and *Maps*. You can customize and save your searches and place them on a dashboard. ++++ @@ -110,6 +110,43 @@ image:images/document-table.png[Document table with fields for manufacturer, geo . To rearrange the table columns, hover the mouse over a column header, and then use the move and sort controls. +[float] +[[add-field-in-discover]] +=== Add a field + +What happens if you forgot to define an important value as a separate field? Or, what if you +want to combine two fields and treat them as one? +You can add a field to your index pattern from inside of **Discover**, +and then use that field for analysis and visualizations, +the same way you do with other fields. + +. Click the ellipsis icon (...), and then click *Add field to index pattern*. ++ +[role="screenshot"] +image:images/add-field-to-pattern.png[Dropdown menu located next to index pattern field with item for adding a field to an index pattern, width=50%] + +. In the *Create field* form, enter `hello` for the name. + +. Turn on *Set value*. + +. Use the Painless scripting language to define the field: ++ +```ts +emit("Hello World!"); +``` + +. Click *Save*. + +. In the fields list, search for the *hello* field, and then click it. ++ +You'll see the top values for the field. The pop-up also includes actions for filtering, +editing, and deleting the field. ++ +[role="screenshot"] +image:images/hello-field.png[Top values for the hello field, width=50%] + +For more information on adding fields and Painless scripting language examples, refer to <>. + [float] [[search-in-discover]] @@ -186,7 +223,8 @@ You can bookmark this document and share the link. === Save your search for later use Save your search so you can repeat it later, generate a CSV report, or use it in visualizations, dashboards, and Canvas workpads. -Saving a search saves the query and the filters. +Saving a search saves the query text, filters, +and current view of *Discover*—the columns selected in the document table, the sort order, and the index pattern. . In the toolbar, click **Save**. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 83e18734f65d4..397ab1717183b 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -131,8 +131,8 @@ Kerberos, PKI, OIDC, and SAML. [cols="50, 50"] |=== -a| <> -|Create and manage the index patterns that retrieve your data from {es}. +a| <> +|Manage the data fields in the index patterns that retrieve your data from {es}. | <> | Copy, edit, delete, import, and export your saved objects. @@ -186,10 +186,10 @@ include::{kib-repo-dir}/management/managing-beats.asciidoc[] include::{kib-repo-dir}/management/action-types.asciidoc[] -include::{kib-repo-dir}/management/managing-fields.asciidoc[] - include::{kib-repo-dir}/management/managing-licenses.asciidoc[] +include::{kib-repo-dir}/management/manage-index-patterns.asciidoc[] + include::{kib-repo-dir}/management/numeral.asciidoc[] include::{kib-repo-dir}/management/rollups/create_and_manage_rollups.asciidoc[] diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index c43e9210dd7c8..4305b39653f8d 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -16,6 +16,7 @@ Having trouble? Here are solutions to common problems you might encounter while * <> * <> * <> +* <> [float] [[reporting-diagnostics]] @@ -163,3 +164,12 @@ In this case, try increasing the memory for the {kib} instance to 2GB. === ARM systems Chromium is not compatible with ARM RHEL/CentOS. + +[float] +[[reporting-troubleshooting-maps-ems]] +=== Unable to connect to Elastic Maps Service + +https://www.elastic.co/elastic-maps-service[{ems} ({ems-init})] is a service that hosts +tile layers and vector shapes of administrative boundaries. +If a report contains a map with a missing basemap layer or administrative boundary, the {kib} server does not have access to {ems-init}. +See <> for information on how to connect your {kib} server to {ems-init}. diff --git a/package.json b/package.json index aa8682d821bc2..b79724dbb63bc 100644 --- a/package.json +++ b/package.json @@ -136,8 +136,12 @@ "@kbn/logging": "link:bazel-bin/packages/kbn-logging/npm_module", "@kbn/monaco": "link:packages/kbn-monaco", "@kbn/securitysolution-constants": "link:bazel-bin/packages/kbn-securitysolution-constants/npm_module", - "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module", + "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module", + "@kbn/securitysolution-io-ts-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-types/npm_module", + "@kbn/securitysolution-io-ts-alerting-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types/npm_module", + "@kbn/securitysolution-io-ts-list-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types/npm_module", "@kbn/securitysolution-io-ts-utils": "link:bazel-bin/packages/kbn-securitysolution-io-ts-utils/npm_module", + "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module", "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", "@kbn/server-route-repository": "link:packages/kbn-server-route-repository", "@kbn/std": "link:bazel-bin/packages/kbn-std/npm_module", @@ -264,6 +268,7 @@ "json-stringify-safe": "5.0.1", "jsonwebtoken": "^8.5.1", "jsts": "^1.6.2", + "@kbn/rule-data-utils": "link:packages/kbn-rule-data-utils", "kea": "^2.3.0", "leaflet": "1.5.1", "leaflet-draw": "0.4.14", @@ -431,7 +436,7 @@ "@babel/traverse": "^7.12.12", "@babel/types": "^7.12.12", "@bazel/ibazel": "^0.15.10", - "@bazel/typescript": "^3.4.2", + "@bazel/typescript": "^3.5.0", "@cypress/snapshot": "^2.1.7", "@cypress/webpack-preprocessor": "^5.6.0", "@elastic/apm-rum": "^5.6.1", @@ -446,7 +451,7 @@ "@kbn/babel-preset": "link:bazel-bin/packages/kbn-babel-preset/npm_module", "@kbn/cli-dev-mode": "link:packages/kbn-cli-dev-mode", "@kbn/dev-utils": "link:bazel-bin/packages/kbn-dev-utils/npm_module", - "@kbn/docs-utils": "link:packages/kbn-docs-utils", + "@kbn/docs-utils": "link:bazel-bin/packages/kbn-docs-utils/npm_module", "@kbn/es": "link:bazel-bin/packages/kbn-es/npm_module", "@kbn/es-archiver": "link:packages/kbn-es-archiver", "@kbn/eslint-import-resolver-kibana": "link:bazel-bin/packages/kbn-eslint-import-resolver-kibana/npm_module", @@ -457,7 +462,7 @@ "@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers", "@kbn/pm": "link:packages/kbn-pm", "@kbn/storybook": "link:packages/kbn-storybook", - "@kbn/telemetry-tools": "link:packages/kbn-telemetry-tools", + "@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools/npm_module", "@kbn/test": "link:packages/kbn-test", "@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector", "@loaders.gl/polyfills": "^2.3.5", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index e33b3d85b44ef..a9c87043575fa 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -16,6 +16,7 @@ filegroup( "//packages/kbn-config-schema:build", "//packages/kbn-crypto:build", "//packages/kbn-dev-utils:build", + "//packages/kbn-docs-utils:build", "//packages/kbn-es:build", "//packages/kbn-eslint-import-resolver-kibana:build", "//packages/kbn-eslint-plugin-eslint:build", @@ -24,9 +25,14 @@ filegroup( "//packages/kbn-logging:build", "//packages/kbn-plugin-generator:build", "//packages/kbn-securitysolution-constants:build", + "//packages/kbn-securitysolution-io-ts-types:build", + "//packages/kbn-securitysolution-io-ts-alerting-types:build", + "//packages/kbn-securitysolution-io-ts-list-types:build", "//packages/kbn-securitysolution-io-ts-utils:build", "//packages/kbn-securitysolution-utils:build", + "//packages/kbn-securitysolution-es-utils:build", "//packages/kbn-std:build", + "//packages/kbn-telemetry-tools:build", "//packages/kbn-tinymath:build", "//packages/kbn-utility-types:build", "//packages/kbn-utils:build", diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index 8f55f0e0f06a7..14e292c056db6 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -2,7 +2,7 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -PKG_BASE_NAME = "kbn-cypto" +PKG_BASE_NAME = "kbn-crypto" PKG_REQUIRE_NAME = "@kbn/crypto" SOURCE_FILES = glob( diff --git a/packages/kbn-docs-utils/BUILD.bazel b/packages/kbn-docs-utils/BUILD.bazel new file mode 100644 index 0000000000000..e72d83851f5d2 --- /dev/null +++ b/packages/kbn-docs-utils/BUILD.bazel @@ -0,0 +1,88 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-docs-utils" +PKG_REQUIRE_NAME = "@kbn/docs-utils" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__/**", + "**/snapshots/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +SRC_DEPS = [ + "//packages/kbn-config", + "//packages/kbn-dev-utils", + "//packages/kbn-utils", + "@npm//dedent", + "@npm//ts-morph", +] + +TYPES_DEPS = [ + "@npm//@types/dedent", + "@npm//@types/jest", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-docs-utils/package.json b/packages/kbn-docs-utils/package.json index 27d38d2d8ed4f..b2a52b2d1f78e 100644 --- a/packages/kbn-docs-utils/package.json +++ b/packages/kbn-docs-utils/package.json @@ -7,9 +7,5 @@ "types": "target/index.d.ts", "kibana": { "devOnly": true - }, - "scripts": { - "kbn:bootstrap": "../../node_modules/.bin/tsc", - "kbn:watch": "../../node_modules/.bin/tsc --watch" } } \ No newline at end of file diff --git a/packages/kbn-docs-utils/tsconfig.json b/packages/kbn-docs-utils/tsconfig.json index 6f4a6fa2af8a5..9868c8b3d2bb4 100644 --- a/packages/kbn-docs-utils/tsconfig.json +++ b/packages/kbn-docs-utils/tsconfig.json @@ -1,11 +1,12 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "target": "ES2019", "declaration": true, "declarationMap": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-docs-utils/src", "types": [ diff --git a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts index fbef255cd9ee5..51d4f28d20f2e 100644 --- a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts @@ -165,6 +165,7 @@ export async function createDefaultSpace({ { index, id: 'space:default', + refresh: 'wait_for', body: { type: 'space', updated_at: new Date().toISOString(), diff --git a/packages/kbn-legacy-logging/src/rotate/log_rotator.ts b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts index 4d57d869b9008..4b1e34839030f 100644 --- a/packages/kbn-legacy-logging/src/rotate/log_rotator.ts +++ b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts @@ -149,7 +149,7 @@ export class LogRotator { if (this.usePolling && !this.shouldUsePolling) { this.log( ['warning', 'logging:rotate'], - 'Looks like your current environment support a faster algorithm then polling. You can try to disable `usePolling`' + 'Looks like your current environment support a faster algorithm than polling. You can try to disable `usePolling`' ); } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 08e90ed829d4a..5748984c7bc6e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -46,7 +46,7 @@ pageLoadAssetSize: lens: 96624 licenseManagement: 41817 licensing: 29004 - lists: 280504 + lists: 200000 logstash: 53548 management: 46112 maps: 80000 @@ -61,7 +61,6 @@ pageLoadAssetSize: remoteClusters: 51327 reporting: 183418 rollup: 97204 - ruleRegistry: 100000 savedObjects: 108518 savedObjectsManagement: 101836 savedObjectsTagging: 59482 diff --git a/packages/kbn-telemetry-tools/babel.config.js b/packages/kbn-rule-data-utils/jest.config.js similarity index 79% rename from packages/kbn-telemetry-tools/babel.config.js rename to packages/kbn-rule-data-utils/jest.config.js index 68c67e549b74b..26cb39fe8b55a 100644 --- a/packages/kbn-telemetry-tools/babel.config.js +++ b/packages/kbn-rule-data-utils/jest.config.js @@ -7,6 +7,7 @@ */ module.exports = { - presets: ['@kbn/babel-preset/node_preset'], - ignore: ['**/*.test.ts', '**/__fixture__/**'], + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-rule-data-utils'], }; diff --git a/packages/kbn-rule-data-utils/package.json b/packages/kbn-rule-data-utils/package.json new file mode 100644 index 0000000000000..6f0b8439ec891 --- /dev/null +++ b/packages/kbn-rule-data-utils/package.json @@ -0,0 +1,13 @@ +{ + "name": "@kbn/rule-data-utils", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true, + "scripts": { + "build": "../../node_modules/.bin/tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + } +} diff --git a/packages/kbn-rule-data-utils/src/index.ts b/packages/kbn-rule-data-utils/src/index.ts new file mode 100644 index 0000000000000..93a2538c7aa2c --- /dev/null +++ b/packages/kbn-rule-data-utils/src/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './technical_field_names'; diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts new file mode 100644 index 0000000000000..31779c9f08e81 --- /dev/null +++ b/packages/kbn-rule-data-utils/src/technical_field_names.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 { ValuesType } from 'utility-types'; + +const ALERT_NAMESPACE = 'kibana.rac.alert'; + +const TIMESTAMP = '@timestamp' as const; +const EVENT_KIND = 'event.kind' as const; +const EVENT_ACTION = 'event.action' as const; +const RULE_UUID = 'rule.uuid' as const; +const RULE_ID = 'rule.id' as const; +const RULE_NAME = 'rule.name' as const; +const RULE_CATEGORY = 'rule.category' as const; +const TAGS = 'tags' as const; +const PRODUCER = `${ALERT_NAMESPACE}.producer` as const; +const ALERT_ID = `${ALERT_NAMESPACE}.id` as const; +const ALERT_UUID = `${ALERT_NAMESPACE}.uuid` as const; +const ALERT_START = `${ALERT_NAMESPACE}.start` as const; +const ALERT_END = `${ALERT_NAMESPACE}.end` as const; +const ALERT_DURATION = `${ALERT_NAMESPACE}.duration.us` as const; +const ALERT_SEVERITY_LEVEL = `${ALERT_NAMESPACE}.severity.level` as const; +const ALERT_SEVERITY_VALUE = `${ALERT_NAMESPACE}.severity.value` as const; +const ALERT_STATUS = `${ALERT_NAMESPACE}.status` as const; +const ALERT_EVALUATION_THRESHOLD = `${ALERT_NAMESPACE}.evaluation.threshold` as const; +const ALERT_EVALUATION_VALUE = `${ALERT_NAMESPACE}.evaluation.value` as const; + +const fields = { + TIMESTAMP, + EVENT_KIND, + EVENT_ACTION, + RULE_UUID, + RULE_ID, + RULE_NAME, + RULE_CATEGORY, + TAGS, + PRODUCER, + ALERT_ID, + ALERT_UUID, + ALERT_START, + ALERT_END, + ALERT_DURATION, + ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY_VALUE, + ALERT_STATUS, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, +}; + +export { + TIMESTAMP, + EVENT_KIND, + EVENT_ACTION, + RULE_UUID, + RULE_ID, + RULE_NAME, + RULE_CATEGORY, + TAGS, + PRODUCER, + ALERT_ID, + ALERT_UUID, + ALERT_START, + ALERT_END, + ALERT_DURATION, + ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY_VALUE, + ALERT_STATUS, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, +}; + +export type TechnicalRuleDataFieldName = ValuesType; diff --git a/packages/kbn-rule-data-utils/tsconfig.json b/packages/kbn-rule-data-utils/tsconfig.json new file mode 100644 index 0000000000000..4b1262d11f3af --- /dev/null +++ b/packages/kbn-rule-data-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": false, + "outDir": "./target", + "stripInternal": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-rule-data-utils/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/packages/kbn-securitysolution-es-utils/BUILD.bazel b/packages/kbn-securitysolution-es-utils/BUILD.bazel new file mode 100644 index 0000000000000..0cc27358c5da2 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/BUILD.bazel @@ -0,0 +1,86 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-es-utils" + +PKG_REQUIRE_NAME = "@kbn/securitysolution-es-utils" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "@npm//@elastic/elasticsearch", + "@npm//@hapi/hapi", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + srcs = SRCS, + args = ["--pretty"], + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + root_dir = "src", + source_map = True, + tsconfig = ":tsconfig", + deps = DEPS, +) + +js_library( + name = PKG_BASE_NAME, + package_name = PKG_REQUIRE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + visibility = ["//visibility:public"], + deps = [":tsc"] + DEPS, +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ], +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-es-utils/README.md b/packages/kbn-securitysolution-es-utils/README.md new file mode 100644 index 0000000000000..b99aa095c84f4 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/README.md @@ -0,0 +1,6 @@ +# kbn-securitysolution-es-utils + +This is the shared security solution elastic search utilities among plugins. This was originally created +to remove the dependencies between security_solution and other projects such as lists. This should only be +used within server side code and not client side code since it is all elastic search utilities and packages. + diff --git a/packages/kbn-securitysolution-es-utils/jest.config.js b/packages/kbn-securitysolution-es-utils/jest.config.js new file mode 100644 index 0000000000000..6b86ec6e2da52 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/jest.config.js @@ -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 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-es-utils'], +}; diff --git a/packages/kbn-securitysolution-es-utils/package.json b/packages/kbn-securitysolution-es-utils/package.json new file mode 100644 index 0000000000000..7d0c0993c6c32 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-es-utils", + "version": "1.0.0", + "description": "security solution elastic search utilities to use across plugins such lists, security_solution, cases, etc...", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-es-utils/src/bad_request_error/index.ts b/packages/kbn-securitysolution-es-utils/src/bad_request_error/index.ts new file mode 100644 index 0000000000000..525f6cfa5c9ff --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/bad_request_error/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export class BadRequestError extends Error {} diff --git a/packages/kbn-securitysolution-es-utils/src/create_boostrap_index/index.ts b/packages/kbn-securitysolution-es-utils/src/create_boostrap_index/index.ts new file mode 100644 index 0000000000000..9671d35dc554e --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/create_boostrap_index/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ElasticsearchClient } from '../elasticsearch_client'; + +// See the reference(s) below on explanations about why -000001 was chosen and +// why the is_write_index is true as well as the bootstrapping step which is needed. +// Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/applying-policy-to-template.html +export const createBootstrapIndex = async ( + esClient: ElasticsearchClient, + index: string +): Promise => { + return ( + await esClient.transport.request({ + path: `/${index}-000001`, + method: 'PUT', + body: { + aliases: { + [index]: { + is_write_index: true, + }, + }, + }, + }) + ).body; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/delete_all_index/index.ts b/packages/kbn-securitysolution-es-utils/src/delete_all_index/index.ts new file mode 100644 index 0000000000000..4df4724aaf2b5 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/delete_all_index/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ElasticsearchClient } from '../elasticsearch_client'; + +export const deleteAllIndex = async ( + esClient: ElasticsearchClient, + pattern: string, + maxAttempts = 5 +): Promise => { + for (let attempt = 1; ; attempt++) { + if (attempt > maxAttempts) { + throw new Error( + `Failed to delete indexes with pattern [${pattern}] after ${maxAttempts} attempts` + ); + } + + // resolve pattern to concrete index names + const { body: resp } = await esClient.indices.getAlias( + { + index: pattern, + }, + { ignore: [404] } + ); + + // @ts-expect-error status doesn't exist on response + if (resp.status === 404) { + return true; + } + + const indices = Object.keys(resp) as string[]; + + // if no indexes exits then we're done with this pattern + if (!indices.length) { + return true; + } + + // delete the concrete indexes we found and try again until this pattern resolves to no indexes + await esClient.indices.delete({ + index: indices, + ignore_unavailable: true, + }); + } +}; diff --git a/packages/kbn-securitysolution-es-utils/src/delete_policy/index.ts b/packages/kbn-securitysolution-es-utils/src/delete_policy/index.ts new file mode 100644 index 0000000000000..34c1d2e5da45f --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/delete_policy/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ElasticsearchClient } from '../elasticsearch_client'; + +export const deletePolicy = async ( + esClient: ElasticsearchClient, + policy: string +): Promise => { + return ( + await esClient.transport.request({ + path: `/_ilm/policy/${policy}`, + method: 'DELETE', + }) + ).body; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/delete_template/index.ts b/packages/kbn-securitysolution-es-utils/src/delete_template/index.ts new file mode 100644 index 0000000000000..2e7a71af9f772 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/delete_template/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ElasticsearchClient } from '../elasticsearch_client'; + +export const deleteTemplate = async ( + esClient: ElasticsearchClient, + name: string +): Promise => { + return ( + await esClient.indices.deleteTemplate({ + name, + }) + ).body; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/elasticsearch_client/index.ts b/packages/kbn-securitysolution-es-utils/src/elasticsearch_client/index.ts new file mode 100644 index 0000000000000..0c2252bdc1f03 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/elasticsearch_client/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +// Copied from src/core/server/elasticsearch/client/types.ts +// as these types aren't part of any package yet. Once they are, remove this completely + +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; +import type { + ApiResponse, + TransportRequestOptions, + TransportRequestParams, + TransportRequestPromise, +} from '@elastic/elasticsearch/lib/Transport'; + +/** + * Client used to query the elasticsearch cluster. + * @deprecated At some point use the one from src/core/server/elasticsearch/client/types.ts when it is made into a package. If it never is, then keep using this one. + * @public + */ +export type ElasticsearchClient = Omit< + KibanaClient, + 'connectionPool' | 'transport' | 'serializer' | 'extend' | 'child' | 'close' +> & { + transport: { + request( + params: TransportRequestParams, + options?: TransportRequestOptions + ): TransportRequestPromise; + }; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/get_index_exists/index.ts b/packages/kbn-securitysolution-es-utils/src/get_index_exists/index.ts new file mode 100644 index 0000000000000..b7d12cab3f48c --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/get_index_exists/index.ts @@ -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 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 { ElasticsearchClient } from '../elasticsearch_client'; + +export const getIndexExists = async ( + esClient: ElasticsearchClient, + index: string +): Promise => { + try { + const { body: response } = await esClient.search({ + index, + size: 0, + allow_no_indices: true, + body: { + terminate_after: 1, + }, + }); + return response._shards.total > 0; + } catch (err) { + if (err.body != null && err.body.status === 404) { + return false; + } else { + throw err.body ? err.body : err; + } + } +}; diff --git a/packages/kbn-securitysolution-es-utils/src/get_policy_exists/index.ts b/packages/kbn-securitysolution-es-utils/src/get_policy_exists/index.ts new file mode 100644 index 0000000000000..cefd47dbe9d07 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/get_policy_exists/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ElasticsearchClient } from '../elasticsearch_client'; + +export const getPolicyExists = async ( + esClient: ElasticsearchClient, + policy: string +): Promise => { + try { + await esClient.transport.request({ + path: `/_ilm/policy/${policy}`, + method: 'GET', + }); + // Return true that there exists a policy which is not 404 or some error + // Since there is not a policy exists API, this is how we create one by calling + // into the API to get it if it exists or rely on it to throw a 404 + return true; + } catch (err) { + if (err.statusCode === 404) { + return false; + } else { + throw err; + } + } +}; diff --git a/packages/kbn-securitysolution-es-utils/src/get_template_exists/index.ts b/packages/kbn-securitysolution-es-utils/src/get_template_exists/index.ts new file mode 100644 index 0000000000000..c56c5b968d45c --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/get_template_exists/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ElasticsearchClient } from '../elasticsearch_client'; + +export const getTemplateExists = async ( + esClient: ElasticsearchClient, + template: string +): Promise => { + return ( + await esClient.indices.existsTemplate({ + name: template, + }) + ).body; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/index.ts b/packages/kbn-securitysolution-es-utils/src/index.ts new file mode 100644 index 0000000000000..657a63eef15cd --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './bad_request_error'; +export * from './create_boostrap_index'; +export * from './delete_all_index'; +export * from './delete_policy'; +export * from './delete_template'; +export * from './elasticsearch_client'; +export * from './get_index_exists'; +export * from './get_policy_exists'; +export * from './get_template_exists'; +export * from './read_privileges'; +export * from './set_policy'; +export * from './set_template'; +export * from './transform_error'; diff --git a/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts b/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts new file mode 100644 index 0000000000000..8b11387a1d020 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +/** + * Copied from src/core/server/elasticsearch/legacy/api_types.ts including its deprecation mentioned below + * TODO: Remove this and refactor the readPrivileges to utilize any newer client side ways rather than all this deprecated legacy stuff + */ +export interface LegacyCallAPIOptions { + /** + * Indicates whether `401 Unauthorized` errors returned from the Elasticsearch API + * should be wrapped into `Boom` error instances with properly set `WWW-Authenticate` + * header that could have been returned by the API itself. If API didn't specify that + * then `Basic realm="Authorization Required"` is used as `WWW-Authenticate`. + */ + wrap401Errors?: boolean; + /** + * A signal object that allows you to abort the request via an AbortController object. + */ + signal?: AbortSignal; +} + +type CallWithRequest, V> = ( + endpoint: string, + params: T, + options?: LegacyCallAPIOptions +) => Promise; + +export const readPrivileges = async ( + callWithRequest: CallWithRequest<{}, unknown>, + index: string +): Promise => { + return callWithRequest('transport.request', { + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + cluster: [ + 'all', + 'create_snapshot', + 'manage', + 'manage_api_key', + 'manage_ccr', + 'manage_transform', + 'manage_ilm', + 'manage_index_templates', + 'manage_ingest_pipelines', + 'manage_ml', + 'manage_own_api_key', + 'manage_pipeline', + 'manage_rollup', + 'manage_saml', + 'manage_security', + 'manage_token', + 'manage_watcher', + 'monitor', + 'monitor_transform', + 'monitor_ml', + 'monitor_rollup', + 'monitor_watcher', + 'read_ccr', + 'read_ilm', + 'transport_client', + ], + index: [ + { + names: [index], + privileges: [ + 'all', + 'create', + 'create_doc', + 'create_index', + 'delete', + 'delete_index', + 'index', + 'manage', + 'maintenance', + 'manage_follow_index', + 'manage_ilm', + 'manage_leader_index', + 'monitor', + 'read', + 'read_cross_cluster', + 'view_index_metadata', + 'write', + ], + }, + ], + }, + }); +}; diff --git a/packages/kbn-securitysolution-es-utils/src/set_policy/index.ts b/packages/kbn-securitysolution-es-utils/src/set_policy/index.ts new file mode 100644 index 0000000000000..dc45ca3e1c089 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/set_policy/index.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 { ElasticsearchClient } from '../elasticsearch_client'; + +export const setPolicy = async ( + esClient: ElasticsearchClient, + policy: string, + body: Record +): Promise => { + return ( + await esClient.transport.request({ + path: `/_ilm/policy/${policy}`, + method: 'PUT', + body, + }) + ).body; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/set_template/index.ts b/packages/kbn-securitysolution-es-utils/src/set_template/index.ts new file mode 100644 index 0000000000000..89aaa44f29e0d --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/set_template/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 { ElasticsearchClient } from '../elasticsearch_client'; + +export const setTemplate = async ( + esClient: ElasticsearchClient, + name: string, + body: Record +): Promise => { + return ( + await esClient.indices.putTemplate({ + name, + body, + }) + ).body; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/transform_error/index.test.ts b/packages/kbn-securitysolution-es-utils/src/transform_error/index.test.ts new file mode 100644 index 0000000000000..e0f520f1ebfd4 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/transform_error/index.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 Boom from '@hapi/boom'; +import { transformError } from '.'; +import { BadRequestError } from '../bad_request_error'; +import { errors } from '@elastic/elasticsearch'; + +describe('transformError', () => { + test('returns transformed output error from boom object with a 500 and payload of internal server error', () => { + const boom = new Boom.Boom('some boom message'); + const transformed = transformError(boom); + expect(transformed).toEqual({ + message: 'An internal server error occurred', + statusCode: 500, + }); + }); + + test('returns transformed output if it is some non boom object that has a statusCode', () => { + const error: Error & { statusCode?: number } = { + statusCode: 403, + name: 'some name', + message: 'some message', + }; + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 403, + }); + }); + + test('returns a transformed message with the message set and statusCode', () => { + const error: Error & { statusCode?: number } = { + statusCode: 403, + name: 'some name', + message: 'some message', + }; + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 403, + }); + }); + + test('transforms best it can if it is some non boom object but it does not have a status Code.', () => { + const error: Error = { + name: 'some name', + message: 'some message', + }; + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 500, + }); + }); + + test('it detects a BadRequestError and returns a status code of 400 from that particular error type', () => { + const error: BadRequestError = new BadRequestError('I have a type error'); + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'I have a type error', + statusCode: 400, + }); + }); + + test('it detects a BadRequestError and returns a Boom status of 400', () => { + const error: BadRequestError = new BadRequestError('I have a type error'); + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'I have a type error', + statusCode: 400, + }); + }); + + it('transforms a ResponseError returned by the elasticsearch client', () => { + const error: errors.ResponseError = { + name: 'ResponseError', + message: 'illegal_argument_exception', + headers: {}, + body: { + error: { + type: 'illegal_argument_exception', + reason: 'detailed explanation', + }, + }, + meta: ({} as unknown) as errors.ResponseError['meta'], + statusCode: 400, + }; + const transformed = transformError(error); + + expect(transformed).toEqual({ + message: 'illegal_argument_exception: detailed explanation', + statusCode: 400, + }); + }); +}); diff --git a/packages/kbn-securitysolution-es-utils/src/transform_error/index.ts b/packages/kbn-securitysolution-es-utils/src/transform_error/index.ts new file mode 100644 index 0000000000000..b532dc5d1b6d0 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/transform_error/index.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 Boom from '@hapi/boom'; +import { errors } from '@elastic/elasticsearch'; +import { BadRequestError } from '../bad_request_error'; + +export interface OutputError { + message: string; + statusCode: number; +} + +export const transformError = (err: Error & Partial): OutputError => { + if (Boom.isBoom(err)) { + return { + message: err.output.payload.message, + statusCode: err.output.statusCode, + }; + } else { + if (err.statusCode != null) { + if (err.body != null && err.body.error != null) { + return { + statusCode: err.statusCode, + message: `${err.body.error.type}: ${err.body.error.reason}`, + }; + } else { + return { + statusCode: err.statusCode, + message: err.message, + }; + } + } else if (err instanceof BadRequestError) { + // allows us to throw request validation errors in the absence of Boom + return { + message: err.message, + statusCode: 400, + }; + } else { + // natively return the err and allow the regular framework + // to deal with the error when it is a non Boom + return { + message: err.message != null ? err.message : '(unknown error message)', + statusCode: 500, + }; + } + } +}; diff --git a/packages/kbn-securitysolution-es-utils/tsconfig.json b/packages/kbn-securitysolution-es-utils/tsconfig.json new file mode 100644 index 0000000000000..be8848d781cae --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-es-utils/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel new file mode 100644 index 0000000000000..ba7123d0c1f21 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel @@ -0,0 +1,94 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-alerting-types" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-alerting-types" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-securitysolution-io-ts-types", + "//packages/kbn-securitysolution-io-ts-utils", + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/README.md b/packages/kbn-securitysolution-io-ts-alerting-types/README.md new file mode 100644 index 0000000000000..b8fa8234f2d85 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/README.md @@ -0,0 +1,8 @@ +# kbn-securitysolution-io-ts-alerting-types + +Types that are specific to the security solution alerting to be shared among plugins. + +Related packages are +* kbn-securitysolution-io-ts-utils +* kbn-securitysolution-io-ts-list-types +* kbn-securitysolution-io-ts-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js b/packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js new file mode 100644 index 0000000000000..6125b95a9bce5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js @@ -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 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-alerting-types'], +}; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/package.json b/packages/kbn-securitysolution-io-ts-alerting-types/package.json new file mode 100644 index 0000000000000..ac972e06c1dc9 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-alerting-types", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/actions/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/actions/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/constants/index.mock.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/constants/index.mock.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/constants/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/constants/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_actions_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_actions_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_actions_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_actions_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.test.ts index 1f81f056386d7..f0fe7f44a6f3e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultExportFileName } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_export_file_name', () => { test('it should validate a regular string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.test.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.test.ts index c1261f514540b..ccfb7923a230c 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultFromString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_from_string', () => { test('it should validate a from string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.test.ts index c4a0dc3664d0e..f5706677e6c5d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultIntervalString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_interval_string', () => { test('it should validate a interval string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.test.ts index 072c541a808a3..82bd8607dae72 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { Language } from '../language'; import { DefaultLanguageString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_language_string', () => { test('it should validate a string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.test.ts index bf703fa52d844..eb2af1dbea41a 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultMaxSignalsNumber } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { DEFAULT_MAX_SIGNALS } from '../constants'; describe('default_from_string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.test.ts index 3bcad15a7ebb8..cca1c7e2774f4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultPage } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_page', () => { test('it should validate a regular number greater than zero', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.ts index 056005b452a03..f9140be68ec8d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; /** * Types the DefaultPerPage as: diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.test.ts index f7361ba12a570..88e91986a65dd 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultPerPage } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_per_page', () => { test('it should validate a regular number greater than zero', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.ts index 026642f91c08a..ea8f30c745062 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; /** * Types the DefaultPerPage as: diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_risk_score_mapping_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_risk_score_mapping_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_risk_score_mapping_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_risk_score_mapping_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_severity_mapping_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_severity_mapping_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_severity_mapping_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_severity_mapping_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.test.ts index ac86b5508ff14..5f1ef3fc61fab 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { Threats } from '../threat'; import { DefaultThreatArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_threat_null', () => { test('it should validate an empty array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts index 4b8877bd532c2..b92815d4fe828 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { Throttle } from '../throttle'; import { DefaultThrottleNull } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_throttle_null', () => { test('it should validate a throttle string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.test.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.test.ts index bcab8ebd5f17c..31c35c8319fab 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultToString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_to_string', () => { test('it should validate a to string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.test.ts index d8cdff416037c..c471141a99a76 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultUuid } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_uuid', () => { test('it should validate a regular string', () => { diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.ts new file mode 100644 index 0000000000000..73bf807e92c43 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.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. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import uuid from 'uuid'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; + +/** + * Types the DefaultUuid as: + * - If null or undefined, then a default string uuid.v4() will be + * created otherwise it will be checked just against an empty string + */ +export const DefaultUuid = new t.Type( + 'DefaultUuid', + t.string.is, + (input, context): Either => + input == null ? t.success(uuid.v4()) : NonEmptyString.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/from/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/from/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts index 963e2fa0444f0..3bf4592a581f5 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/from/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts @@ -8,7 +8,7 @@ import { Either } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; -import { parseScheduleDates } from '../parse_schedule_dates'; +import { parseScheduleDates } from '@kbn/securitysolution-io-ts-types'; const stringValidator = (input: unknown): input is string => typeof input === 'string'; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts new file mode 100644 index 0000000000000..639140be049f2 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/index.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. + */ + +export * from './actions'; +export * from './constants'; +export * from './default_actions_array'; +export * from './default_export_file_name'; +export * from './default_from_string'; +export * from './default_interval_string'; +export * from './default_language_string'; +export * from './default_max_signals_number'; +export * from './default_page'; +export * from './default_per_page'; +export * from './default_risk_score_mapping_array'; +export * from './default_severity_mapping_array'; +export * from './default_threat_array'; +export * from './default_throttle_null'; +export * from './default_to_string'; +export * from './default_uuid'; +export * from './from'; +export * from './language'; +export * from './max_signals'; +export * from './normalized_ml_job_id'; +export * from './references_default_array'; +export * from './risk_score'; +export * from './risk_score_mapping'; +export * from './saved_object_attributes'; +export * from './severity'; +export * from './severity_mapping'; +export * from './threat'; +export * from './threat_mapping'; +export * from './threat_subtechnique'; +export * from './threat_tactic'; +export * from './threat_technique'; +export * from './throttle'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/language/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/language/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts similarity index 86% rename from packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts index 4c68cb01cf00f..83360234c65a1 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts @@ -9,7 +9,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; export const max_signals = PositiveIntegerGreaterThanZero; export type MaxSignals = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/normalized_ml_job_id/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/normalized_ml_job_id/index.ts index 6c7eb0ae33ab9..db26264c029cd 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/normalized_ml_job_id/index.ts @@ -10,7 +10,7 @@ import * as t from 'io-ts'; -import { NonEmptyArray } from '../non_empty_array'; +import { NonEmptyArray } from '@kbn/securitysolution-io-ts-types'; export const machine_learning_job_id_normalized = NonEmptyArray(t.string); export type MachineLearningJobIdNormalized = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.test.ts similarity index 77% rename from packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.test.ts index 41754a7ce0606..38fd27ac40fdf 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.test.ts @@ -8,13 +8,13 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { DefaultStringArray } from '../default_string_array'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { ReferencesDefaultArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_string_array', () => { test('it should validate an empty array', () => { const payload: string[] = []; - const decoded = DefaultStringArray.decode(payload); + const decoded = ReferencesDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -23,7 +23,7 @@ describe('default_string_array', () => { test('it should validate an array of strings', () => { const payload = ['value 1', 'value 2']; - const decoded = DefaultStringArray.decode(payload); + const decoded = ReferencesDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -32,18 +32,18 @@ describe('default_string_array', () => { test('it should not validate an array with a number', () => { const payload = ['value 1', 5]; - const decoded = DefaultStringArray.decode(payload); + const decoded = ReferencesDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultStringArray"', + 'Invalid value "5" supplied to "referencesWithDefaultArray"', ]); expect(message.schema).toEqual({}); }); test('it should return a default array entry', () => { const payload = null; - const decoded = DefaultStringArray.decode(payload); + const decoded = ReferencesDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.test.ts index bca8b92134928..d341ca8b3b4f7 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { RiskScore } from '.'; describe('risk_score', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts index 1d7ca20e80b3b..b35b502811ec9 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts @@ -9,10 +9,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; +import { operator } from '@kbn/securitysolution-io-ts-types'; import { RiskScore } from '../risk_score'; -import { operator } from '../operator'; - export const riskScoreOrUndefined = t.union([RiskScore, t.undefined]); export type RiskScoreOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/saved_object_attributes/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/saved_object_attributes/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/saved_object_attributes/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/saved_object_attributes/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/severity/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/severity/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/severity/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/severity/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/severity_mapping/index.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/severity_mapping/index.ts index 9e7ee7d2831cd..1a3fd50039c29 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/severity_mapping/index.ts @@ -10,7 +10,7 @@ import * as t from 'io-ts'; -import { operator } from '../operator'; +import { operator } from '@kbn/securitysolution-io-ts-types'; import { severity } from '../severity'; export const severity_mapping_field = t.string; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.test.ts index 7f754fb2d87de..16fd1647e5bfc 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.test.ts @@ -16,8 +16,7 @@ import { ThreatMappingEntries, threat_mapping, } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; -import { exactCheck } from '../exact_check'; +import { foldLeftRight, getPaths, exactCheck } from '@kbn/securitysolution-io-ts-utils'; describe('threat_mapping', () => { describe('threatMappingEntries', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.ts index 4fc64fe1e0982..abee0d2baceb0 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.ts @@ -9,10 +9,12 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; +import { + NonEmptyArray, + NonEmptyString, + PositiveIntegerGreaterThanZero, +} from '@kbn/securitysolution-io-ts-types'; import { language } from '../language'; -import { NonEmptyArray } from '../non_empty_array'; -import { NonEmptyString } from '../non_empty_string'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; export const threat_query = t.string; export type ThreatQuery = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_subtechnique/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_subtechnique/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_tactic/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_tactic/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_tactic/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_tactic/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_technique/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_technique/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/throttle/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/throttle/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json new file mode 100644 index 0000000000000..3411ce2c93d05 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-alerting-types/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel new file mode 100644 index 0000000000000..e9b806288addd --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel @@ -0,0 +1,94 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-list-types" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-list-types" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-securitysolution-io-ts-types", + "//packages/kbn-securitysolution-io-ts-utils", + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-list-types/README.md b/packages/kbn-securitysolution-io-ts-list-types/README.md new file mode 100644 index 0000000000000..090ede2ed7d62 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/README.md @@ -0,0 +1,8 @@ +# kbn-securitysolution-io-ts-list-types + +io-ts types that are specific to lists to be shared among plugins + +Related packages are +* kbn-securitysolution-io-ts-alerting-types +* kbn-securitysolution-io-ts-ts-utils +* kbn-securitysolution-io-ts-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-list-types/jest.config.js b/packages/kbn-securitysolution-io-ts-list-types/jest.config.js new file mode 100644 index 0000000000000..0312733b6a02b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/jest.config.js @@ -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 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-list-types'], +}; diff --git a/packages/kbn-securitysolution-io-ts-list-types/package.json b/packages/kbn-securitysolution-io-ts-list-types/package.json new file mode 100644 index 0000000000000..74893e59855bc --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-list-types", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.mock.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/comment/index.mock.ts index 56440d628e4aa..380f7f13b6210 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.mock.ts @@ -7,7 +7,7 @@ */ import { Comment, CommentsArray } from '.'; -import { DATE_NOW, ID, USER } from '../../constants/index.mock'; +import { DATE_NOW, ID, USER } from '../constants/index.mock'; export const getCommentsMock = (): Comment => ({ comment: 'some old comment', diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/comment/index.test.ts index 0f0bfac5e2068..89e734a92fd04 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.test.ts @@ -17,8 +17,8 @@ import { CommentsArrayOrUndefined, commentsArrayOrUndefined, } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; -import { DATE_NOW } from '../../constants/index.mock'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { DATE_NOW } from '../constants/index.mock'; describe('Comment', () => { describe('comment', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.ts similarity index 77% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/comment/index.ts index 783d8606b8a96..3b8cc6cc6ce95 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.ts @@ -8,12 +8,12 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; -import { created_at } from '../../created_at'; -import { created_by } from '../../created_by'; -import { id } from '../../id'; -import { updated_at } from '../../updated_at'; -import { updated_by } from '../../updated_by'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { created_at } from '../created_at'; +import { created_by } from '../created_by'; +import { id } from '../id'; +import { updated_at } from '../updated_at'; +import { updated_by } from '../updated_by'; export const comment = t.intersection([ t.exact( diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts new file mode 100644 index 0000000000000..d2107ae864f15 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 const ENTRY_VALUE = 'some host name'; +export const FIELD = 'host.name'; +export const MATCH = 'match'; +export const MATCH_ANY = 'match_any'; +export const OPERATOR = 'included'; +export const NESTED = 'nested'; +export const NESTED_FIELD = 'parent.field'; +export const LIST_ID = 'some-list-id'; +export const LIST = 'list'; +export const TYPE = 'ip'; +export const EXISTS = 'exists'; +export const WILDCARD = 'wildcard'; +export const USER = 'some user'; +export const DATE_NOW = '2020-04-20T15:25:31.830Z'; + +// Exception List specific +export const ID = 'uuid_here'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts new file mode 100644 index 0000000000000..f86986fc328c5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * This ID is used for _both_ the Saved Object ID and for the list_id + * for the single global space agnostic endpoint list. + * + * TODO: Create a kbn-securitysolution-constants and add this to it. + * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. + */ +export const ENDPOINT_LIST_ID = 'endpoint_list'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.test.ts index 1ac605e232ea1..3baf0054221db 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.test.ts @@ -17,7 +17,7 @@ import { CreateCommentsArrayOrUndefined, createCommentsArrayOrUndefined, } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('CreateComment', () => { describe('createComment', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.ts similarity index 93% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.ts index 438f946e796d6..883675ce51f91 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; export const createComment = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/created_at/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/created_at/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/created_at/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/created_at/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/created_by/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/created_by/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/created_by/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/created_by/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.test.ts index 5e667380e2adf..440c601876682 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { CommentsArray } from '../comment'; import { DefaultCommentsArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getCommentsArrayMock } from '../comment/index.mock'; describe('default_comments_array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.test.ts index a4581fabbf6a9..de45fd9f300fa 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { CommentsArray } from '../comment'; import { DefaultCommentsArray } from '../default_comments_array'; import { getCommentsArrayMock } from '../comment/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.test.ts index 1decca0de6c50..21e8c375b3d01 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultNamespace } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_namespace', () => { test('it should validate "single"', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.test.ts index 8bc7a16b96097..b02a3b96a5a3d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultNamespaceArray, DefaultNamespaceArrayType } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_namespace_array', () => { test('it should validate "null" single item as an array with a "single" value', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.test.ts index f52baa49530ec..fa6613538b18e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { UpdateCommentsArray } from '../update_comment'; import { DefaultUpdateCommentsArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getUpdateCommentsArrayMock } from '../update_comment/index.mock'; describe('default_update_comments_array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.test.ts index f77903d2d030d..fd7b12123b6bb 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultVersionNumber } from '../default_version_number'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_version_number', () => { test('it should validate a version number', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/description/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/description/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/description/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/description/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.test.ts index f5cb89ee79607..09f1740567bc1 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.test.ts @@ -14,7 +14,7 @@ import { nonEmptyEndpointEntriesArray, NonEmptyEndpointEntriesArray, } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEndpointEntryNestedMock } from '../entry_nested/index.mock'; import { getEndpointEntriesArrayMock } from './index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.mock.ts similarity index 86% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.mock.ts index 7104406c4869c..17a1a083d73d8 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.mock.ts @@ -7,7 +7,7 @@ */ import { EndpointEntryMatch } from '.'; -import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants/index.mock'; export const getEndpointEntryMatchMock = (): EndpointEntryMatch => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.test.ts index cc0423fc119c7..fc3a2dded177d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEndpointEntryMatchMock } from './index.mock'; import { EndpointEntryMatch, endpointEntryMatch } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchMock } from '../../entry_match/index.mock'; describe('endpointEntryMatch', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.ts similarity index 84% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.ts index 83e2a0f61bb4a..07a1fc58a3d54 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.ts @@ -7,8 +7,7 @@ */ import * as t from 'io-ts'; -import { operatorIncluded } from '../../../operator'; -import { NonEmptyString } from '../../../non_empty_string'; +import { NonEmptyString, operatorIncluded } from '@kbn/securitysolution-io-ts-types'; export const endpointEntryMatch = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.mock.ts similarity index 86% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.mock.ts index 95bd6008f1d7c..13fb16d73457d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.mock.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants/index.mock'; import { EndpointEntryMatchAny } from '.'; export const getEndpointEntryMatchAnyMock = (): EndpointEntryMatchAny => ({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.test.ts index 0fd878986d5a2..cf64647772519 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEndpointEntryMatchAnyMock } from './index.mock'; import { EndpointEntryMatchAny, endpointEntryMatchAny } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchAnyMock } from '../../entry_match_any/index.mock'; describe('endpointEntryMatchAny', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.ts similarity index 76% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.ts index b39a428bb49dd..23c15767a511c 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.ts @@ -7,9 +7,11 @@ */ import * as t from 'io-ts'; -import { nonEmptyOrNullableStringArray } from '../../../non_empty_or_nullable_string_array'; -import { operatorIncluded } from '../../../operator'; -import { NonEmptyString } from '../../../non_empty_string'; +import { + NonEmptyString, + nonEmptyOrNullableStringArray, + operatorIncluded, +} from '@kbn/securitysolution-io-ts-types'; export const endpointEntryMatchAny = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_wildcard/index.ts similarity index 85% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_wildcard/index.ts index b66c5a2588eef..2697f3edc3db4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_wildcard/index.ts @@ -7,8 +7,7 @@ */ import * as t from 'io-ts'; -import { operatorIncluded } from '../../../operator'; -import { NonEmptyString } from '../../../non_empty_string'; +import { NonEmptyString, operatorIncluded } from '@kbn/securitysolution-io-ts-types'; export const endpointEntryMatchWildcard = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.mock.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.mock.ts index f59e29c8ce526..31d983ba58fe3 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.mock.ts @@ -7,7 +7,7 @@ */ import { EndpointEntryNested } from '.'; -import { FIELD, NESTED } from '../../../constants/index.mock'; +import { FIELD, NESTED } from '../../constants/index.mock'; import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.test.ts index 03c02f67b71ad..f8e54e4956527 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { EndpointEntryNested, endpointEntryNested } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEndpointEntryNestedMock } from './index.mock'; import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.ts similarity index 91% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.ts index 249dcc9077b34..bd4c90d851a90 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { nonEmptyEndpointNestedEntriesArray } from '../non_empty_nested_entries_array'; export const endpointEntryNested = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/non_empty_nested_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/non_empty_nested_entries_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/non_empty_nested_entries_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/non_empty_nested_entries_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries/index.test.ts index b6e448f94ce6a..f68fea35e6fdf 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryMatchMock } from '../entry_match/index.mock'; import { entriesArray, entriesArrayOrUndefined, entry } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; import { getEntryListMock } from '../entries_list/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.mock.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.mock.ts index 0882883f4d239..ad2164a3862eb 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryExists } from '.'; -import { EXISTS, FIELD, OPERATOR } from '../../constants/index.mock'; +import { EXISTS, FIELD, OPERATOR } from '../constants/index.mock'; export const getEntryExistsMock = (): EntryExists => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.test.ts index db4edb54dfc29..05451b11de7a6 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryExistsMock } from './index.mock'; import { entriesExists, EntryExists } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesExists', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.ts index f8f1ddecc9ff9..6d65d458583bd 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.ts @@ -8,8 +8,8 @@ import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { listOperator as operator } from '../list_operator'; -import { NonEmptyString } from '../../non_empty_string'; export const entriesExists = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.mock.ts similarity index 86% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.mock.ts index c4afb28f5ac54..2349b9d5ab2b3 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryList } from '.'; -import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../../constants/index.mock'; +import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../constants/index.mock'; export const getEntryListMock = (): EntryList => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.test.ts index 2be3803c356de..5b72242777875 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.test.ts @@ -11,7 +11,7 @@ import { left } from 'fp-ts/lib/Either'; import { getEntryListMock } from './index.mock'; import { entriesList, EntryList } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesList', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.ts similarity index 91% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.ts index b386ca35d2bbb..61d3c7b156fd2 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { type } from '../type'; import { listOperator as operator } from '../list_operator'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.mock.ts similarity index 88% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.mock.ts index 4fdd8d915fe04..38c9f0f922c46 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryMatch } from '.'; -import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../constants/index.mock'; export const getEntryMatchMock = (): EntryMatch => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.test.ts index 744c74c1223df..bff65ad7f6bec 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryMatchMock } from './index.mock'; import { entriesMatch, EntryMatch } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesMatch', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.ts index cab6d0dd4a07f..4f04e01cf8f63 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { listOperator as operator } from '../list_operator'; export const entriesMatch = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.mock.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.mock.ts index 0022b00c604b0..efaf23fe1e784 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryMatchAny } from '.'; -import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../constants/index.mock'; export const getEntryMatchAnyMock = (): EntryMatchAny => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.test.ts index 60fc4cdc26005..c0eb017fdab54 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryMatchAnyMock } from './index.mock'; import { entriesMatchAny, EntryMatchAny } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesMatchAny', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.ts similarity index 82% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.ts index 0add9a610f30b..86e97c579a02c 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.ts @@ -8,9 +8,8 @@ import * as t from 'io-ts'; +import { NonEmptyString, nonEmptyOrNullableStringArray } from '@kbn/securitysolution-io-ts-types'; import { listOperator as operator } from '../list_operator'; -import { nonEmptyOrNullableStringArray } from '../../non_empty_or_nullable_string_array'; -import { NonEmptyString } from '../../non_empty_string'; export const entriesMatchAny = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.mock.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.mock.ts index 9810fe5e9875b..f81a8c6cba2ef 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryMatchWildcard } from '.'; -import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../constants/index.mock'; export const getEntryMatchWildcardMock = (): EntryMatchWildcard => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.test.ts index d9170dd60ab40..8a5a152ce7e65 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryMatchWildcardMock } from './index.mock'; import { entriesMatchWildcard, EntryMatchWildcard } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesMatchWildcard', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.ts similarity index 91% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.ts index aab5ba5e8e32c..ea1953b983d45 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { listOperator as operator } from '../list_operator'; export const entriesMatchWildcard = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.mock.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.mock.ts index acde4443cccb7..05f42cdf69bc0 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryNested } from '.'; -import { NESTED, NESTED_FIELD } from '../../constants/index.mock'; +import { NESTED, NESTED_FIELD } from '../constants/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; import { getEntryMatchExcludeMock, getEntryMatchMock } from '../entry_match/index.mock'; import { getEntryMatchAnyExcludeMock, getEntryMatchAnyMock } from '../entry_match_any/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.test.ts index b6bbc4dbef4a3..b21737535fd77 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryNestedMock } from './index.mock'; import { entriesNested, EntryNested } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.ts index ff224dd836a19..f5ac68cc98702 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { nonEmptyNestedEntriesArray } from '../non_empty_nested_entries_array'; export const entriesNested = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/exception_list/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/exception_list/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list_item_type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/exception_list_item_type/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list_item_type/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/exception_list_item_type/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/id/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/id/index.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/id/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/id/index.ts index 7b187d7730f73..5952bd2eda21f 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/id/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/id/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; export const id = NonEmptyString; export type Id = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/index.ts similarity index 78% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/index.ts index 9dd58e2a5a177..1a1c1c3314821 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/index.ts @@ -5,13 +5,19 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + export * from './comment'; +export * from './constants'; export * from './create_comment'; +export * from './created_at'; +export * from './created_by'; export * from './default_comments_array'; export * from './default_create_comments_array'; export * from './default_namespace'; export * from './default_namespace_array'; export * from './default_update_comments_array'; +export * from './default_version_number'; +export * from './description'; export * from './endpoint'; export * from './entries'; export * from './entries_exist'; @@ -22,12 +28,19 @@ export * from './entry_match_wildcard'; export * from './entry_nested'; export * from './exception_list'; export * from './exception_list_item_type'; +export * from './id'; export * from './item_id'; +export * from './list_operator'; export * from './lists'; export * from './lists_default_array'; +export * from './meta'; +export * from './name'; export * from './non_empty_entries_array'; export * from './non_empty_nested_entries_array'; -export * from './list_operator'; export * from './os_type'; +export * from './tags'; export * from './type'; export * from './update_comment'; +export * from './updated_at'; +export * from './updated_by'; +export * from './version'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/item_id/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/item_id/index.ts index 171db8fd60fd1..dcb03884eadab 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/item_id/index.ts @@ -9,7 +9,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; export const item_id = NonEmptyString; export type ItemId = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/list_operator/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/list_operator/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/list_operator/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/list_operator/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.mock.ts similarity index 93% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists/index.mock.ts index c6f54b57d937b..e9f34c4cf789f 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.mock.ts @@ -7,7 +7,7 @@ */ import { List, ListArray } from '.'; -import { ENDPOINT_LIST_ID } from '../../constants'; +import { ENDPOINT_LIST_ID } from '../constants'; export const getListMock = (): List => ({ id: 'some_uuid', diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists/index.test.ts index 77d5e72ef8bc8..88dcc1ced8607 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEndpointListMock, getListArrayMock, getListMock } from './index.mock'; import { List, list, ListArray, listArray, ListArrayOrUndefined, listArrayOrUndefined } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('Lists', () => { describe('list', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.ts similarity index 93% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists/index.ts index 1bd1806564856..7881a6bb3322e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.ts @@ -7,9 +7,9 @@ */ import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { exceptionListType } from '../exception_list'; import { namespaceType } from '../default_namespace'; -import { NonEmptyString } from '../../non_empty_string'; export const list = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.test.ts index 03d16d8e1b5ca..58a52d26aa34f 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultListArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getListArrayMock } from '../lists/index.mock'; describe('lists_default_array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/meta/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/meta/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/meta/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/meta/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/name/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/name/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/name/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/name/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.test.ts index 11e6e54b344a9..98976f3cd6d21 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { EntriesArray } from '../entries'; import { nonEmptyEntriesArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchMock } from '../entry_match/index.mock'; import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.test.ts index 95b74a6d4fe43..8ac958577f8d7 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { EntriesArray } from '../entries'; import { nonEmptyNestedEntriesArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchMock } from '../entry_match/index.mock'; import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/os_type/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/os_type/index.ts index 5ff60e05817d5..b7fa544c956ee 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/os_type/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { DefaultArray } from '../../default_array'; +import { DefaultArray } from '@kbn/securitysolution-io-ts-types'; export const osType = t.keyof({ linux: null, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/tags/index.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/tags/index.ts index 48bcca0551352..f0f23d9e4717d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/tags/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { DefaultStringArray } from '../default_string_array'; +import { DefaultStringArray } from '@kbn/securitysolution-io-ts-types'; export const tags = DefaultStringArray; export type Tags = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.mock.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.mock.ts index 3b5cb256b28bf..e9a56119dcc20 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.mock.ts @@ -7,7 +7,7 @@ */ import { UpdateComment, UpdateCommentsArray } from '.'; -import { ID } from '../../constants/index.mock'; +import { ID } from '../constants/index.mock'; export const getUpdateCommentMock = (): UpdateComment => ({ comment: 'some comment', diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.test.ts index a6fc285f05465..8dd0301c54dd8 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.test.ts @@ -17,7 +17,7 @@ import { UpdateCommentsArrayOrUndefined, updateCommentsArrayOrUndefined, } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('CommentsUpdate', () => { describe('updateComment', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.ts index 496ff07c5616f..5499690c97716 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.ts @@ -7,8 +7,8 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; -import { id } from '../../id'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { id } from '../id'; export const updateComment = t.intersection([ t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/updated_at/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/updated_at/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/updated_at/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/updated_at/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/updated_by/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/updated_by/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/updated_by/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/updated_by/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/version/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/version/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/version/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/version/index.ts index 38cb47ebce53e..97a81b546c841 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/version/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/version/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; /** * Note this is just a positive number, but we use it as a type here which is still ok. diff --git a/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json new file mode 100644 index 0000000000000..d926653a4230b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-list-types/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel new file mode 100644 index 0000000000000..0a21f5ed94f01 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel @@ -0,0 +1,93 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-types" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-types" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-securitysolution-io-ts-utils", + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-types/README.md b/packages/kbn-securitysolution-io-ts-types/README.md new file mode 100644 index 0000000000000..552c663d819e3 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/README.md @@ -0,0 +1,8 @@ +# kbn-securitysolution-io-ts-types + +Generic io-ts types that are not specific to any particular domain for use with other packages or across different plugins/domains + +Related packages are: +* kbn-securitysolution-io-ts-utils +* kbn-securitysolution-io-ts-list-types +* kbn-securitysolution-io-ts-alerting-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-types/jest.config.js b/packages/kbn-securitysolution-io-ts-types/jest.config.js new file mode 100644 index 0000000000000..18d31eaa75219 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/jest.config.js @@ -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 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-types'], +}; diff --git a/packages/kbn-securitysolution-io-ts-types/package.json b/packages/kbn-securitysolution-io-ts-types/package.json new file mode 100644 index 0000000000000..0381a6d24a136 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-types", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_array/index.test.ts index 82fa884b1c577..4ca45e7de3377 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_array/index.test.ts @@ -11,7 +11,7 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; const testSchema = t.keyof({ valid: true, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.test.ts index bddf9cc0747ea..c87a67ec4e5d4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultBooleanFalse } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_boolean_false', () => { test('it should validate a boolean false', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.test.ts index a05fb586c2e92..3ec33fda392e4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultBooleanTrue } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_boolean_true', () => { test('it should validate a boolean false', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.test.ts index 5bdc9b298649e..02fb74510d604 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultEmptyString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_empty_string', () => { test('it should validate a regular string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.test.ts index c7137d9c56b0d..7b1f217f55ad5 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultStringArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_string_array', () => { test('it should validate an empty array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.test.ts index 2443e8f71fecd..3e96c942de74a 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultStringBooleanFalse } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_string_boolean_false', () => { test('it should validate a boolean false', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts new file mode 100644 index 0000000000000..c471141a99a76 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.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 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 { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultUuid } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_uuid', () => { + test('it should validate a regular string', () => { + const payload = '1'; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "DefaultUuid"']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of a uuid', () => { + const payload = null; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.test.ts index 86ffba6eeb60a..5b7863947cad4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { EmptyStringArray, EmptyStringArrayEncoded } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('empty_string_array', () => { test('it should validate "null" and create an empty array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/index.ts b/packages/kbn-securitysolution-io-ts-types/src/index.ts new file mode 100644 index 0000000000000..8b5a4d9e4de9a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './default_array'; +export * from './default_boolean_false'; +export * from './default_boolean_true'; +export * from './default_empty_string'; +export * from './default_string_array'; +export * from './default_string_boolean_false'; +export * from './default_uuid'; +export * from './empty_string_array'; +export * from './iso_date_string'; +export * from './non_empty_array'; +export * from './non_empty_or_nullable_string_array'; +export * from './non_empty_string'; +export * from './non_empty_string_array'; +export * from './operator'; +export * from './only_false_allowed'; +export * from './parse_schedule_dates'; +export * from './positive_integer'; +export * from './positive_integer_greater_than_zero'; +export * from './string_to_positive_number'; +export * from './uuid'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.test.ts index 4b73ed1b136dc..e70a738d7336e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { IsoDateString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('ios_date_string', () => { test('it should validate a iso string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.ts b/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.test.ts index 0ea7eb5539ba9..0586195360142 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.test.ts @@ -11,7 +11,7 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { NonEmptyArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; const testSchema = t.keyof({ valid: true, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.test.ts index fb2e91862d91e..355bd9d20061e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { nonEmptyOrNullableStringArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('nonEmptyOrNullableStringArray', () => { test('it should FAIL validation when given an empty array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.test.ts index 15c8ced8c915f..ae3b8cd9acad5 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { NonEmptyString } from '.'; describe('non_empty_string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.test.ts index 9fec36f46dd27..f56fa7faed2a4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { NonEmptyStringArray } from '.'; describe('non_empty_string_array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.test.ts index 7f06ec2153a50..de05872c0dc31 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { OnlyFalseAllowed } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('only_false_allowed', () => { test('it should validate a boolean false as false', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.ts b/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/operator/index.ts b/packages/kbn-securitysolution-io-ts-types/src/operator/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/operator/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/operator/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts b/packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts similarity index 85% rename from packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts index a2cc15d82391c..d6a99b5fbf880 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts @@ -9,10 +9,6 @@ import moment from 'moment'; import dateMath from '@elastic/datemath'; -/** - * TODO: Move this to kbn-securitysolution-utils - * @deprecated Use the parseScheduleDates from the kbn-securitysolution-utils. - */ export const parseScheduleDates = (time: string): moment.Moment | null => { const isValidDateString = !isNaN(Date.parse(time)); const isValidInput = isValidDateString || time.trim().startsWith('now'); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.test.ts index c6c841b746089..deea8951a3d39 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { PositiveInteger } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.test.ts index 4655207a6448e..4ea6fe920cf14 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { PositiveIntegerGreaterThanZero } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/string_to_positive_number/index.ts b/packages/kbn-securitysolution-io-ts-types/src/string_to_positive_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/string_to_positive_number/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/string_to_positive_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/uuid/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/uuid/index.test.ts index e8214ac60313f..4333fab102d44 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/uuid/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { UUID } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('uuid', () => { test('it should validate a uuid', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.ts b/packages/kbn-securitysolution-io-ts-types/src/uuid/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/uuid/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/uuid/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-types/tsconfig.json new file mode 100644 index 0000000000000..42a059439ecb5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-types/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-utils/README.md b/packages/kbn-securitysolution-io-ts-utils/README.md index 908651b50b80a..146f965391aa0 100644 --- a/packages/kbn-securitysolution-io-ts-utils/README.md +++ b/packages/kbn-securitysolution-io-ts-utils/README.md @@ -1,10 +1,12 @@ # kbn-securitysolution-io-ts-utils -Temporary location for all the io-ts-utils from security solutions. This is a lift-and-shift, where -we are moving them here for phase 1. +Very small set of utilities for io-ts which we use across plugins within security solutions such as securitysolution, lists, cases, etc... +This folder should remain small and concise since it is pulled into front end and the more files we add the more weight will be added to all +of the plugins. Also, any new dependencies added to this will add weight here and the other plugins, so be careful of what is added here. -Phase 2 is deprecating across plugins any copied code or sharing of io-ts utils that are now in here. +You might consider making another package instead and putting a dependency on this one if needed, instead. -Phase 3 is replacing those deprecated types with the ones in here. - -Phase 4+ is (potentially) consolidating any duplication or everything altogether with the `kbn-io-ts-utils` project \ No newline at end of file +Related packages are +* kbn-securitysolution-io-ts-alerting-types +* kbn-securitysolution-io-ts-list-types +* kbn-securitysolution-io-ts-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts deleted file mode 100644 index b9e9a3ff367e4..0000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts +++ /dev/null @@ -1,65 +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 { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultVersionNumber } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_version_number', () => { - test('it should validate a version number', () => { - const payload = 5; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a 0', () => { - const payload = 0; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a -1', () => { - const payload = -1; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a string', () => { - const payload = '5'; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of 1', () => { - const payload = null; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(1); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts deleted file mode 100644 index 245ff9d0db7dd..0000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { Either } from 'fp-ts/lib/Either'; -import { version, Version } from '../version'; - -/** - * Types the DefaultVersionNumber as: - * - If null or undefined, then a default of the number 1 will be used - */ -export const DefaultVersionNumber = new t.Type( - 'DefaultVersionNumber', - version.is, - (input, context): Either => - input == null ? t.success(1) : version.validate(input, context), - t.identity -); - -export type DefaultVersionNumberDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/index.ts index 1a18293393af5..c21096e497134 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/index.ts @@ -7,71 +7,7 @@ */ export * from './format_errors'; -export * from './actions'; -export * from './constants'; -export * from './created_at'; -export * from './created_by'; -export * from './default_version_number'; -export * from './default_actions_array'; -export * from './default_array'; -export * from './default_boolean_false'; -export * from './default_boolean_true'; -export * from './default_empty_string'; -export * from './default_export_file_name'; -export * from './default_from_string'; -export * from './default_interval_string'; -export * from './default_language_string'; -export * from './default_max_signals_number'; -export * from './default_page'; -export * from './default_per_page'; -export * from './default_risk_score_mapping_array'; -export * from './default_severity_mapping_array'; -export * from './default_string_array'; -export * from './default_string_boolean_false'; -export * from './default_threat_array'; -export * from './default_throttle_null'; -export * from './default_to_string'; -export * from './default_uuid'; -export * from './default_version_number'; -export * from './description'; -export * from './empty_string_array'; export * from './exact_check'; export * from './format_errors'; -export * from './from'; -export * from './id'; -export * from './iso_date_string'; -export * from './language'; -export * from './list_types'; -export * from './max_signals'; -export * from './meta'; -export * from './name'; -export * from './non_empty_array'; -export * from './non_empty_or_nullable_string_array'; -export * from './non_empty_string'; -export * from './non_empty_string_array'; -export * from './normalized_ml_job_id'; -export * from './only_false_allowed'; -export * from './operator'; -export * from './parse_schedule_dates'; -export * from './positive_integer'; -export * from './positive_integer_greater_than_zero'; -export * from './references_default_array'; -export * from './risk_score'; -export * from './risk_score_mapping'; -export * from './saved_object_attributes'; -export * from './severity'; -export * from './severity_mapping'; -export * from './string_to_positive_number'; -export * from './tags'; export * from './test_utils'; -export * from './threat'; -export * from './threat_mapping'; -export * from './threat_subtechnique'; -export * from './threat_tactic'; -export * from './threat_technique'; -export * from './throttle'; -export * from './updated_at'; -export * from './updated_by'; -export * from './uuid'; export * from './validate'; -export * from './version'; diff --git a/packages/kbn-securitysolution-utils/BUILD.bazel b/packages/kbn-securitysolution-utils/BUILD.bazel index 33c25a3091c24..42897e93593b6 100644 --- a/packages/kbn-securitysolution-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-utils/BUILD.bazel @@ -35,7 +35,7 @@ SRC_DEPS = [ TYPES_DEPS = [ "@npm//@types/jest", "@npm//@types/node", - "@npm//@types/uuid" + "@npm//@types/uuid" ] DEPS = SRC_DEPS + TYPES_DEPS @@ -83,4 +83,4 @@ filegroup( ":npm_module", ], visibility = ["//visibility:public"], -) \ No newline at end of file +) diff --git a/packages/kbn-telemetry-tools/BUILD.bazel b/packages/kbn-telemetry-tools/BUILD.bazel new file mode 100644 index 0000000000000..4d1b4f21117c4 --- /dev/null +++ b/packages/kbn-telemetry-tools/BUILD.bazel @@ -0,0 +1,97 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-telemetry-tools" +PKG_REQUIRE_NAME = "@kbn/telemetry-tools" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__fixture__/**", + "**/__snapshots__/**", + ] +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", + "GUIDELINE.md", +] + +SRC_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-utility-types", + "@npm//glob", + "@npm//jest-styled-components", + "@npm//listr", + "@npm//normalize-path", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/glob", + "@npm//@types/jest", + "@npm//@types/listr", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/normalize-path", + "@npm//@types/testing-library__jest-dom", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-telemetry-tools/package.json b/packages/kbn-telemetry-tools/package.json index 31fac5c043832..cd3fd3aa966c7 100644 --- a/packages/kbn-telemetry-tools/package.json +++ b/packages/kbn-telemetry-tools/package.json @@ -7,10 +7,5 @@ "private": true, "kibana": { "devOnly": true - }, - "scripts": { - "build": "../../node_modules/.bin/babel src --out-dir target --delete-dir-on-start --extensions .ts --source-maps=inline", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" } } \ No newline at end of file diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 419af1d02f83b..926ebff17f639 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "declaration": true, "declarationMap": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-telemetry-tools/src", "isolatedModules": true diff --git a/rfcs/images/0018_agent_manager.png b/rfcs/images/0018_agent_manager.png new file mode 100644 index 0000000000000..92a135386e258 Binary files /dev/null and b/rfcs/images/0018_agent_manager.png differ diff --git a/rfcs/images/0018_buildkite_build.png b/rfcs/images/0018_buildkite_build.png new file mode 100644 index 0000000000000..1b04fc85561c8 Binary files /dev/null and b/rfcs/images/0018_buildkite_build.png differ diff --git a/rfcs/images/0018_buildkite_deps.png b/rfcs/images/0018_buildkite_deps.png new file mode 100644 index 0000000000000..f4b0376d6e26f Binary files /dev/null and b/rfcs/images/0018_buildkite_deps.png differ diff --git a/rfcs/images/0018_buildkite_uptime.png b/rfcs/images/0018_buildkite_uptime.png new file mode 100644 index 0000000000000..f850df50b0f1b Binary files /dev/null and b/rfcs/images/0018_buildkite_uptime.png differ diff --git a/rfcs/images/0018_jenkins_pipeline_steps.png b/rfcs/images/0018_jenkins_pipeline_steps.png new file mode 100644 index 0000000000000..51ad8fde8aae0 Binary files /dev/null and b/rfcs/images/0018_jenkins_pipeline_steps.png differ diff --git a/rfcs/images/timeslider/toolbar.png b/rfcs/images/timeslider/toolbar.png new file mode 100644 index 0000000000000..3dae2af0d77f7 Binary files /dev/null and b/rfcs/images/timeslider/toolbar.png differ diff --git a/rfcs/images/timeslider/v1.png b/rfcs/images/timeslider/v1.png new file mode 100644 index 0000000000000..4766fa24c816a Binary files /dev/null and b/rfcs/images/timeslider/v1.png differ diff --git a/rfcs/images/timeslider/v2.png b/rfcs/images/timeslider/v2.png new file mode 100644 index 0000000000000..e4a77e241dc83 Binary files /dev/null and b/rfcs/images/timeslider/v2.png differ diff --git a/rfcs/text/0018_buildkite.md b/rfcs/text/0018_buildkite.md new file mode 100644 index 0000000000000..560540ae3af0b --- /dev/null +++ b/rfcs/text/0018_buildkite.md @@ -0,0 +1,923 @@ +- Start Date: 2021-03-29 +- RFC PR: [#95070](https://github.com/elastic/kibana/pull/95070) +- Kibana Issue: [#94630](https://github.com/elastic/kibana/issues/94630) + +--- + +- [Summary](#summary) +- [Motivation](#motivation) + - [Required and Desired Capabilities](#required-and-desired-capabilities) + - [Required](#required) + - [Scalable](#scalable) + - [Stable](#stable) + - [Surfaces information intuitively](#surfaces-information-intuitively) + - [Pipelines](#pipelines) + - [Advanced Pipeline logic](#advanced-pipeline-logic) + - [Cloud-friendly pricing model](#cloud-friendly-pricing-model) + - [Public access](#public-access) + - [Secrets handling](#secrets-handling) + - [Support or Documentation](#support-or-documentation) + - [Scheduled Builds](#scheduled-builds) + - [Container support](#container-support) + - [Desired](#desired) + - [Customization](#customization) + - [Core functionality is first-party](#core-functionality-is-first-party) + - [First-class support for test results](#first-class-support-for-test-results) + - [GitHub Integration](#github-integration) +- [Buildkite - Detailed design](#buildkite---detailed-design) + - [Overview](#overview) + - [Required and Desired Capabilities](#required-and-desired-capabilities-1) + - [Required](#required-1) + - [Scalable](#scalable-1) + - [Stable](#stable-1) + - [Surfaces information intuitively](#surfaces-information-intuitively-1) + - [Pipelines](#pipelines-1) + - [Advanced Pipeline logic](#advanced-pipeline-logic-1) + - [Cloud-friendly pricing model](#cloud-friendly-pricing-model-1) + - [Public access](#public-access-1) + - [Secrets handling](#secrets-handling-1) + - [Support or Documentation](#support-or-documentation-1) + - [Scheduled Builds](#scheduled-builds-1) + - [Container support](#container-support-1) + - [Desired](#desired-1) + - [Customization](#customization-1) + - [Core functionality is first-party](#core-functionality-is-first-party-1) + - [First-class support for test results](#first-class-support-for-test-results-1) + - [GitHub Integration](#github-integration-1) + - [What we will build and manage](#what-we-will-build-and-manage) + - [Elastic Buildkite Agent Manager](#elastic-buildkite-agent-manager) + - [Overview](#overview-1) + - [Design](#design) + - [Protection against creating too many instances](#protection-against-creating-too-many-instances) + - [Configuration](#configuration) + - [Build / Deploy](#build--deploy) + - [Elastic Buildkite PR Bot](#elastic-buildkite-pr-bot) + - [Overview](#overview-2) + - [Configuration](#configuration-1) + - [Build / Deploy](#build--deploy-1) + - [Infrastructure](#infrastructure) + - [Monitoring / Alerting](#monitoring--alerting) + - [Agent Image management](#agent-image-management) + - [Buildkite org-level settings management](#buildkite-org-level-settings-management) + - [IT Security Processes](#it-security-processes) +- [Drawbacks](#drawbacks) +- [Alternatives](#alternatives) + - [Jenkins](#jenkins) + - [Required](#required-2) + - [Scalable](#scalable-2) + - [Stable](#stable-2) + - [Updates](#updates) + - [Surfaces information intuitively](#surfaces-information-intuitively-2) + - [Pipelines](#pipelines-2) + - [Advanced Pipeline logic](#advanced-pipeline-logic-2) + - [Cloud-friendly pricing model](#cloud-friendly-pricing-model-2) + - [Public access](#public-access-2) + - [Secrets handling](#secrets-handling-2) + - [Support or Documentation](#support-or-documentation-2) + - [Scheduled Builds](#scheduled-builds-2) + - [Container support](#container-support-2) + - [Desired](#desired-2) + - [Customization](#customization-2) + - [Core functionality is first-party](#core-functionality-is-first-party-2) + - [First-class support for test results](#first-class-support-for-test-results-2) + - [GitHub Integration](#github-integration-2) + - [Other solutions](#other-solutions) + - [CircleCI](#circleci) + - [GitHub Actions](#github-actions) +- [Adoption strategy](#adoption-strategy) +- [How we teach this](#how-we-teach-this) + +# Summary + +Implement a CI system for Kibana teams that is highly scalable and stable, surfaces information in an intuitive way, and supports pipelines that are easy to understand and change. + +This table provides an overview of the conclusions made throughout the rest of this document. A lot of this is subjective, but we've tried to take an honest look at each system and feature, based on a large amount of research on and/or experience with each system, our requirements, and our preferences as a team. Your team would likely come to different conclusions based on your preferences and requirements. + +| | Jenkins | Buildkite | GitHub Actions | CircleCI | TeamCity | +| ------------------------------------ | ------- | --------- | -------------- | -------- | -------- | +| Scalable | No | Yes | No | Yes | No | +| Stable | No | Yes | No | Yes | Partial | +| Surfaces information intuitively | No | Yes | No | Yes | Yes | +| Pipelines | Yes | Yes | Yes | Yes | Partial | +| Advanced Pipeline logic | Yes | Yes | Partial | Partial | No | +| Cloud-friendly pricing model | Yes | Yes | Yes | No | No | +| Public access | Yes | Yes | Yes | Partial | Yes | +| Secrets handling | Yes | Partial | Yes | Partial | Partial | +| Support or Documentation | No | Yes | Yes | Partial | Yes | +| Scheduled Builds | Yes | Yes | Yes | Yes | Yes | +| Container support | Partial | Yes | Yes | Yes | Partial | +| | | | | | | +| Customization | No | Yes | No | No | No | +| Core functionality is first-party | No | Yes | Mostly | Yes | Mostly | +| First-class support for test results | Buggy | No | No | Yes | Yes | +| GitHub Integration | Yes | Limited | Yes | Yes | Yes | + +# Motivation + +We have lived with the scalability and stability problems of our current Jenkins infrastructure for several years. We have spent a significant amount of time designing around problems, and are limited in how we can design our pipelines. Since the company-wide effort to move to a new system has been cancelled for the foreseeable future, we are faced with either re-engineering the way we use Jenkins, or exploring other solutions and potentially managing one ourselves. + +This RFC is focused on the option of using a system other than Jenkins, and managing it ourselves (to the extent that it must be managed). If the RFC is rejected, the alternative will be to instead invest significantly into Jenkins to further stabilize and scale our usage of it. + +## Required and Desired Capabilities + +### Required + +#### Scalable + +- Able to run 100s of pipelines and 1000s of individual steps in parallel without issues. +- If scaling agents/hosts is self-managed, dynamically scaling up and down based on usage should be supported and reasonably easy to do. + +#### Stable + +- Every minute of downtime can affect 100s of developers. +- The Kibana Operations team can't have an on-call rotation, so we need to minimize our responsibilities around stability/uptime. +- For systems provided as a service, they should not have frequent outages. This is a bit hard to define. 1-2 hours of downtime, twice a month, during peak working hours, is extremely disruptive. 10 minutes of downtime once or twice a week can also be very disruptive, as builds might need to be re-triggered, etc. +- For self-hosted solutions, they should be reasonably easy to keep online and have a solution for high-availability. At a minimum, most upgrades should not require waiting for all currently running jobs to finish before deploying. +- Failures are ideally handled gracefully. For example, agents may continue running tasks correctly, once the primary service becomes available again. + +#### Surfaces information intuitively + +- Developers should be able to easily understand what happened during their builds, and find information related to failures. +- User interfaces should be functional and easy to use. +- Overview and details about failures and execution time are particularly important. + +#### Pipelines + +- Pipelines should be defined as code. +- Pipelines should be reasonably easy to understand and change. Kibana team members should be able to follow a simple guide and create new pipelines on their own. +- Changes to pipelines should generally be able to be tested in Pull Requests before being merged. + +#### Advanced Pipeline logic + +With such a large codebase and CI pipeline, we often have complex requirements around when and how certain tasks should run, and we want the ability to handle this built into the system we use. It can be very difficult and require complex solutions for fairly simple use cases when the system does not support advanced pipeline logic out of the box. + +For example, the flaky test suite runner that we currently have in Jenkins is fairly simple: run a given task (which might have a dependency) `N` number of times on `M` agents. This is very difficult to model in a system like TeamCity, which does not have dynamic dependencies. + +- Retries + - Automatic (e.g. run a test suite twice to account for flakiness) and manual (user-initiated) + - Full (e.g. a whole pipeline) and partial (e.g. a single step) +- Dynamic pipelines + - Conditional dependencies/steps + - Based on user input + - Based on external events/data (e.g. PR label) + - Based on source code or changes (e.g. only run this for .md changes) +- Metadata and Artifacts re-usable between tasks + - Metadata could be a docker image tag for a specific task, built from a previous step + +#### Cloud-friendly pricing model + +If the given system has a cost, the pricing model should be cloud-friendly and/or usage-based. + +A per-agent or per-build model based on peak usage in a month is not a good model, because our peak build times are generally short-lived (e.g. around feature freeze). + +A model based on build-minutes can also be bad, if it encourages running things in parallel on bigger machines to keep costs down. For example, running two tasks on a single 2-CPU machine with our own orchestration should not be cheaper than running two tasks on two 1-CPU machines using the system's built-in orchestration. + +#### Public access + +Kibana is a publicly-available repository with contributors from outside Elastic. CI information needs to be available publicly in some form. + +#### Secrets handling + +Good, first-class support for handling secrets is a must-have for any CI system. This support can take many forms. + +- Secrets should not need to be stored in plaintext, in a repo nor on the server. +- For systems provided as a service, it is ideal if secrets are kept mostly/entirely on our infrastructure. +- There should be protections against accidentally leaking secrets to the console. +- There should be programmatic ways to manage secrets. +- Secrets are, by nature, harder to handle. However, the easier the system makes it, the more likely people are to follow best practices. + +#### Support or Documentation + +For paid systems, both self-hosted and as a service, good support is important. If a problem specific to Elastic is causing us downtime, we expect quick and efficient support. Again, 100s of developers are potentially affected by downtime. + +For open source solutions, good documentation is especially important. If much of the operational knowledge of a system can only be gained by working with the system and/or reading the source code, it will be harder to solve problems quickly. + +#### Scheduled Builds + +We have certain pipelines (ES Snapshots) that run once daily, and `master` CI currently only runs once an hour. We need the ability to configure scheduled builds. + +#### Container support + +We have the desire to use containers to create fast, clean environments for CI stages that can also be used locally. We think that we can utilize [modern layer-caching options](https://github.com/moby/buildkit#cache), both local and remote, to optimize bootstrapping various CI stages, doing retries, etc. + +For self-hosted options, containers will allow us to utilize longer-running instances (with cached layers, git repos, etc) without worrying about polluting the build environment between builds. + +If we use containers for CI stages, when a test fails, developers can pull the image and reproduce the failure in the same environment that was used in CI. + +So, we need a solution that at least allows us to build and run our own containers. The more features that exist for managing this, the easier it will be. + +### Desired + +#### Customization + +We have very large CI pipelines which generate a lot of information (bundle sizes, performance numbers, etc). Being able to attach this information to builds, so that it lives with the builds in the CI system, is highly desirable. The alternative is building custom reports and UIs outside of the system. + +#### Core functionality is first-party + +Most core functionality that we depend on should be created and maintained by the organization maintaining the CI software. It's important for bugs to be addressed quickly, for security issues to be resolved, and for functionality to be tested before a new release of the system. In this way, there is a large amount of risk associated with relying on third-party solutions for too much core functionality. + +#### First-class support for test results + +One of the primary reasons we run CI is to run tests and make sure they pass. There are currently around 65,000 tests (unit, integration, and functional) that run in CI. Being able to see summaries, histories, and details of test execution directly on build pages is extremely useful. Flaky test identification is also very useful, as we deal with flaky tests on a daily basis. + +For example, being able to easily see that a build passed but included 5,000 tests fewer than the previous build can make something like a pipeline misconfiguration more obvious. Being able to click on a failed test and see other recent builds where the same test failed can help identify what kind of failure it is and how important it is to resolve it quickly (e.g is it failing in 75% of builds or 5% of builds?). + +For any system that doesn't have this kind of support, we will need to maintain our own solution, customize build pages to include this (if the system allows), or both. + +#### GitHub Integration + +- Ability to trigger jobs based on webhooks +- Integrate GitHub-specific information into UI, e.g. a build for a PR should link back to the PR +- Ability to set commit statuses based on job status +- Fine-grained permission handling for pull request triggering + +# Buildkite - Detailed design + +For the alternative system in this RFC, we are recommending Buildkite. The UI, API, and documentation have been a joy to work with, they provide most of our desired features and functionality, the team is responsive and knowledgeable, and the pricing model does not encourage bad practices to lower cost. + +## Overview + +[Buildkite](https://buildkite.com/home) is a CI system where the user manages and hosts their own agents, and Buildkite manages and hosts everything else (core services, APIs, UI). + +The [Buildkite features](https://buildkite.com/features) page is a great overview of the functionality offered. + +For some public instances of Buildkite in action, see: + +- [Bazel](https://buildkite.com/bazel) +- [Rails](https://buildkite.com/rails) +- [Chef](https://buildkite.com/chef-oss) + +## Required and Desired Capabilities + +How does Buildkite stack up against our required and desired capabilities? + +### Required + +#### Scalable + +Buildkite claims to support up to 10,000 connected agents "without breaking a sweat." + +We were able to connect 2,200 running agents and run a [single job with 1,800 parallel build steps](https://buildkite.com/elastic/kibana-custom/builds/8). The job ran with only about 15 seconds of total overhead (the rest of the time, the repo was being cloned, or the actual tasks were executing). We would likely never define a single job this large, but not only did it execute without any problems, the UI handles it very well. + +2,200 agents was the maximum that we were able to test because of quotas on our GCP account that could not easily be increased. + +We also created a job with 5 parallel steps, and triggered 300 parallel builds at once. The jobs executed and finished quickly, across ~1500 agents, with no issues and very little overhead. Interestingly, it seems that we were able to see the effects of our test in Buildkite's status page graphs (see below), but, from a user perspective, we were unable to notice any issues. + +![Status Graphs](../images/0018_buildkite_uptime.png) + +#### Stable + +So far, we have witnessed no stability issues in our testing. + +If Buildkite's status pages are accurate, they seem to be extremely stable, and respond quickly to issues. + +- [Buildkite Status](https://www.buildkitestatus.com/) +- [Historical Uptime](https://www.buildkitestatus.com/uptime) +- [Incident History](https://www.buildkitestatus.com/history) + +For agents, stability and availability will depend primarily on the infrastructure that we build and the availability of the cloud provider (GCP, primarily) running our agents. Since [we control our agents](#elastic-buildkite-agent-manager), we will be able to run agents across multiple zones, and possibly regions, in GCP for increased availability. + +They have a [99.95% uptime SLA](https://buildkite.com/enterprise) for Enterprise customers. + +#### Surfaces information intuitively + +The Buildkite UI is very easy to use, and works as expected. Here is some of the information surfaced for each build: + +- The overall status of the job, as well as which steps succeeded and failed. +- Logs for each individual step +- The timeline for each individual step, including how long it took Buildkite to schedule/handle the job on their end +- Artifacts uploaded by each step +- The entire agent/job configuration at the time the step executed, expressed as environment variables + +![Example Build](../images/0018_buildkite_build.png) + +Note that dependencies between steps are mostly not shown in the UI. See screenshot below for an example. There are several layers of dependencies between all of the steps in this pipeline. The only one that is shown is the final step (`Post All`), which executes after all steps beforehand are finished. There are some other strategies to help organize the steps (such as the new grouping functionality) if we need. + +![Dependencies](../images/0018_buildkite_deps.png) + +Buildkite has rich build page customization via "annotations" which will let us surface custom information. See the [customization section](#customization-1). + +#### Pipelines + +- [Buildkite pipelines](https://buildkite.com/docs/pipelines) must be defined as code. Even if you configure them through the UI, you still have to do so using yaml. +- This is subjective, but the yaml syntax for pipelines is friendly and straightforward. We feel that it will be easy for teams to create and modify pipelines with minimal instructions. +- If your pipeline is configured to use yaml stored in your repo for its definition, branches and PRs will use the version in their source by default. This means that PRs that change the pipeline can be tested as part of the PR CI. +- Top-level pipeline configurations, i.e. basically a pointer to a repo that has the real pipeline yaml in it, can be configured via the UI, API, or terraform. + +#### Advanced Pipeline logic + +Buildkite supports very advanced pipeline logic, and has support for generating dynamic pipeline definitions at runtime. + +- [Conditionals](https://buildkite.com/docs/pipelines/conditionals) +- [Dependencies](https://buildkite.com/docs/pipelines/dependencies) with lots of options, including being optional/conditional +- [Retries](https://buildkite.com/docs/pipelines/command-step#retry-attributes), both automatic and manual, including configuring retry conditions by different exit codes +- [Dynamic pipelines](https://buildkite.com/docs/pipelines/defining-steps#dynamic-pipelines) - pipelines can be generated by running a script at runtime +- [Metadata](https://buildkite.com/docs/pipelines/build-meta-data) can be set in one step, and read in other steps +- [Artifacts](https://buildkite.com/docs/pipelines/artifacts) can be uploaded from and downloaded in steps, and are visible in the UI +- [Parallelism and Concurrency](https://buildkite.com/docs/tutorials/parallel-builds) settings + +Here's an example of a dynamically-generated pipeline based on user input that runs a job `RUN_COUNT` times (from user input), across up to a maximum of 25 agents at once: + +```yaml +# pipeline.yml + +steps: + - input: 'Test Suite Runner' + fields: + - select: 'Test Suite' + key: 'test-suite' + required: true + options: + - label: 'Default CI Group 1' + value: 'default:cigroup:1' + - label: 'Default CI Group 2' + value: 'default:cigroup:2' + - text: 'Number of Runs' + key: 'run-count' + required: true + default: 75 + - wait + - command: .buildkite/scripts/flaky-test-suite-runner.sh | buildkite-agent pipeline upload + label: ':pipeline: Upload' +``` + +```bash +#!/usr/bin/env bash + +# flaky-test-suite-runner.sh + +set -euo pipefail + +TEST_SUITE="$(buildkite-agent meta-data get 'test-suite')" +export TEST_SUITE + +RUN_COUNT="$(buildkite-agent meta-data get 'run-count')" +export RUN_COUNT + +UUID="$(cat /proc/sys/kernel/random/uuid)" +export UUID + +cat << EOF +steps: + - command: | + echo 'Bootstrap' + label: Bootstrap + agents: + queue: bootstrap + key: bootstrap + - command: | + echo 'Build Default Distro' + label: Build Default Distro + agents: + queue: bootstrap + key: default-build + depends_on: bootstrap + - command: 'echo "Running $TEST_SUITE"; sleep 10;' + label: 'Run $TEST_SUITE' + agents: + queue: ci-group + parallelism: $RUN_COUNT + concurrency: 25 + concurrency_group: '$UUID' + depends_on: default-build +EOF +``` + +#### Cloud-friendly pricing model + +Buildkite is priced using a per-user model, where a user is effectively an Elastic employee triggering builds for Kibana via PR, merging code, or through the Buildkite UI. That means that the cost essentially grows with our company size. Most importantly, we don't need to make CI pipeline design decisions based on the Buildkite pricing model. + +However, since we manage our own agents, we will still pay for our compute usage, and will need to consider that cost when designing our pipelines. + +#### Public access + +Buildkite has read-only public access, configurable for each pipeline. An organization can contain a mix of both public and private pipelines. + +There are not fine-grained settings for this, and all information in the build is publicly accessible. + +#### Secrets handling + +[Managing Pipeline Secrets](https://buildkite.com/docs/pipelines/secrets) + +Because agents run on customers' infrastructure, secrets can stay completely in the customer's environment. For this reason, Buildkite doesn't provide a real mechanism for storing secrets, and instead provide recommendations for accessing secrets in pipelines in secure ways. + +There are two recommended methods for handling secrets: using a third-party secrets service like Vault or GCP's Secret Manager, or baking them into agent images and only letting certain jobs access them. Since Elastic already uses Vault, we could utilize Vault the same way we do in Jenkins today. + +Also, a new experimental feature, [redacted environment variables](https://buildkite.com/docs/pipelines/managing-log-output#redacted-environment-variables) can automatically redact the values of environment variables that match some configurable suffixes if they are accidentally written to the console. This would only redact environment variables that were set prior to execution of a build step, e.g. during the `environment` or `pre-command` hooks, and not variables that were created during execution, e.g. by accessing Vault in the middle of a build step. + +#### Support or Documentation + +[Buildkite's documentation](https://buildkite.com/docs/pipelines) is extensive and well-written, as mentioned earlier. + +Besides this, [Enterprise](https://buildkite.com/enterprise) customers get 24/7 emergency help, prioritized support, a dedicated chat channel, and guaranteed response times. They will also consult on best practices, etc. + +#### Scheduled Builds + +[Buildkite has scheduled build](https://buildkite.com/docs/pipelines/scheduled-builds) support with a cron-like syntax. Schedules are defined separately from the pipeline yaml, and can be managed via the UI, API, or terraform. + +#### Container support + +Since we will manage our own agents with Buildkite, we have full control over the container management tools we install and use. In particular, this means that we can easily use modern container tooling, such as Docker with Buildkit, and we can pre-cache layers or other data in our agent images. + +[Buildkite maintains](https://buildkite.com/docs/tutorials/docker-containerized-builds) two officially-supported plugins for making it easier to create pipelines using containers: [one for Docker](https://github.com/buildkite-plugins/docker-buildkite-plugin) and [one for Docker Compose](https://github.com/buildkite-plugins/docker-compose-buildkite-plugin). + +The Docker plugin is essentially a wrapper around `docker run` that makes it easier to define steps that run in containers, while setting various flags. It also provides some logging, and provides mechanisms for automatically propagating environment variables or mounting the workspace into the container. + +A simple, working example for running Jest tests using a container is below. The `Dockerfile` contains all dependencies for CI, and runs `yarn kbn bootstrap` so that it contains a full environment, ready to run tasks. + +```yaml +steps: + - command: | + export DOCKER_BUILDKIT=1 && \ + docker build -t gcr.io/elastic-kibana-184716/buildkite/ci/base:$BUILDKITE_COMMIT -f .ci/Dockerfile . --progress plain && \ + docker push gcr.io/elastic-kibana-184716/buildkite/ci/base:$BUILDKITE_COMMIT + - wait + - command: node scripts/jest --ci --verbose --maxWorkers=6 + label: 'Jest' + artifact_paths: target/junit/**/*.xml + plugins: + - docker#v3.8.0: + image: 'gcr.io/elastic-kibana-184716/buildkite/ci/base:$BUILDKITE_COMMIT' + propagate-environment: true + mount-checkout: false + parallelism: 2 + timeout_in_minutes: 120 +``` + +### Desired + +#### Customization + +We have very large CI pipelines which generate a lot of information (bundle sizes, performance numbers, etc). Being able to attach this information to builds, so that it lives with the builds in the CI system, is highly desirable. The alternative is building custom reports and UIs outside of the system. + +[Annotations](https://buildkite.com/docs/agent/v3/cli-annotate) provide a way to add rich, well-formatted, custom information to build pages using CommonMark Markdown. There are several built-in CSS classes for formatting and several visual styles. Images, emojis, and links can be embedded as well. Just for some examples: Metrics such as bundle sizes, links to the distro builds for that build, and screenshots for test failures could all be embedded directly into the build pages. + +The structure of logs can also be easily customized by adding [collapsible groups](https://buildkite.com/docs/pipelines/managing-log-output#collapsing-output) for log messages. + +#### Core functionality is first-party + +There's a large number of [plugins for Buildkite](https://buildkite.com/plugins), but, so far, there are only two plugins we've been considering using (one for Docker and one for test results), and they're both maintained by Buildkite. All other functionality we've assessed that we need is either built directly into Buildkite, or [we are building it](#what-we-will-build-and-manage). + +#### First-class support for test results + +Buildkite doesn't really have any built-in support specifically for handling test results. Test result reports (e.g. JUnit) can be uploaded as artifacts, and test results can be rendered on the build page using annotations. They have [a plugin](https://github.com/buildkite-plugins/junit-annotate-buildkite-plugin) for automatically annotating builds with test results from JUnit reports in a simple fashion. We would likely want to build our own annotation for this. + +This does mean that Buildkite lacks test-related features of other CI systems: tracking tests over time across build, flagging flaky tests, etc. We would likely need to ingest test results into Elasticsearch and build out Kibana dashboards/visualizations for this, or similar. + +#### GitHub Integration + +Buildkite's [GitHub Integration](https://buildkite.com/docs/integrations/github) can trigger builds based on GitHub webhooks (e.g. on commit/push for branches and PRs), and update commit statuses. Buildkite also adds basic information to build pages, such as links to commits on GitHub and links to PRs. This should cover what we need for tracked branch builds. + +However, for Pull Requests, because we have a lot of requirements around when builds should run and who can run them, we will need to [build a solution](#elastic-buildkite-pr-bot) for handling PRs ourselves. The work for this is already close to complete. + +## What we will build and manage + +### Elastic Buildkite Agent Manager + +#### Overview + +Currently, with Buildkite, the agent lifecycle is managed entirely by customers. Customers can run "static" workers that are online all of the time, or dynamically scale their agents up and down as needed. + +For AWS, Buildkite maintains an auto-scaling solution called [Elastic CI Stack for AWS](https://github.com/buildkite/elastic-ci-stack-for-aws). + +Since, we primarily need support for GCP, we built our own agent manager. It's not 100% complete, but has been working very well during our testing/evaluation of Buildkite, and can handle 1000s of agents. + +[Elastic Buildkite Agent Manager](https://github.com/brianseeders/buildkite-agent-manager) + +Features: + +- Handles many different agent configurations with one instance +- Configures long-running agents, one-time use agents, and agents that will terminate after being idle for a configured amount of time +- Configures both minimum and maximum agent limits - i.e. can ensure a certain number of agents are always online, even if no jobs currently require them +- Supports overprovisioning agents by a percentage or a fixed number +- Supports many GCE settings: zone, image/image family, machine type, disk type and size, tags, metadata, custom startup scripts +- Agent configuration is stored in a separate repo and read at runtime +- Agents are gracefully replaced (e.g. after they finish their current job) if they are running using an out-of-date agent configuration that can affect the underlying GCE instance +- Detect and remove orphaned GCP instances +- Handles 1000s of agents (tested with 2200 before we hit GCP quotas) +- Does instance creation/deletion in large, parallel batches so that demand spikes are handled quickly + +Also planned: + +- Balance creating agents across numerous GCP zones for higher availability +- Automatically gracefully replace agents if disk usage gets too high +- Scaling idle timeouts: e.g. the first agent for a configuration might have an idle timeout of 1 hour, but the 200th might be 5 minutes + +#### Design + +The agent manager is primarily concerned with ensuring that, given an agent configuration, the number of online agents for that configuration is **greater than or equal to** the desired number. Buildkite then determines how to use the agents: which jobs they should execute and when they should go offline (due to being idle, done with jobs, etc). Even when stopping agents due to having an outdated configuration, Buildkite still determines the actual time that the agent should disconnect. + +The current version of the agent manager only handles GCP-based agents, but support for other platforms could be added as well, such as AWS or Kubernetes. There's likely more complexity in managing all of the various agent images than in maintaining support in the agent manager. + +It is also designed to itself be stateless, so that it is easy to deploy and reason about. State is effectively stored in GCP and Buildkite. + +![High-Level Design](../images/0018_agent_manager.png) + +The high-level design for the agent manager is pretty straightforward. There are three primary stages during execution: + +1. Gather Current State + 1. Data and agent configuration is gathered from various sources/APIs in parallel +2. Create Plan + 1. Given the current state across the various services, a plan is created based on agent configurations, current Buildkite job queue sizes, and current GCE instances. + 2. Instances need to be created when there aren't enough online/in-progress agents of a particular configuration to satisfy the needs of its matching queue. + 3. Agents need to be stopped when the agents have been online for too long (based on their configuration) or when their configuration is out-of-date. This is a soft stop, they will terminate after finishing their current job. + 4. Instances need to be deleted if they have been stopped (which happens when their agent stops), or when they have been online past their hard stop time (based on configuration). +3. Execute Plan + 1. The different types of actions in the plan are executed in parallel. Instance creating and deleting is done in batches to handle spikes quickly. + +An error at any step, e.g. when checking current state of GCP instances, will cause the rest of the run to abort. + +Because the service gathers data about the total current state and creates a plan based on that state each run, it's reasonably resistant to errors and it's self-healing. + +##### Protection against creating too many instances + +Creating too many instances in GCP could be costly, so it is worth mentioning here. Since the agent manager itself is stateless, and only looks at the current, external state when determining an execution plan, there is the possibility of creating too many instances. + +There are two primary mechanisms to protect against this: + +One is usage of GCP quotas. Maintaining reasonable GCP quotas will ensure that we don't create too many instances in a situation where something goes catastrophically wrong during operation. It's an extra failsafe. + +The other is built into the agent manager. The agent manager checks both the number of connected agents in Buildkite for a given configuration, as well as the number of instances currently running and being created in GCP. It uses whichever number is greater as the current number of instances. + +This is a simple failsafe, but means that a large number of unnecessary instances should only be able to be created in a pretty specific scenario (keep in mind that errors will abort the current agent manager run): + +- The GCP APIs (both read and create) are returning success codes +- The GCP API for listing instances is returning partial/missing/erroneous data, with a success code +- GCP instances are successfully being created +- Created GCP instances are unable to connect to Buildkite, or Buildkite Agents API is returning partial/missing/erroneous data + +All of these things would need to be true at the same time for a large number of instances to be created. In the unlikely event that that were to happen, the GCP quotas would still be in-place. + +#### Configuration + +Here's an example configuration, which would likely reside in the `master` branch of the kibana repository. + +```js +{ + gcp: { + // Configurations at this level are defaults for all configurations defined under `agents` + project: 'elastic-kibana-184716', + zone: 'us-central1-b', + serviceAccount: 'elastic-buildkite-agent@elastic-kibana-184716.iam.gserviceaccount.com', + agents: [ + { + queue: 'default', + name: 'kibana-buildkite', + overprovision: 0, // percentage or flat number + minimumAgents: 1, + maximumAgents: 500, + gracefulStopAfterSecs: 60 * 60 * 6, + hardStopAfterSecs: 60 * 60 * 9, + idleTimeoutSecs: 60 * 60, + exitAfterOneJob: false, + imageFamily: 'kibana-bk-dev-agents', + machineType: 'n2-standard-1', + diskType: 'pd-ssd', + diskSizeGb: 75 + }, + { + // ... + }, + } +} +``` + +#### Build / Deploy + +Currently, the agent manager is built and deployed using [Google Cloud Build](https://cloud.google.com/build). It is deployed to and hosted using [GKE Auto-Pilot](https://cloud.google.com/blog/products/containers-kubernetes/introducing-gke-autopilot) (Kubernetes). GKE was used, rather than Cloud Run, primarily because the agent manager runs continuously (with a 30sec pause between executions) whereas Cloud Run is for services that respond to HTTP requests. + +It uses [Google Secret Manager](https://cloud.google.com/secret-manager) for storing/retrieving tokens for accessing Buildkite. It uses a GCP service account and [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) to manage GCP resources. + +### Elastic Buildkite PR Bot + +#### Overview + +For TeamCity, we built a bot that was going to handle webhooks from GitHub and trigger builds for PRs based on configuration, user permissions, etc. Since we will not be moving to TeamCity, we've repurposed this bot for Buildkite, since Buildkite does not support all of our requirements around triggering builds for PRs out-of-the-box. The bot supports everything we currently use in Jenkins, and has some additional features as well. + +[Elastic Buildkite PR Bot](https://github.com/elastic/buildkite-pr-bot) + +Features supported by the bot: + +- Triggering builds on commit / when the PR is opened +- Triggering builds on comment +- Permissions for who can trigger builds based on: Elastic org membership, write and/or admin access to the repo, or user present in an allowed list +- Limit builds to PRs targeting a specific branch +- Custom regex for trigger comment, e.g. "buildkite test this" +- Triggering builds based on labels +- Setting labels, comment body, and other PR info as env vars on triggered build +- Skip triggering build if a customizable label is present +- Option to set commit status on trigger +- Capture custom arguments from comment text using capture groups and forward them to the triggered build + +#### Configuration + +The configuration is stored in a `json` file (default: `.ci/pull-requests.json`) in the repo for which pull requests will be monitored. Multiple branches in the repo can store different configurations, or one configuration (e.g. in `master`) can cover the entire repo. + +Example configuration: + +```json +{ + "jobs": [ + { + "repoOwner": "elastic", + "repoName": "kibana", + "pipelineSlug": "kibana", + + "enabled": true, + "target_branch": "master", + "allow_org_users": true, + "allowed_repo_permissions": ["admin", "write"], + "allowed_list": ["renovate[bot]"], + "set_commit_status": true, + "commit_status_context": "kibana-buildkite", + "trigger_comment_regex": "^(?:(?:buildkite\\W+)?(?:build|test)\\W+(?:this|it))|^retest$" + } + ] +} +``` + +Github Webhooks must also be configured to send events to the deployed bot. + +#### Build / Deploy + +Currently, the bot is built and deployed using [Google Cloud Build](https://cloud.google.com/build). It is deployed to and hosted on [Google Cloud Run](https://cloud.google.com/run). It uses [Google Secret Manager](https://cloud.google.com/secret-manager) for storing/retrieving tokens for accessing GitHub and Buildkite. + +[Build/deploy configuration](https://github.com/elastic/buildkite-pr-bot/blob/main/cloudbuild.yaml) + +### Infrastructure + +We will need to maintain our infrastructure related to Buildkite, primarily ephemeral agents. To start, it will mean supporting infrastructure in GCP, but could later mean AWS as well. + +- Separate GCP project for CI resources +- Hosting for bots/services we maintain, such as the Agent Manager (GKE Auto-Pilot) and GitHub PR bot (Cloud Run) +- Google Storage Buckets for CI artifacts +- Networking (security, we may also need Cloud NAT) +- IAM and Security +- Agent images + +We are already using Terraform to manage most resources related to Buildkite, and will continue to do so. + +### Monitoring / Alerting + +We will need to set up and maintain monitoring and alerting for our GCP infrastructure, as well as Buildkite metrics. + +Some examples: + +GCP + +- Number of instances by type +- Age of instances +- Resource Quotas + +Buildkite + +- Agent queues +- Job wait times +- Build status + +### Agent Image management + +We will need to maintain images used to create GCP instances for our Buildkite agents. These images would need to be built on a regular basis (daily, or possibly more often). + +We could likely maintain a single linux-based image to cover all of our current CI needs. However, in the future, if we need to maintain many images across different operating systems and architectures, this is likely to become the most complex part of the CI system that we would need to maintain. Every operating system and architecture we need to support adds another group of required images, with unique dependencies and configuration automation. + +Another thing to note: Just because we need to run something on a specific OS or architecture, it doesn't necessarily mean we need to maintain an agent image for it. For example, we might use something like Vagrant to create a separate VM, using the default, cloud-provided images, that we run something on (e.g. for testing system packages), rather than running it on the same machine as the agent. In this case, we would potentially only be managing a small number of images, or even a single image. + +Also, we always have the option of running a small number of jobs using Jenkins, if we need to do so to target additional OSes and architectures. + +For our testing, we have a single GCP image, [built using Packer](https://github.com/elastic/kibana/tree/kb-bk/.buildkite/agents/packer), with the Buildkite agent installed and all of our dependencies. + +Summary of Responsibilities + +- An automated process for creating new images, at least daily, running automated smoke tests against them, and promoting them +- Delete old images when creating new ones +- Ability to roll back images easily and/or pin specific image versions +- Manage dependencies, failures, updates, etc across all supported OSes and architectures, on a regular basis + +### Buildkite org-level settings management + +There are a few settings outside of pipelines that we will need to manage. + +- Top-level pipelines and their settings +- Pipeline schedules / scheduled jobs +- Public visibility of pipelines +- Teams and Permissions +- Single Sign On settings + +Most of the content for our pipelines will be stored in repositories as YAML. However, a job still must exist in Buildkite that points to that repo and that YAML. For managing those top-level configurations, an official [Terraform provider](https://registry.terraform.io/providers/buildkite/buildkite/latest/docs/resources/pipeline) exists, which we will likely take advantage of. + +Pipeline schedules can also be managed using the Terraform provider. + +Teams can also be managed using Terraform, but it's unlikely we will need to use Teams. + +For everything else, we will likely start off using UI and build automation (or contribute to the Terraform provider) where we see fit. Most of the other settings are easy to configure, and unlikely to change. + +### IT Security Processes + +There will likely be numerous IT Security processes we will need to follow, since we will be managing infrastructure. This could include regular audits, specific software and configurations that must be baked into our agents, documentation procedures, or other conditions that we will need to satisfy. There is risk here, as the processes and workload are currently unknown to us. + +# Drawbacks + +The biggest drawback to doing this is that we will be duplicating a large amount of work and providing/maintaining a service that is already provided to us by another team at Elastic. Jenkins is already provided to us, and there is automation for creating Jenkins worker images and managing worker instances in both AWS and GCP, and IT Security policies are already being handled for all of this. It is hard to predict what the extra workload will be for the Kibana Operations team if we move our CI processes to Buildkite, but we know we will have to maintain all of the things listed under [What we will build and manage](#what-we-will-build-and-manage). + +Some other drawbacks: + +- CI Pipelines and other jobs built in Jenkins will need to be re-built, which includes building support for things like CI Stats, Slack notifications, GitHub PR comments, etc. +- Developers will need to learn a new system. +- The service is an additional cost to the company. +- There is a lot of Jenkins knowledge throughout the company, but likely little Buildkite knowledge. + +# Alternatives + +## Jenkins + +We are not happy with the experience provided by our instance of Jenkins and our current pipelines. If we stick with Jenkins, we will need to invest a likely significant amount of time in improving the experience and making our pipelines scale given the limitations we face. + +### Required + +#### Scalable + +Our current Jenkins instance only allows for 300-400 connected agents, before effectively going offline. We have struggled with this issue for several years, and completely redesigned our pipelines around this limitation. The resulting design, which involves running 20+ tasks in parallel on single, large machines, and managing all of the concurrency ourselves, is complicated and problematic. + +Other teams at Elastic, especially over the last few months, have been experiencing this same limitation with their Jenkins instances as well. The team that manages Jenkins at Elastic is well aware of this issue, and is actively investigating. It is currently unknown whether or not it is a solvable problem (without sharding) or a limitation of Jenkins. + +#### Stable + +Firstly, Jenkins was not designed for high availability. If the primary/controller goes offline, CI is offline. + +The two biggest sources of stability issues for us are currently related to scaling (see above) and updates. + +##### Updates + +The typical update process for Jenkins looks like this: + +- Put Jenkins into shutdown mode, which stops any new builds from starting +- Wait for all currently-running jobs to finish +- Shutdown Jenkins +- Do the update +- Start Jenkins + +For us, shutdown mode also means that `gobld` stops creating new agents for our jobs. This means that many running jobs will never finish executing while shutdown mode is active. + +So, for us, the typical update process is: + +- Put Jenkins into shutdown mode, which stops any new builds from starting, and many from finishing +- Hard kill all of our currently running jobs +- Shutdown Jenkins +- Do the update +- Start Jenkins +- A human manually restarts CI for all PRs that were running before the update + +This is pretty disruptive for us, as developers have to wait several hours longer before merging or seeing the status of their PRs, plus there is manual work that must be done to restart CI. If we stay with Jenkins, we'll need to fix this process, and likely build some automation for it. + +#### Surfaces information intuitively + +Our pipelines are very complex, mainly because of the issues mentioned above related to designing around scaling issues, and none of the UIs in Jenkins work well for us. + +The [Stage View](https://kibana-ci.elastic.co/job/elastic+kibana+pipeline-pull-request) only works for very simple pipelines. Even if we were able to re-design our pipelines to populate this page better, there are just too many stages to display in this manner. + +[Blue Ocean](https://kibana-ci.elastic.co/blue/organizations/jenkins/elastic%2Bkibana%2Bpipeline-pull-request/activity), which is intended to be the modern UI for Jenkins, doesn't work at all for our pipelines. We have nested parallel stages in our pipelines, which [are not supported](https://issues.jenkins.io/browse/JENKINS-54010). + +[Pipeline Steps](https://kibana-ci.elastic.co/job/elastic+kibana+pipeline-pull-request/) (Choose a build -> Pipeline Steps) shows information fairly accurately (sometimes logs/errors are not attached to any steps, and do not show), but is very difficult to read. There are entire pages of largely irrelevant information (Setting environment variables, starting a `try` block, etc), which is difficult to read through, especially developers who don't interact with Jenkins every day. + +![Pipeline Steps](../images/0018_jenkins_pipeline_steps.png) + +We push a lot of information to GitHub and Slack, and have even built custom UIs, to try to minimize how much people need to interact directly with Jenkins. In particular, when things go wrong, it is very difficult to investigate using the Jenkins UI. + +#### Pipelines + +Jenkins supports pipeline-as-code through [Pipelines](https://www.jenkins.io/doc/book/pipeline), which we currently use. + +Pros: + +- Overall pretty powerful, pipelines execute Groovy code at runtime, so pipelines can do a lot and can be pretty complex, if you're willing to write the code +- Pipeline changes can be tested in PRs +- Shared Libraries allow shared code to be used across pipelines easily + +Cons: + +- The sandbox is pretty difficult to work with. There's a [hard-coded list](https://github.com/jenkinsci/script-security-plugin/tree/e99ba9cffb0502868b05d19ef5cd205ca7e0e5bd/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists) of allowed methods for pipelines. Other methods must be approved separately, or put in a separate shared repository that runs trusted code. +- Pipeline code is serialized by Jenkins, and the serialization process leads to a lot of issues that are difficult to debug and reason about. See [JENKINS-44924](https://issues.jenkins.io/browse/JENKINS-44924) - `List.sort()` doesn't work and silently returns `-1` instead of a list +- Reasonably complex pipelines are difficult to view in the UI ([see above](#surfaces-information-intuitively-2)) +- Using Pipelines to manage certain configurations (such as Build Parameters) requires running an outdated job once and letting it fail to update it +- Jobs that reference a pipeline have to be managed separately. Only third-party tools exist for managing these jobs as code (JJB and Job DSL). +- Very difficult to test code without running it live in Jenkins + +#### Advanced Pipeline logic + +See above section. Jenkins supports very advanced pipeline logic using scripted pipelines and Groovy. + +#### Cloud-friendly pricing model + +Given that Jenkins is open-source, we pay only for infrastructure and people to manage it. + +#### Public access + +- Fine-grained authorization settings +- Anonymous user access +- Per-job authorization, so some jobs can be private + +#### Secrets handling + +- Supports [Credentials](https://www.jenkins.io/doc/book/using/using-credentials/), which are stored encrypted on disk and have authorization settings + - Credentials are difficult to manage in an automated way +- Pipeline support for accessing credentials +- Credentials masked in log output +- Support for masking custom values in log output + +#### Support or Documentation + +Documentation for Jenkins is notoriously fragmented. All major functionality is provided in plugins, and documentation is spread out across the Jenkins Handbook, the CloudBees website, JIRA issues, wikis, GitHub repos, JavaDoc pages. Many plugins have poor documentation, and source code often has to be read to understand how to configure something. + +CloudBees offers paid support, but we're not familiar with it at this time. + +#### Scheduled Builds + +Jenkins supports scheduled builds via a Cron-like syntax, and can spread scheduled jobs out. For example, if many jobs are scheduled to run every day at midnight, a syntax is available that will automatically spread the triggered jobs evenly out across the midnight hour. + +#### Container support + +Jenkins has support for using Docker to [run containers for specific stages in a Pipeline](https://www.jenkins.io/doc/book/pipeline/docker/). It is effectively a wrapper around `docker run`. There are few conveniences, and figuring out how to do things like mount the workspace into the container is left up to the user. There are also gotchas that are not well-documented, such as the fact that the user running inside the container will be automatically changed using `-u`, which can cause issues. + +Though we have control over the agents running our jobs at Elastic, and thus all of the container-related tooling, it is not currently easy for the Operations team to manage our container tooling. We are mostly dependent on another team to do this for us. + +### Desired + +#### Customization + +The only way to customize information added to build pages is through custom plugins. [Creating and maintaining plugins for Jenkins](https://www.jenkins.io/doc/developer/plugin-development/) is a fairly significant investment, and we do not currently have a good way to manage plugins for Jenkins instances at Elastic. It's a pretty involved process that, at the moment, has to be done by another team. + +Given that, we feel we would be able to build a higher-quality experience in less time by creating custom applications separate from Jenkins, which we have actually [done in the past](https://ci.kibana.dev/es-snapshots). + +#### Core functionality is first-party + +Jenkins is very modular, and almost all Jenkins functionality is provided by plugins. + +It's difficult to understand which plugins are required to support which base features. For example, Pipelines support is provided by a group of many plugins, and many of them have outdated names ([Pipeline: Nodes and Processes](https://github.com/jenkinsci/workflow-durable-task-step-plugin) is actually a plugin called `workflow-durable-task-step-plugin`). + +Many plugins are maintained by CloudBees employees, but it can be very difficult to determine which ones are, without knowing the names of CloudBees employees. All Jenkins community/third-party plugins reside under the `jenkinsci` organization in GitHub, which makes finding "official" ones difficult. Given the open source nature of the Jenkins ecosystem and the way that development is handled by Cloudbees, it might be incorrect to say that any plugins outside of the Cloudbees plugins (for the Cloudbees Jenkins distribution) are "first-party". + +#### First-class support for test results + +It's a bit buggy at times (for example, if you run the same test multiple times, you have to load pages in a specific order to see the correct results in the UI), but Jenkins does have support for ingesting and displaying test results, including graphs that show changes over time. We use this feature to ingest test results from JUnit files produced by unit tests, integration tests, and end-to-end/functional tests. + +#### GitHub Integration + +Jenkins has rich support for GitHub spread across many different plugins. It can trigger builds in response to webhook payloads, automatically create jobs for repositories in an organization, has support for self-hosted GitHub, and has many settings for triggering pull requests. + +It's worth mentioning, however, that we've had and continue to have many issues with these integrations. For example, the GitHub Pull Request Builder plugin, which currently provides PR triggering for us and other teams, has been the source of several issues at Elastic. It's had performance issues, triggers builds erroneously, and has been mostly unmaintained for several years. + +## Other solutions + +### CircleCI + +CircleCI is a mature, widely-used option that is scalable and fulfills a lot of our requirements. We felt that we could create a good CI experience with this solution, but it had several disadvantages for us compared to Buildkite: + +- The pricing model for self-hosted runners felt punishing for breaking CI into smaller tasks +- Public access to build pages is gated behind a login, and gives CircleCI access to your private repos by default +- There are no customization options for adding information to build pages +- Options for advanced pipeline logic are limited compared to other solutions + +### GitHub Actions + +GitHub Actions is an interesting option, but it didn't pass our initial consideration round for one main reason: scalability. + +To ensure we're able to run the number of parallel tasks that we need to run, we'll have to use self-hosted runners. Self-hosted runners aren't subject to concurrency limits. However, managing auto-scaling runners seems to be pretty complex at the moment, and GitHub doesn't seem to have any official guidance on how to do it. + +Also, even with self-hosted runners, there is a 1,000 API request per hour hard limit, though it does not specify which APIs. Assuming even that 1 parallel step in a job is one API request, given the large number of small tasks that we'd like to split our CI into, we will likely hit this limit pretty quickly. + +# Adoption strategy + +We have already done a lot of the required legwork to begin building and running pipelines in Buildkite, including getting approval from various business groups inside Elastic. After all business groups have signed off, and a deal has been signed with Buildkite, we can begin adopting Buildkite. A rough plan outline is below. It's not meant to be a full migration plan. + +- Build minimal supporting services, automation, and pipelines to migrate a low-risk job from Jenkins to Buildkite (e.g. "Baseline" CI for tracked branches) + - The following will need to exist (some of which has already been built) + - New GCP project for infrastructure, with current implementations migrated + - Agent Manager + - Agent image build/promote + - Slack notifications for failures (possibly utilize Buildkite's built-in solution) + - The Buildkite pipeline and supporting code + - Run the job in parallel with Jenkins until we have confidence that it's working well + - Turn off the Jenkins version +- Build, test, migrate the next low-risk pipelines: ES Snapshot and/or Flaky Test Suite Runner +- Build, test, migrate tracked branch pipelines +- Build, test, migrate PR pipelines + - Will additionally need PR comment support + - PR pipelines are the most disruptive if there are problems, so we should have a high level of confidence before migrating + +# How we teach this + +The primary way that developers interact with Jenkins/CI today is through pull requests. Since we push a lot of information to pull requests via comments, developers mostly only need to interact with Jenkins when something goes wrong. + +The Buildkite UI is simple and intuitive enough that, even without documentation, there would likely be a pretty small learning curve to navigating the build page UI that will be linked from PR comments. That's not to say we're not going to provide documentation, we just think it would be easy even without it! + +We would also like to provide simple documentation that will guide developers through setting up new pipelines without our help. Getting a new job up and running with our current Jenkins setup is a bit complicated for someone who hasn't done it before, and there isn't good documentation for it. We'd like to change that if we move to Buildkite. + +To teach and inform, we will likely do some subset of these things: + +- Documentation around new CI pipelines in Buildkite +- Documentation on how to handle PR failures using Buildkite +- Documentation on the new infrastructure, supporting services, etc. +- Zoom sessions with walkthrough and Q&A +- E-mail announcement with links to documentation +- Temporarily add an extra message to PR comments, stating the change and adding links to relevant documentation diff --git a/rfcs/text/0018_timeslider.md b/rfcs/text/0018_timeslider.md new file mode 100644 index 0000000000000..aa8e7263260d2 --- /dev/null +++ b/rfcs/text/0018_timeslider.md @@ -0,0 +1,217 @@ +- Start Date: 2020-04-26 +- RFC PR: (leave this empty) +- Kibana Issue: (leave this empty) + +--- +- [1. Summary](#1-summary) +- [2. Detailed design](#2-detailed-design) +- [3. Unresolved questions](#3-unresolved-questions) + +# 1. Summary + +A timeslider is a UI component that allows users to intuitively navigate through time-based data. + +This RFC proposes adding a timeslider control to the Maps application. + +It proposes a two phased roll-out in the Maps application. The [design proposal](#2-detailed-design) focuses on the first phase. + +Since the timeslider UI is relevant for other Kibana apps, the implementation should be portable. We propose to implement the control as a React-component +without implicit dependencies on Kibana-state or Maps-state. + +The RFC details the integration of this control in Maps. It includes specific consideration to how timeslider affects data-refresh in the context of Maps. + +This RFC also outlines a possible integration of this Timeslider-React component with an Embeddable, and the introduction of a new piece of embeddable-state `Timeslice`. + +This RFC does not address how this component should _behave_ in apps other than the Maps-app. + +# 2. Detailed design + +Below outlines: +- the two delivery phases intended for Kibana Maps +- outline of the Timeslider UI component implementation (phase 1) +- outline of the integration in Maps of the Timeslider UI component (phase 1) + +## 2.1 Design phases overview + + + +### 2.1.1 Time-range selection and stepped navigation + +A first phase includes arbitrary time-range selection and stepped navigation. + +![Timeslider version 1](../images/timeslider/v1.png) + +This is the focus of this RFC. + +Check [https://github.com/elastic/kibana/pull/96791](https://github.com/elastic/kibana/pull/96791) for a POC implementation. + +### 2.2.2 Data distribution preview with histogram and playback + +A second phase adds a date histogram showing the preview of the data. + +![Timeslider version 2](../images/timeslider/v2.png) + +Details on this phase 2 are beyond the scope of this RFC. + +## 2.2 The timeslider UI React-component (phase 1) + +This focuses on Phase 1. Phase 2, with date histogram preview and auto-playback is out of scope for now. + +### 2.2.1 Interface of the React-component + +The core timeslider-UI is a React-component. + +The component has no implicit dependencies on any Kibana-state or Maps-store state. + +Its interface is fully defined by its `props`-contract. + +``` +export type Timeslice = { + from: number; // epoch timestamp + to: number; // epoch timestamp +}; + +export interface TimesliderProps { + onTimesliceChanged: (timeslice: Timeslice) => void; + timerange: TimeRange; // TimeRange The start and end time of the entire time-range. TimeRange is defined in `data/common` + timeslice?: Timeslice; // The currently displayed timeslice. Needs to be set after onTimesliceChange to be reflected back in UI. If ommitted, the control selects the first timeslice. +} +``` + +`timeslice` is clamped to the bounds of `timeRange`. + +Any change to `timeslice`, either by dragging the handles of the timeslider, or pressing the back or forward buttons, calls the `onTimesliceChanged` handler. + +Since the initial use is inside Maps, the initial location of this React-component is inside the Maps plugin, `x-pack/plugins/maps/public/timeslider`. + +Nonetheless, this UI-component should be easily "cut-and-pastable" to another location. + +### 2.2.2 Internals + +The timeslider automatically subdivides the timerange with equal breaks that are heuristically determined. + +It assigns about 6-10 breaks within the `timerange`, snaps the "ticks" to a natural "pretty date" using `calcAutoIntervalNear` from `data/common`. + +For example; +- a `timerange` of 8.5 days, it would assign a 8 day-steps, plus some padding on either end, depending on the entire `timerange`. +- a `timerange` of 6.9 years would snap to year-long step, plus some padding on either end, depending on the entire `timerange`. + +The slider itself is a ``. + +### 2.2.2 End-user behavior + +- the user can manipulate the `timeslice`-double ranged slider to any arbitrary range within `timerange`. +- the user can press the forward and back buttons for a stepped navigation. The range of the current time-slice is preserved when there is room for the `timeslice` within the `timerange`. + - when the user has _not modified the width_ of the `timeslice`, using the buttons means stepping through the pre-determined ticks (e.g. by year, by day, ...) + - when the user has _already modified the width_ of the `timeslice`, it means stepping through the `timerange`, with a stride of the width of the `timeslice`. +- the `timeslice` "snaps" to the beginning or end (depending on direction) of the `timerange`. In practice, this means the `timeslice` will collaps or reduce in width. + +## 2.3 Maps integration of the timeslider React component + +This control will be integrated in the Maps-UI. + +Maps is Redux-based, so `timeslice` selection and activation/de-activation all propagates to the Redux-store. + +#### 2.3.1 Position in the UI + +The timeslider control is enabled/disabled by the timeslider on/off toggle-button in the main toolbar. + +![Timeslider version 1](../images/timeslider/toolbar.png) + + +#### 2.3.2 Layer interactions + + +Enabling the Timeslider will automatically retrigger refresh of _all_ time-based layers to the currently selected `timeslice`. + +The Layer-TOC will indicate which layer is currently "time-filtered" by the timeslider. + +On a layer-per-layer basis, users will be able to explicitly opt-out if they should be governed by the timerange or not. This is an existing toggle in Maps already. +This is relevant for having users add contextual layers that should _not_ depend on the time. + + +#### 2.3.3 Omitting timeslider on a dashboard + +Maps will not display the timeslider-activation button on Maps that are embedded in a Dashboard. + +We believe that a Timeslider-embeddable would be a better vehicle to bring timeslider-functionality do Dashboards. See also the [last section](#3-unresolved-questions). + +#### 2.3.3 Data-fetch considerations + +--- +**NOTE** + +The below section is very Maps-specific, although similar challenges would be present in other applications as well. + +Some of these considerations will not generalize to all of Kibana. + +The main ways that Maps distinguishes in data-fetch from other use-cases: + - the majority of the data-fetching for layers in Maps depends on the scale and extent. Ie. different data is requested based on the current zoom-level and current-extent of the Map. So for example, even if two views share the same time, query and filter-state, if their extent and/or scale is different, their requests to ES will be different. + - for some layer-types, Maps will fetch individual documents, rather than the result of an aggregation. + +--- + +Data-fetch for timeslider should be responsive and smooth. A user dragging the slider should have an immediate visual result on the map. + +In addition, we expect the timeslider will be used a lot for "comparisons". For example, imagine a user stepping back&forth between timeslices. + +For this reason, apps using a timeslider (such as Maps) ideally: +- pre-fetch data when possible +- cache data when possible + +For Maps specifically, when introducing timeslider, layers will therefore need to implement time-based data fetch based on _two_ pieces of state +- the entire `timerange` (aka. the global Kibana timerange) +- the selected `timeslice` (aka. the `timeslice` chosen by the user using the UI-component) + +##### 2.3.3.1 Pre-fetching individual documents and masking of data + +ES-document layers (which display individual documents) can prefetch all documents within the entire `timerange`, when the total number of docs is below some threshold. In the context of Maps, this threshold is the default index-search-size of the index. + +Maps can then just mask data on the map based on some filter-expression. The evaluation of this filter-expression is done by mapbox-gl is fast because it occurs on the GPU. There is immediate visual feedback to the user as they manipulate the timeslider, because it does not require a roundtrip to update the data. + +##### 2.3.3.2 Caching of aggregated searches + +Aggregated data can be cached on the client, so toggling between timeslices can avoid a round-trip data-fetch. +The main technique here is for layers to use `.mvt`-data format to request data. Tiled-based data can be cached client-side + +We do _not_ propose _pre-fetching_ of aggregated data in this initial phase of the Maps timeslider effort. There is a couple reasons: +- Based on the intended user-interactions for timeslider, because a user can flexibly select a `timeslice` of arbitrary widths, it would be really hard to determine how to determine which timeslices to aggregate up front. +- Maps already strains the maximum bucket sizes it can retrieve from Elasticsearch. Cluster/grid-layers often push up to 10k or more buckets, and terms-aggregations for choropleth maps also is going up to 10k buckets. Prefetching this for timeslices (e.g. say x10 timeslices) would easily exceed the default bucket limit sizes of Elasticsearch. + + +##### 2.3.3.3 Decouple data-fetch from UI-effort + +Apart from refactoring the data-fetch for layers to now use two pieces of time-based state, the implementation will decouple any data-fetch considerations from the actual timeslider-UI work. + +The idea is that dial in data-fetch optimizations can be dialed into Maps in a parallel work-thread, not necessarily dependent on any changes or additions to the UI. +Any optimizations would not only affect timeslider users, but support all interaction patterns that require smooth data-updates (e.g. panning back&forth to two locations, toggling back&forth between two filters, ...) + +The main effort to support efficient data-fetch in a maps-context is to use `.mvt` as the default data format ([https://github.com/elastic/kibana/issues/79868](https://github.com/elastic/kibana/issues/79868)). This is a stack-wide effort in collaboration with the Elasticsearch team ([https://github.com/elastic/elasticsearch/issues/58696](https://github.com/elastic/elasticsearch/issues/58696), which will add `.mvt` as a core response format to Elasticsearch. + +Growing the use of `mvt`([https://docs.mapbox.com/vector-tiles/reference/](https://docs.mapbox.com/vector-tiles/reference/)) in Maps will help with both pre-fetching and client-side caching: +- `mvt` is a binary format which allows more data to be packed inside, as compared to Json. Multiple tiles are patched together, so this introduces a form of parallelization as well. Due to growing the amount of data inside a single tile, and due to the parallelization, Maps has a pathway to increase the number of features that can be time-filtered. +- Because vector tiles have fixed extents and scales (defined by a `{x}/{y}/{scale}`-tuple), this type of data-fetching allows tiles to be cached on the client. This cache can be the implicit browser disc-cache, or the transient in-mem cache of mapbox-gl. Using mvt thus provides a pathway for fast toggling back&forth between timeslices, without round-trips to fetch data. + + +##### 2.3.3.4 timeslider and async search + +It is unclear on what the practical uses for async-search would be in the context of a timeslider-control in Maps. + +Timeslider is a highly interactive control that require immediate visual feedback. We also do not intend to activate timeslider in Maps on a Dashboard (see above). + +End-users who need to view a dashboard with a long-running background search will need to manipulate the _global Kibana time picker_ to select the time-range, and will not be able to use the timeslider to do so. + +# 3. Unresolved questions + +## Making Timeslider a Kibana Embeddable + +This below is a forward looking section. It is a proposal of how the Timeslider-UI can be exposed as an Embeddable, when that time should come. + +We expect a few steps: +- This would require the extraction of the timeslider React-component out of Maps into a separate plugin. As outlined above, this migration should be fairly straightforward, a "cut and paste". +- It would require the creation of a `TimesliderEmbeddable` which wraps this UI-component. +- It would also require the introduction of a new piece of embeddable-state, `Timeslice`, which can be controlled by the `TimesliderEmbeddable`. +We believe it is important to keep `timeslice` and `timerange` separate, as individual apps and other embeddables will have different mechanism to efficiently fetch data and respond to changes in `timeslice` and/or `timerange`. + +Having timeslider as a core Embeddable likely provides a better pathway to integrate timeslider-functionality in Dashboards or apps other than Maps. + diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 2a202a426c16c..b2eec43cc5ad7 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -347,7 +347,7 @@ export class DocLinksService { snapshotRestoreRepos: `${PLUGIN_DOCS}repository.html`, }, snapshotRestore: { - guide: `${ELASTICSEARCH_DOCS}snapshot-restore.html`, + guide: `${KIBANA_DOCS}snapshot-repositories.html`, changeIndexSettings: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html#change-index-settings-during-restore`, createSnapshot: `${ELASTICSEARCH_DOCS}snapshots-take-snapshot.html`, registerSharedFileSystem: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-filesystem-repository`, diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index d0623de51e4c3..c2e0476960c3b 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -456,6 +456,7 @@ export const openPit = ( export interface ReadWithPit { outdatedDocuments: SavedObjectsRawDoc[]; readonly lastHitSortValue: number[] | undefined; + readonly totalHits: number | undefined; } /* @@ -481,13 +482,20 @@ export const readWithPit = ( pit: { id: pitId, keep_alive: pitKeepAlive }, size: batchSize, search_after: searchAfter, - // Improve performance by not calculating the total number of hits - // matching the query. - track_total_hits: false, + /** + * We want to know how many documents we need to process so we can log the progress. + * But we also want to increase the performance of these requests, + * so we ask ES to report the total count only on the first request (when searchAfter does not exist) + */ + track_total_hits: typeof searchAfter === 'undefined', query, }, }) .then((response) => { + const totalHits = + typeof response.body.hits.total === 'number' + ? response.body.hits.total // This format is to be removed in 8.0 + : response.body.hits.total?.value; const hits = response.body.hits.hits; if (hits.length > 0) { @@ -495,12 +503,14 @@ export const readWithPit = ( // @ts-expect-error @elastic/elasticsearch _source is optional outdatedDocuments: hits as SavedObjectsRawDoc[], lastHitSortValue: hits[hits.length - 1].sort as number[], + totalHits, }); } return Either.right({ outdatedDocuments: [], lastHitSortValue: undefined, + totalHits, }); }) .catch(catchRetryableEsClientErrors); diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index bdaedba9c9ea3..adeb78e568af3 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -45,6 +45,7 @@ import { createInitialState, model } from './model'; import { ResponseType } from './next'; import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; import { TransformErrorObjects, TransformSavedObjectDocumentError } from '../migrations/core'; +import { createInitialProgress } from './progress'; describe('migrations v2 model', () => { const baseState: BaseState = { @@ -768,6 +769,8 @@ describe('migrations v2 model', () => { expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_READ'); expect(newState.sourceIndexPitId).toBe('pit_id'); expect(newState.lastHitSortValue).toBe(undefined); + expect(newState.progress.processed).toBe(undefined); + expect(newState.progress.total).toBe(undefined); }); }); @@ -783,6 +786,7 @@ describe('migrations v2 model', () => { lastHitSortValue: undefined, corruptDocumentIds: [], transformErrors: [], + progress: createInitialProgress(), }; it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_INDEX if the index has outdated documents to reindex', () => { @@ -791,21 +795,34 @@ describe('migrations v2 model', () => { const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({ outdatedDocuments, lastHitSortValue, + totalHits: 1, }); const newState = model(state, res) as ReindexSourceToTempIndex; expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_INDEX'); expect(newState.outdatedDocuments).toBe(outdatedDocuments); expect(newState.lastHitSortValue).toBe(lastHitSortValue); + expect(newState.progress.processed).toBe(undefined); + expect(newState.progress.total).toBe(1); + expect(newState.logs).toMatchInlineSnapshot(` + Array [ + Object { + "level": "info", + "message": "Starting to process 1 documents.", + }, + ] + `); }); it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_CLOSE_PIT if no outdated documents to reindex', () => { const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({ outdatedDocuments: [], lastHitSortValue: undefined, + totalHits: undefined, }); const newState = model(state, res) as ReindexSourceToTempClosePit; expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT'); expect(newState.sourceIndexPitId).toBe('pit_id'); + expect(newState.logs).toStrictEqual([]); // No logs because no hits }); it('REINDEX_SOURCE_TO_TEMP_READ -> FATAL if no outdated documents to reindex and transform failures seen with previous outdated documents', () => { @@ -817,12 +834,14 @@ describe('migrations v2 model', () => { const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({ outdatedDocuments: [], lastHitSortValue: undefined, + totalHits: undefined, }); const newState = model(testState, res) as FatalState; expect(newState.controlState).toBe('FATAL'); expect(newState.reason).toMatchInlineSnapshot( `"Migrations failed. Reason: Corrupt saved object documents: a:b. To allow migrations to proceed, please delete these documents."` ); + expect(newState.logs).toStrictEqual([]); // No logs because no hits }); }); @@ -857,6 +876,7 @@ describe('migrations v2 model', () => { lastHitSortValue: undefined, corruptDocumentIds: [], transformErrors: [], + progress: { processed: undefined, total: 1 }, }; const processedDocs = [ { @@ -869,8 +889,28 @@ describe('migrations v2 model', () => { const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ processedDocs, }); - const newState = model(state, res); + const newState = model(state, res) as ReindexSourceToTempIndexBulk; + expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK'); + expect(newState.progress.processed).toBe(0); // Result of `(undefined ?? 0) + corruptDocumentsId.length` + }); + + it('increments the progress.processed counter', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ + processedDocs, + }); + + const testState = { + ...state, + outdatedDocuments: [{ _id: '1', _source: { type: 'vis' } }], + progress: { + processed: 1, + total: 1, + }, + }; + + const newState = model(testState, res) as ReindexSourceToTempIndexBulk; expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK'); + expect(newState.progress.processed).toBe(2); }); it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded but we have carried through previous failures', () => { @@ -886,6 +926,7 @@ describe('migrations v2 model', () => { expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ'); expect(newState.corruptDocumentIds.length).toEqual(1); expect(newState.transformErrors.length).toEqual(0); + expect(newState.progress.processed).toBe(0); }); it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ when response is left documents_transform_failed', () => { @@ -918,6 +959,7 @@ describe('migrations v2 model', () => { sourceIndexPitId: 'pit_id', targetIndex: '.kibana_7.11.0_001', lastHitSortValue: undefined, + progress: createInitialProgress(), }; test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded', () => { const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.right( @@ -1018,6 +1060,7 @@ describe('migrations v2 model', () => { hasTransformedDocs: false, corruptDocumentIds: [], transformErrors: [], + progress: createInitialProgress(), }; it('OUTDATED_DOCUMENTS_SEARCH_READ -> OUTDATED_DOCUMENTS_TRANSFORM if found documents to transform', () => { @@ -1026,21 +1069,65 @@ describe('migrations v2 model', () => { const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_READ'> = Either.right({ outdatedDocuments, lastHitSortValue, + totalHits: 10, }); const newState = model(state, res) as OutdatedDocumentsTransform; expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_TRANSFORM'); expect(newState.outdatedDocuments).toBe(outdatedDocuments); expect(newState.lastHitSortValue).toBe(lastHitSortValue); + expect(newState.progress.processed).toBe(undefined); + expect(newState.progress.total).toBe(10); + expect(newState.logs).toMatchInlineSnapshot(` + Array [ + Object { + "level": "info", + "message": "Starting to process 10 documents.", + }, + ] + `); + }); + + it('keeps the previous progress.total if not obtained in the result', () => { + const outdatedDocuments = [{ _id: '1', _source: { type: 'vis' } }]; + const lastHitSortValue = [123456]; + const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_READ'> = Either.right({ + outdatedDocuments, + lastHitSortValue, + totalHits: undefined, + }); + const testState = { + ...state, + progress: { + processed: 5, + total: 10, + }, + }; + const newState = model(testState, res) as OutdatedDocumentsTransform; + expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_TRANSFORM'); + expect(newState.outdatedDocuments).toBe(outdatedDocuments); + expect(newState.lastHitSortValue).toBe(lastHitSortValue); + expect(newState.progress.processed).toBe(5); + expect(newState.progress.total).toBe(10); + expect(newState.logs).toMatchInlineSnapshot(` + Array [ + Object { + "level": "info", + "message": "Processed 5 documents out of 10.", + }, + ] + `); }); it('OUTDATED_DOCUMENTS_SEARCH_READ -> OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT if no outdated documents to transform', () => { const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_READ'> = Either.right({ outdatedDocuments: [], lastHitSortValue: undefined, + totalHits: undefined, }); const newState = model(state, res) as OutdatedDocumentsSearchClosePit; expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT'); expect(newState.pitId).toBe('pit_id'); + expect(newState.logs).toStrictEqual([]); // No logs because no hits }); it('OUTDATED_DOCUMENTS_SEARCH_READ -> FATAL if no outdated documents to transform and we have failed document migrations', () => { @@ -1060,6 +1147,7 @@ describe('migrations v2 model', () => { const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_READ'> = Either.right({ outdatedDocuments: [], lastHitSortValue: undefined, + totalHits: undefined, }); const transformErrorsState: OutdatedDocumentsSearchRead = { ...state, @@ -1072,6 +1160,7 @@ describe('migrations v2 model', () => { expect(newState.reason.includes('Corrupt saved object documents: ')).toBe(true); expect(newState.reason.includes('Transformation errors: ')).toBe(true); expect(newState.reason.includes('randomvis: 7.12.0')).toBe(true); + expect(newState.logs).toStrictEqual([]); // No logs because no hits }); }); @@ -1138,6 +1227,7 @@ describe('migrations v2 model', () => { pitId: 'pit_id', lastHitSortValue: [3, 4], hasTransformedDocs: false, + progress: createInitialProgress(), }; describe('OUTDATED_DOCUMENTS_TRANSFORM if action succeeds', () => { const processedDocs = [ @@ -1156,6 +1246,7 @@ describe('migrations v2 model', () => { expect(newState.transformedDocs).toEqual(processedDocs); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); + expect(newState.progress.processed).toBe(outdatedDocuments.length); }); test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ if there are are existing documents that failed transformation', () => { const outdatedDocumentsTransformStateWithFailedDocuments: OutdatedDocumentsTransform = { @@ -1172,6 +1263,7 @@ describe('migrations v2 model', () => { expect(newState.corruptDocumentIds).toEqual(corruptDocumentIds); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); + expect(newState.progress.processed).toBe(outdatedDocuments.length); }); test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ if there are are existing documents that failed transformation because of transform errors', () => { const outdatedDocumentsTransformStateWithFailedDocuments: OutdatedDocumentsTransform = { @@ -1189,6 +1281,7 @@ describe('migrations v2 model', () => { expect(newState.transformErrors.length).toEqual(1); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); + expect(newState.progress.processed).toBe(outdatedDocuments.length); }); }); describe('OUTDATED_DOCUMENTS_TRANSFORM if action fails', () => { @@ -1204,6 +1297,7 @@ describe('migrations v2 model', () => { ) as OutdatedDocumentsSearchRead; expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); expect(newState.corruptDocumentIds).toEqual(corruptDocumentIds); + expect(newState.progress.processed).toBe(outdatedDocuments.length); }); test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ combines newly failed documents with those already on state if documents failed the transform', () => { const newFailedTransformDocumentIds = ['b:other', 'c:__']; @@ -1226,6 +1320,7 @@ describe('migrations v2 model', () => { ...corruptDocumentIds, ...newFailedTransformDocumentIds, ]); + expect(newState.progress.processed).toBe(outdatedDocuments.length); }); }); }); @@ -1246,6 +1341,7 @@ describe('migrations v2 model', () => { pitId: 'pit_id', lastHitSortValue: [3, 4], hasTransformedDocs: false, + progress: createInitialProgress(), }; test('TRANSFORMED_DOCUMENTS_BULK_INDEX should throw a throwBadResponse error if action failed', () => { const res: ResponseType<'TRANSFORMED_DOCUMENTS_BULK_INDEX'> = Either.left({ diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index cf9d6aec6b5b0..3ef3cb4f83b6f 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -18,6 +18,12 @@ import { SavedObjectsMigrationVersion } from '../types'; import { disableUnknownTypeMappingFields } from '../migrations/core/migration_context'; import { excludeUnusedTypesQuery, TransformErrorObjects } from '../migrations/core'; import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; +import { + createInitialProgress, + incrementProcessedProgress, + logProgress, + setProgressTotal, +} from './progress'; /** * A helper function/type for ensuring that all control state's are handled. @@ -509,6 +515,7 @@ export const model = (currentState: State, resW: ResponseType): // placeholders to collect document transform problems corruptDocumentIds: [], transformErrors: [], + progress: createInitialProgress(), }; } else { throwBadResponse(stateP, res); @@ -517,12 +524,16 @@ export const model = (currentState: State, resW: ResponseType): // we carry through any failures we've seen with transforming documents on state const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { + const progress = setProgressTotal(stateP.progress, res.right.totalHits); + const logs = logProgress(stateP.logs, progress); if (res.right.outdatedDocuments.length > 0) { return { ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX', outdatedDocuments: res.right.outdatedDocuments, lastHitSortValue: res.right.lastHitSortValue, + progress, + logs, }; } else { // we don't have any more outdated documents and need to either fail or move on to updating the target mappings. @@ -542,6 +553,7 @@ export const model = (currentState: State, resW: ResponseType): return { ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT', + logs, }; } } @@ -566,12 +578,18 @@ export const model = (currentState: State, resW: ResponseType): // collecting issues along the way rather than failing // REINDEX_SOURCE_TO_TEMP_INDEX handles the document transforms const res = resW as ExcludeRetryableEsError>; + + // Increment the processed documents, no matter what the results are. + // Otherwise the progress might look off when there are errors. + const progress = incrementProcessedProgress(stateP.progress, stateP.outdatedDocuments.length); + if (Either.isRight(res)) { if (stateP.corruptDocumentIds.length === 0 && stateP.transformErrors.length === 0) { return { ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK', // handles the actual bulk indexing into temp index transformedDocs: [...res.right.processedDocs], + progress, }; } else { // we don't have any transform issues with the current batch of outdated docs but @@ -581,6 +599,7 @@ export const model = (currentState: State, resW: ResponseType): return { ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_READ', + progress, }; } } else { @@ -592,6 +611,7 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'REINDEX_SOURCE_TO_TEMP_READ', corruptDocumentIds: [...stateP.corruptDocumentIds, ...left.corruptDocumentIds], transformErrors: [...stateP.transformErrors, ...left.transformErrors], + progress, }; } else { // should never happen @@ -676,6 +696,7 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', pitId: res.right.pitId, lastHitSortValue: undefined, + progress: createInitialProgress(), hasTransformedDocs: false, corruptDocumentIds: [], transformErrors: [], @@ -687,11 +708,16 @@ export const model = (currentState: State, resW: ResponseType): const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { if (res.right.outdatedDocuments.length > 0) { + const progress = setProgressTotal(stateP.progress, res.right.totalHits); + const logs = logProgress(stateP.logs, progress); + return { ...stateP, controlState: 'OUTDATED_DOCUMENTS_TRANSFORM', outdatedDocuments: res.right.outdatedDocuments, lastHitSortValue: res.right.lastHitSortValue, + progress, + logs, }; } else { // we don't have any more outdated documents and need to either fail or move on to updating the target mappings. @@ -720,6 +746,11 @@ export const model = (currentState: State, resW: ResponseType): } } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_TRANSFORM') { const res = resW as ExcludeRetryableEsError>; + + // Increment the processed documents, no matter what the results are. + // Otherwise the progress might look off when there are errors. + const progress = incrementProcessedProgress(stateP.progress, stateP.outdatedDocuments.length); + if (Either.isRight(res)) { // we haven't seen corrupt documents or any transformation errors thus far in the migration // index the migrated docs @@ -729,6 +760,7 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX', transformedDocs: [...res.right.processedDocs], hasTransformedDocs: true, + progress, }; } else { // We have seen corrupt documents and/or transformation errors @@ -736,6 +768,7 @@ export const model = (currentState: State, resW: ResponseType): return { ...stateP, controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', + progress, }; } } else { @@ -747,6 +780,7 @@ export const model = (currentState: State, resW: ResponseType): corruptDocumentIds: [...stateP.corruptDocumentIds, ...res.left.corruptDocumentIds], transformErrors: [...stateP.transformErrors, ...res.left.transformErrors], hasTransformedDocs: false, + progress, }; } else { throwBadResponse(stateP, res as never); diff --git a/src/core/server/saved_objects/migrationsv2/progress.test.ts b/src/core/server/saved_objects/migrationsv2/progress.test.ts new file mode 100644 index 0000000000000..a0d89c2c63300 --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/progress.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright 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 { MigrationLog } from './types'; +import { + createInitialProgress, + incrementProcessedProgress, + logProgress, + setProgressTotal, +} from './progress'; + +describe('createInitialProgress', () => { + test('create initial progress', () => { + expect(createInitialProgress()).toStrictEqual({ + processed: undefined, + total: undefined, + }); + }); +}); + +describe('setProgressTotal', () => { + const previousProgress = { + processed: undefined, + total: 10, + }; + test('should keep the previous total if not provided', () => { + expect(setProgressTotal(previousProgress)).toStrictEqual(previousProgress); + }); + + test('should keep the previous total is undefined', () => { + expect(setProgressTotal(previousProgress, undefined)).toStrictEqual(previousProgress); + }); + + test('should overwrite if the previous total is provided', () => { + expect(setProgressTotal(previousProgress, 20)).toStrictEqual({ + processed: undefined, + total: 20, + }); + }); +}); + +describe('logProgress', () => { + const previousLogs: MigrationLog[] = []; + + test('should not log anything if there is no total', () => { + const progress = { + processed: undefined, + total: undefined, + }; + expect(logProgress(previousLogs, progress)).toStrictEqual([]); + }); + + test('should not log anything if total is 0', () => { + const progress = { + processed: undefined, + total: 0, + }; + expect(logProgress(previousLogs, progress)).toStrictEqual([]); + }); + + test('should log the "Starting..." log', () => { + const progress = { + processed: undefined, + total: 10, + }; + expect(logProgress(previousLogs, progress)).toStrictEqual([ + { + level: 'info', + message: 'Starting to process 10 documents.', + }, + ]); + }); + + test('should log the "Processed..." log', () => { + const progress = { + processed: 5, + total: 10, + }; + expect(logProgress(previousLogs, progress)).toStrictEqual([ + { + level: 'info', + message: 'Processed 5 documents out of 10.', + }, + ]); + }); +}); + +describe('incrementProcessedProgress', () => { + const previousProgress = { + processed: undefined, + total: 10, + }; + test('should not increment if the incrementValue is not defined', () => { + expect(incrementProcessedProgress(previousProgress)).toStrictEqual({ + processed: 0, + total: 10, + }); + }); + + test('should not increment if the incrementValue is undefined', () => { + expect(incrementProcessedProgress(previousProgress, undefined)).toStrictEqual({ + processed: 0, + total: 10, + }); + }); + + test('should not increment if the incrementValue is not defined (with some processed values)', () => { + const testPreviousProgress = { + ...previousProgress, + processed: 1, + }; + expect(incrementProcessedProgress(testPreviousProgress, undefined)).toStrictEqual({ + processed: 1, + total: 10, + }); + }); + + test('should increment if the incrementValue is defined', () => { + expect(incrementProcessedProgress(previousProgress, 5)).toStrictEqual({ + processed: 5, + total: 10, + }); + }); + + test('should increment if the incrementValue is defined (with some processed values)', () => { + const testPreviousProgress = { + ...previousProgress, + processed: 5, + }; + expect(incrementProcessedProgress(testPreviousProgress, 5)).toStrictEqual({ + processed: 10, + total: 10, + }); + }); +}); diff --git a/src/core/server/saved_objects/migrationsv2/progress.ts b/src/core/server/saved_objects/migrationsv2/progress.ts new file mode 100644 index 0000000000000..d626cd6528902 --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/progress.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { MigrationLog, Progress } from './types'; + +/** + * Returns an initial state of the progress object (everything undefined) + */ +export function createInitialProgress(): Progress { + return { + processed: undefined, + total: undefined, + }; +} + +/** + * Overwrites the total of the progress if anything provided + * @param previousProgress + * @param total + */ +export function setProgressTotal( + previousProgress: Progress, + total = previousProgress.total +): Progress { + return { + ...previousProgress, + total, + }; +} + +/** + * Returns a new list of MigrationLogs with the info entry about the progress + * @param previousLogs + * @param progress + */ +export function logProgress(previousLogs: MigrationLog[], progress: Progress): MigrationLog[] { + const logs = [...previousLogs]; + + if (progress.total) { + if (typeof progress.processed === 'undefined') { + logs.push({ + level: 'info', + message: `Starting to process ${progress.total} documents.`, + }); + } else { + logs.push({ + level: 'info', + message: `Processed ${progress.processed} documents out of ${progress.total}.`, + }); + } + } + + return logs; +} + +/** + * Increments the processed count and returns a new Progress + * @param previousProgress Previous state of the progress + * @param incrementProcessedBy Amount to increase the processed count by + */ +export function incrementProcessedProgress( + previousProgress: Progress, + incrementProcessedBy = 0 +): Progress { + return { + ...previousProgress, + processed: (previousProgress.processed ?? 0) + incrementProcessedBy, + }; +} diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index f5800a3cd9570..e3e52212d56cb 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -26,6 +26,11 @@ export interface MigrationLog { message: string; } +export interface Progress { + processed: number | undefined; + total: number | undefined; +} + export interface BaseState extends ControlState { /** The first part of the index name such as `.kibana` or `.kibana_task_manager` */ readonly indexPrefix: string; @@ -183,6 +188,7 @@ export interface ReindexSourceToTempRead extends PostInitState { readonly lastHitSortValue: number[] | undefined; readonly corruptDocumentIds: string[]; readonly transformErrors: TransformErrorObjects[]; + readonly progress: Progress; } export interface ReindexSourceToTempClosePit extends PostInitState { @@ -197,6 +203,7 @@ export interface ReindexSourceToTempIndex extends PostInitState { readonly lastHitSortValue: number[] | undefined; readonly corruptDocumentIds: string[]; readonly transformErrors: TransformErrorObjects[]; + readonly progress: Progress; } export interface ReindexSourceToTempIndexBulk extends PostInitState { @@ -204,6 +211,7 @@ export interface ReindexSourceToTempIndexBulk extends PostInitState { readonly transformedDocs: SavedObjectsRawDoc[]; readonly sourceIndexPitId: string; readonly lastHitSortValue: number[] | undefined; + readonly progress: Progress; } export type SetTempWriteBlock = PostInitState & { @@ -252,6 +260,7 @@ export interface OutdatedDocumentsSearchRead extends PostInitState { readonly hasTransformedDocs: boolean; readonly corruptDocumentIds: string[]; readonly transformErrors: TransformErrorObjects[]; + readonly progress: Progress; } export interface OutdatedDocumentsSearchClosePit extends PostInitState { @@ -276,6 +285,7 @@ export interface OutdatedDocumentsTransform extends PostInitState { readonly hasTransformedDocs: boolean; readonly corruptDocumentIds: string[]; readonly transformErrors: TransformErrorObjects[]; + readonly progress: Progress; } export interface TransformedDocumentsBulkIndex extends PostInitState { /** @@ -286,6 +296,7 @@ export interface TransformedDocumentsBulkIndex extends PostInitState { readonly lastHitSortValue: number[] | undefined; readonly hasTransformedDocs: boolean; readonly pitId: string; + readonly progress: Progress; } export interface MarkVersionIndexReady extends PostInitState { diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts index a737bc6a73004..4ce71b24332c1 100644 --- a/src/dev/run_find_plugins_with_circular_deps.ts +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -19,9 +19,7 @@ interface Options { type CircularDepList = Set; -const allowedList: CircularDepList = new Set([ - 'x-pack/plugins/lists -> x-pack/plugins/security_solution', -]); +const allowedList: CircularDepList = new Set([]); run( async ({ flags, log }) => { diff --git a/src/plugins/console/public/lib/autocomplete/engine.js b/src/plugins/console/public/lib/autocomplete/engine.js index 7852c9da7898f..bd72af3c0e8cf 100644 --- a/src/plugins/console/public/lib/autocomplete/engine.js +++ b/src/plugins/console/public/lib/autocomplete/engine.js @@ -146,7 +146,7 @@ export function populateContext(tokenPath, context, editor, includeAutoComplete, if (!wsToUse && walkStates.length > 1 && !includeAutoComplete) { console.info( - "more then one context active for current path, but autocomplete isn't requested", + "more than one context active for current path, but autocomplete isn't requested", walkStates ); } diff --git a/src/plugins/console/server/config.ts b/src/plugins/console/server/config.ts index 90839a18e1210..4e42e3c21d2ad 100644 --- a/src/plugins/console/server/config.ts +++ b/src/plugins/console/server/config.ts @@ -15,8 +15,6 @@ export const config = schema.object( enabled: schema.boolean({ defaultValue: true }), proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }), ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}), - - // This does not actually work, track this issue: https://github.com/elastic/kibana/issues/55576 proxyConfig: schema.arrayOf( schema.object({ match: schema.object({ diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts index 30667d59eff84..14ae24a2a5626 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -105,4 +105,11 @@ describe('createFiltersFromValueClick', () => { expect(rangeFilter.range.bytes.lt).toEqual(2078); } }); + + test('handles non-unique filters', async () => { + const [point] = dataPoints; + const filters = await createFiltersFromValueClickAction({ data: [point, point] }); + + expect(filters.length).toEqual(1); + }); }); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts index fb8240a0cdc27..13cb9ba419bf9 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import _ from 'lodash'; import { Datatable } from '../../../../../plugins/expressions/public'; import { esFilters, Filter } from '../../../public'; import { getIndexPatterns, getSearchService } from '../../../public/services'; @@ -140,5 +141,7 @@ export const createFiltersFromValueClickAction = async ({ }) ); - return esFilters.mapAndFlattenFilters(filters); + return _.uniqWith(esFilters.mapAndFlattenFilters(filters), (a, b) => + esFilters.compareFilters(a, b, esFilters.COMPARE_ALL_OPTIONS) + ); }; diff --git a/src/plugins/data/public/query/filter_manager/lib/generate_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/generate_filter.test.ts index 1a29a57ff058a..0daaf804e7b40 100644 --- a/src/plugins/data/public/query/filter_manager/lib/generate_filter.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/generate_filter.test.ts @@ -174,4 +174,22 @@ describe('Generate filters', () => { [FIELD.name]: ANOTHER_PHRASE, }); }); + + it('should use only distinct values', () => { + const ANOTHER_PHRASE = 'another-value'; + const filters = generateFilters( + mockFilterManager, + FIELD, + [PHRASE_VALUE, ANOTHER_PHRASE, PHRASE_VALUE, ANOTHER_PHRASE], + '', + INDEX_NAME + ); + expect(filters).toHaveLength(2); + expect((filters[0] as PhraseFilter).query.match_phrase).toEqual({ + [FIELD.name]: PHRASE_VALUE, + }); + expect((filters[1] as PhraseFilter).query.match_phrase).toEqual({ + [FIELD.name]: ANOTHER_PHRASE, + }); + }); }); diff --git a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts index 9a52a02edcd4e..0a4998a159523 100644 --- a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts @@ -71,7 +71,7 @@ export function generateFilters( operation: string, index: string ): Filter[] { - values = Array.isArray(values) ? values : [values]; + values = Array.isArray(values) ? _.uniq(values) : [values]; const fieldObj = (_.isObject(field) ? field : { diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 343ea9ef7f03c..69687f75f3098 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -362,14 +362,21 @@ describe('Execution', () => { }); test('result is undefined until execution completes', async () => { + jest.useFakeTimers(); const execution = createExecution('sleep 10'); expect(execution.state.get().result).toBe(undefined); execution.start(null).subscribe(jest.fn()); expect(execution.state.get().result).toBe(undefined); - await new Promise((r) => setTimeout(r, 1)); + + jest.advanceTimersByTime(1); + await new Promise(process.nextTick); expect(execution.state.get().result).toBe(undefined); - await new Promise((r) => setTimeout(r, 11)); + + jest.advanceTimersByTime(10); + await new Promise(process.nextTick); expect(execution.state.get().result).toBe(null); + + jest.useRealTimers(); }); test('handles functions returning observables', () => { diff --git a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts index 9577f25184ba0..70f180d7cb5f2 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts @@ -11,11 +11,11 @@ import { DocLinksStart } from 'src/core/public'; export const getLinks = (docLinks: DocLinksStart) => { const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; - const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; const painlessDocsBase = `${docsBase}/elasticsearch/painless/${DOC_LINK_VERSION}`; + const kibanaDocsBase = `${docsBase}/kibana/${DOC_LINK_VERSION}`; return { - runtimePainless: `${esDocsBase}/runtime.html#runtime-mapping-fields`, + runtimePainless: `${kibanaDocsBase}/managing-index-patterns.html#runtime-fields`, painlessSyntax: `${painlessDocsBase}/painless-lang-spec.html`, }; }; diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index 51344e2d28ab6..55e10e7861e51 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -34,14 +34,14 @@ export interface Props { value: string; /** Function invoked when text in editor is changed */ - onChange: (value: string) => void; + onChange: (value: string, event: monaco.editor.IModelContentChangedEvent) => void; /** * Options for the Monaco Code Editor * Documentation of options can be found here: - * https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.ieditorconstructionoptions.html + * https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html */ - options?: monaco.editor.IEditorConstructionOptions; + options?: monaco.editor.IStandaloneEditorConstructionOptions; /** * Suggestion provider for autocompletion diff --git a/src/plugins/kibana_react/public/code_editor/index.tsx b/src/plugins/kibana_react/public/code_editor/index.tsx index 635e84b1d8c20..2440974c3b1d1 100644 --- a/src/plugins/kibana_react/public/code_editor/index.tsx +++ b/src/plugins/kibana_react/public/code_editor/index.tsx @@ -16,7 +16,7 @@ import { import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { useUiSetting } from '../ui_settings'; -import type { Props } from './code_editor'; +import { Props } from './code_editor'; const LazyBaseEditor = React.lazy(() => import('./code_editor')); @@ -26,6 +26,8 @@ const Fallback = () => ( ); +export type CodeEditorProps = Props; + /** * Renders a Monaco code editor with EUI color theme. * diff --git a/src/plugins/vis_type_timeseries/common/enums/metric_types.ts b/src/plugins/vis_type_timeseries/common/enums/metric_types.ts index 17352f0f9da25..8e2bc8f346eb6 100644 --- a/src/plugins/vis_type_timeseries/common/enums/metric_types.ts +++ b/src/plugins/vis_type_timeseries/common/enums/metric_types.ts @@ -27,6 +27,7 @@ export enum METRIC_TYPES { // We should probably use BUCKET_TYPES from data plugin in future. export enum BUCKET_TYPES { TERMS = 'terms', + FILTERS = 'filters', } export const EXTENDED_STATS_TYPES = [ diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts index df0874fdd73ec..ff7b2f497fd70 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts @@ -15,6 +15,18 @@ jest.mock('../../../services', () => { getDataStart: jest.fn(() => { return { indexPatterns: jest.fn(), + query: { + timefilter: { + timefilter: { + getTime: jest.fn(() => { + return { + from: '2021-04-30T16:42:24.502Z', + to: '2021-05-05T14:42:24.502Z', + }; + }), + }, + }, + }, }; }), }; @@ -25,9 +37,12 @@ describe('convert series to datatables', () => { beforeEach(() => { const fieldMap: Record = { - test1: { name: 'test1', spec: { type: 'date' } } as IndexPatternField, - test2: { name: 'test2' } as IndexPatternField, - test3: { name: 'test3', spec: { type: 'boolean' } } as IndexPatternField, + test1: { name: 'test1', spec: { type: 'date', name: 'test1' } } as IndexPatternField, + test2: { + name: 'test2', + spec: { type: 'number', name: 'Average of test2' }, + } as IndexPatternField, + test3: { name: 'test3', spec: { type: 'boolean', name: 'test3' } } as IndexPatternField, }; const getFieldByName = (name: string): IndexPatternField | undefined => fieldMap[name]; @@ -41,8 +56,8 @@ describe('convert series to datatables', () => { describe('addMetaColumns()', () => { test('adds the correct meta to a date column', () => { - const columns = [{ id: 0, name: 'test1', isSplit: false }]; - const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'count'); + const columns = [{ id: 0, name: 'test1', isMetric: true, type: 'date_histogram' }]; + const columnsWithMeta = addMetaToColumns(columns, indexPattern); expect(columnsWithMeta).toEqual([ { id: '0', @@ -54,6 +69,13 @@ describe('convert series to datatables', () => { enabled: true, indexPatternId: 'index1', type: 'date_histogram', + schema: 'metric', + params: { + timeRange: { + from: '2021-04-30T16:42:24.502Z', + to: '2021-05-05T14:42:24.502Z', + }, + }, }, type: 'date', }, @@ -63,8 +85,8 @@ describe('convert series to datatables', () => { }); test('adds the correct meta to a non date column', () => { - const columns = [{ id: 1, name: 'Average of test2', isSplit: false }]; - const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'avg'); + const columns = [{ id: 1, name: 'test2', isMetric: true, type: 'avg' }]; + const columnsWithMeta = addMetaToColumns(columns, indexPattern); expect(columnsWithMeta).toEqual([ { id: '1', @@ -76,17 +98,21 @@ describe('convert series to datatables', () => { enabled: true, indexPatternId: 'index1', type: 'avg', + schema: 'metric', + params: { + field: 'Average of test2', + }, }, type: 'number', }, - name: 'Average of test2', + name: 'test2', }, ]); }); test('adds the correct meta for a split column', () => { - const columns = [{ id: 2, name: 'test3', isSplit: true }]; - const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'avg'); + const columns = [{ id: 2, name: 'test3', isMetric: false, type: 'terms' }]; + const columnsWithMeta = addMetaToColumns(columns, indexPattern); expect(columnsWithMeta).toEqual([ { id: '2', @@ -98,6 +124,10 @@ describe('convert series to datatables', () => { enabled: true, indexPatternId: 'index1', type: 'terms', + schema: 'group', + params: { + field: 'test3', + }, }, type: 'boolean', }, diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts index 35506f9e62081..19a1910afbe2f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts @@ -6,51 +6,74 @@ * Side Public License, v 1. */ import { IndexPattern } from 'src/plugins/data/public'; -import { - Datatable, - DatatableRow, - DatatableColumn, - DatatableColumnType, -} from 'src/plugins/expressions/public'; +import { DatatableRow, DatatableColumn, DatatableColumnType } from 'src/plugins/expressions/public'; +import { Query } from 'src/plugins/data/common'; import { TimeseriesVisParams } from '../../../types'; import type { PanelData } from '../../../../common/types'; +import { BUCKET_TYPES } from '../../../../common/enums'; import { fetchIndexPattern } from '../../../../common/index_patterns_utils'; import { getDataStart } from '../../../services'; import { X_ACCESSOR_INDEX } from '../../visualizations/constants'; +import type { TSVBTables } from './types'; -interface TSVBTables { - [key: string]: Datatable; +interface FilterParams { + filter?: Query; + label?: string; + field?: string; } interface TSVBColumns { id: number; name: string; - isSplit: boolean; + isMetric: boolean; + type: string; + params?: FilterParams[]; } export const addMetaToColumns = ( columns: TSVBColumns[], - indexPattern: IndexPattern, - metricsType: string + indexPattern: IndexPattern ): DatatableColumn[] => { return columns.map((column) => { const field = indexPattern.getFieldByName(column.name); const type = (field?.spec.type as DatatableColumnType) || 'number'; + + let params: unknown = { + field: field?.spec.name, + }; + if (column.type === BUCKET_TYPES.FILTERS && column.params) { + const filters = column.params.map((col) => ({ + input: col.filter, + label: col.label, + })); + params = { + filters, + }; + } else if (column.type === 'date_histogram') { + const { query } = getDataStart(); + const timeRange = query.timefilter.timefilter.getTime(); + params = { + timeRange, + }; + } + const cleanedColumn = { id: column.id.toString(), name: column.name, meta: { type, - field: column.name, + field: field?.spec.name, index: indexPattern.title, source: 'esaggs', sourceParams: { enabled: true, indexPatternId: indexPattern?.id, - type: type === 'date' ? 'date_histogram' : column.isSplit ? 'terms' : metricsType, + type: column.type, + schema: column.isMetric ? 'metric' : 'group', + params, }, }, - }; + } as DatatableColumn; return cleanedColumn; }); }; @@ -73,30 +96,58 @@ export const convertSeriesToDataTable = async ( usedIndexPattern = indexPattern; } } - const isGroupedByTerms = layer.split_mode === 'terms'; + const isGroupedByTerms = layer.split_mode === BUCKET_TYPES.TERMS; + const isGroupedByFilters = layer.split_mode === BUCKET_TYPES.FILTERS; const seriesPerLayer = series.filter((s) => s.seriesId === layer.id); let id = X_ACCESSOR_INDEX; const columns: TSVBColumns[] = [ - { id, name: usedIndexPattern.timeFieldName || '', isSplit: false }, + { + id, + name: usedIndexPattern.timeFieldName || '', + isMetric: false, + type: 'date_histogram', + }, ]; + if (seriesPerLayer.length) { id++; - columns.push({ id, name: seriesPerLayer[0].splitByLabel, isSplit: false }); - // Adds an extra column, if the layer is split by terms aggregation + const metrics = layer.metrics; + columns.push({ + id, + name: metrics[metrics.length - 1].field || seriesPerLayer[0].splitByLabel, + isMetric: true, + type: metrics[metrics.length - 1].type, + }); + + // Adds an extra column, if the layer is split by terms or filters aggregation if (isGroupedByTerms) { id++; - columns.push({ id, name: layer.terms_field || '', isSplit: true }); + columns.push({ + id, + name: layer.terms_field || '', + isMetric: false, + type: BUCKET_TYPES.TERMS, + }); + } else if (isGroupedByFilters) { + id++; + columns.push({ + id, + name: BUCKET_TYPES.FILTERS, + isMetric: false, + params: layer?.split_filters as FilterParams[], + type: BUCKET_TYPES.FILTERS, + }); } } - const columnsWithMeta = addMetaToColumns(columns, usedIndexPattern, layer.metrics[0].type); - + const columnsWithMeta = addMetaToColumns(columns, usedIndexPattern); + const filtersColumn = columns.find((col) => col.type === BUCKET_TYPES.FILTERS); let rows: DatatableRow[] = []; for (let j = 0; j < seriesPerLayer.length; j++) { const data = seriesPerLayer[j].data.map((rowData) => { const row: DatatableRow = [rowData[0], rowData[1]]; // If the layer is split by terms aggregation, the data array should also contain the split value. - if (isGroupedByTerms) { + if (isGroupedByTerms || filtersColumn) { row.push(seriesPerLayer[j].label); } return row; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.test.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.test.ts new file mode 100644 index 0000000000000..2b9a1f930c15a --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.test.ts @@ -0,0 +1,207 @@ +/* + * Copyright 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 { XYChartSeriesIdentifier, GeometryValue } from '@elastic/charts'; +import { getClickFilterData } from './get_click_filter_data'; +import type { TSVBTables } from './types'; +import { TimeseriesVisParams } from '../../../types'; + +describe('getClickFilterData', () => { + test('gets the correct data for a group by everything timeseries chart', () => { + const points = [ + [ + { + x: 1620032400000, + y: 72, + accessor: 'y1', + datum: [1620032400000, 72], + }, + { + key: + 'groupId{yaxis_50589930-ad98-11eb-b6f6-59011d7388b7_main_group}spec{61ca57f1-469d-11e7-af02-69e470af7417}yAccessor{1}splitAccessors{}', + specId: '61ca57f1-469d-11e7-af02-69e470af7417', + yAccessor: 1, + }, + ], + ] as Array<[GeometryValue, XYChartSeriesIdentifier]>; + const tables = { + '61ca57f1-469d-11e7-af02-69e470af7417': { + type: 'datatable', + rows: [ + [1620010800000, 8], + [1620021600000, 33], + [1620032400000, 72], + [1620043200000, 66], + [1620054000000, 36], + [1620064800000, 11], + ], + columns: [ + { + id: '0', + name: 'timestamp', + meta: { + type: 'date', + field: 'timestamp', + index: 'kibana_sample_data_logs', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', + type: 'date_histogram', + schema: 'group', + params: { + field: 'timestamp', + }, + }, + }, + }, + { + id: '1', + name: 'Count', + meta: { + type: 'number', + index: 'kibana_sample_data_logs', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', + type: 'count', + schema: 'metric', + params: {}, + }, + }, + }, + ], + }, + } as TSVBTables; + const model = { + series: [ + { + id: '61ca57f1-469d-11e7-af02-69e470af7417', + split_mode: 'everything', + }, + ], + } as TimeseriesVisParams; + const data = getClickFilterData(points, tables, model); + expect(data[0].column).toEqual(0); + expect(data[0].row).toEqual(2); + expect(data[0].value).toEqual(points[0][0].x); + }); + + test('gets the correct data for a group by terms timeseries chart', () => { + const points = [ + [ + { + x: 1619481600000, + y: 3, + accessor: 'y1', + datum: [1619481600000, 3], + }, + { + key: + 'groupId{yaxis_6e0353a0-ad9b-11eb-b112-89cce8e43380_main_group}spec{61ca57f1-469d-11e7-af02-69e470af7417:1}yAccessor{1}splitAccessors{}', + specId: '61ca57f1-469d-11e7-af02-69e470af7417:1', + }, + ], + ] as Array<[GeometryValue, XYChartSeriesIdentifier]>; + const tables = { + '61ca57f1-469d-11e7-af02-69e470af7417': { + type: 'datatable', + rows: [ + [1619449200000, 31, 0], + [1619460000000, 36, 0], + [1619470800000, 35, 0], + [1619481600000, 40, 0], + [1619492400000, 38, 0], + [1619503200000, 30, 0], + [1620172800000, 0, 0], + [1619449200000, 4, 1], + [1619460000000, 4, 1], + [1619470800000, 3, 1], + [1619481600000, 3, 1], + [1619492400000, 2, 1], + [1619503200000, 3, 1], + ], + columns: [ + { + id: '0', + name: 'timestamp', + meta: { + type: 'date', + field: 'timestamp', + index: 'kibana_sample_data_flights', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + type: 'date_histogram', + schema: 'group', + params: { + field: 'timestamp', + }, + }, + }, + }, + { + id: '1', + name: 'Count', + meta: { + type: 'number', + index: 'kibana_sample_data_flights', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + type: 'count', + schema: 'metric', + params: {}, + }, + }, + }, + { + id: '2', + name: 'Cancelled', + meta: { + type: 'boolean', + field: 'Cancelled', + index: 'kibana_sample_data_flights', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + type: 'terms', + schema: 'group', + params: { + field: 'Cancelled', + }, + }, + }, + }, + ], + }, + } as TSVBTables; + const model = { + series: [ + { + id: '61ca57f1-469d-11e7-af02-69e470af7417', + split_mode: 'terms', + }, + ], + } as TimeseriesVisParams; + const data = getClickFilterData(points, tables, model); + expect(data.length).toEqual(2); + expect(data[0].column).toEqual(0); + expect(data[0].row).toEqual(10); + expect(data[0].value).toEqual(points[0][0].x); + + expect(data[1].column).toEqual(2); + expect(data[1].row).toEqual(10); + // expect(data).toEqual([]); + const splitValue = points[0][1].specId.split(':'); + expect(data[1].value).toEqual(parseInt(splitValue[1], 10)); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.ts new file mode 100644 index 0000000000000..a6e35aa6e1032 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.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 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 { XYChartSeriesIdentifier, GeometryValue } from '@elastic/charts'; +import { ValueClickContext } from 'src/plugins/embeddable/public'; +import { X_ACCESSOR_INDEX } from '../../visualizations/constants'; +import { BUCKET_TYPES } from '../../../../common/enums'; +import { TimeseriesVisParams } from '../../../types'; +import type { TSVBTables } from './types'; + +export const getClickFilterData = ( + points: Array<[GeometryValue, XYChartSeriesIdentifier]>, + tables: TSVBTables, + model: TimeseriesVisParams +) => { + const data: ValueClickContext['data']['data'] = []; + points.forEach((point) => { + const [geometry] = point; + const { specId } = point[1]; + // specId for a split series has the format + // 61ca57f1-469d-11e7-af02-69e470af7417:Men's Accessories, : + const [layerId, splitLabel] = specId.split(':'); + const table = tables[layerId]; + + const layer = model.series.filter(({ id }) => id === layerId); + let label = splitLabel; + // compute label for filters split mode + if (splitLabel && layer.length && layer[0].split_mode === BUCKET_TYPES.FILTERS) { + const filter = layer[0]?.split_filters?.filter(({ id }) => id === splitLabel); + label = filter?.[0].label || (filter?.[0].filter?.query as string); + } + const index = table.rows.findIndex((row) => { + const condition = + geometry.x === row[X_ACCESSOR_INDEX] && geometry.y === row[X_ACCESSOR_INDEX + 1]; + return splitLabel ? condition && row[X_ACCESSOR_INDEX + 2].toString() === label : condition; + }); + if (index < 0) return; + + // Filter out the metric column + const bucketCols = table.columns.filter((col) => col.meta.sourceParams?.schema === 'group'); + + const newData = bucketCols.map(({ id }) => ({ + table, + column: parseInt(id, 10), + row: index, + value: table.rows[index][id] ?? null, + })); + if (newData.length) { + data.push(...newData); + } + }); + return data; +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/types.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/types.ts new file mode 100644 index 0000000000000..fee6670b49b9c --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Datatable } from 'src/plugins/expressions/public'; + +export interface TSVBTables { + [key: string]: Datatable; +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx index 9683089094054..5391bf319ee57 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx @@ -12,7 +12,7 @@ import React, { useCallback, useEffect } from 'react'; import { get } from 'lodash'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - +import { XYChartSeriesIdentifier, GeometryValue } from '@elastic/charts'; import { IUiSettingsClient } from 'src/core/public'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { PersistedState } from 'src/plugins/visualizations/public'; @@ -27,6 +27,7 @@ import { fetchIndexPattern } from '../../../common/index_patterns_utils'; import { TimeseriesVisParams } from '../../types'; import { getDataStart } from '../../services'; import { convertSeriesToDataTable } from './lib/convert_series_to_datatable'; +import { getClickFilterData } from './lib/get_click_filter_data'; import { X_ACCESSOR_INDEX } from '../visualizations/constants'; import { LastValueModeIndicator } from './last_value_mode_indicator'; import { getInterval } from './lib/get_interval'; @@ -102,6 +103,37 @@ function TimeseriesVisualization({ [handlers, model] ); + const handleFilterClick = useCallback( + async (series: PanelData[], points: Array<[GeometryValue, XYChartSeriesIdentifier]>) => { + const indexPatternValue = model.index_pattern || ''; + const { indexPatterns } = getDataStart(); + const { indexPattern } = await fetchIndexPattern(indexPatternValue, indexPatterns); + + // it should work only if index pattern is found + if (!indexPattern) return; + + const tables = indexPattern + ? await convertSeriesToDataTable(model, series, indexPattern) + : null; + + if (!tables) return; + + const data = getClickFilterData(points, tables, model); + + const event = { + name: 'filterBucket', + data: { + data, + negate: false, + timeFieldName: indexPattern.timeFieldName, + }, + }; + + handlers.event(event); + }, + [handlers, model] + ); + const handleUiState = useCallback( (field: string, value: { column: string; order: string }) => { uiState.set(field, value); @@ -156,6 +188,7 @@ function TimeseriesVisualization({ visData={visData} uiState={uiState} onBrush={onBrush} + onFilterClick={handleFilterClick} onUiState={handleUiState} syncColors={syncColors} palettesService={palettesService} diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx index de3163c44f95b..99f2564328402 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx @@ -8,6 +8,7 @@ import React, { Component } from 'react'; import * as Rx from 'rxjs'; +import uuid from 'uuid/v4'; import { share } from 'rxjs/operators'; import { isEqual, isEmpty, debounce } from 'lodash'; import { EventEmitter } from 'events'; @@ -79,6 +80,9 @@ export class VisEditor extends Component Promise; + onFilterClick: ( + series: PanelData[], + points: Array<[GeometryValue, XYChartSeriesIdentifier]> + ) => Promise; onUiState: ( field: string, value: { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js index 6510ad24615a6..ef6b30be30a30 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js @@ -91,6 +91,7 @@ MarkdownVisualization.propTypes = { className: PropTypes.string, model: PropTypes.object, onBrush: PropTypes.func, + onFilterClick: PropTypes.func, onChange: PropTypes.func, visData: PropTypes.object, getConfig: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js index 2b58145a4928d..3029bba04b450 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js @@ -79,6 +79,7 @@ MetricVisualization.propTypes = { additionalLabel: PropTypes.string, model: PropTypes.object, onBrush: PropTypes.func, + onFilterClick: PropTypes.func, onChange: PropTypes.func, visData: PropTypes.object, getConfig: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index 29560c4bd9368..8e59e8e1bb628 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -26,6 +26,7 @@ class TimeseriesVisualization extends Component { static propTypes = { model: PropTypes.object, onBrush: PropTypes.func, + onFilterClick: PropTypes.func, visData: PropTypes.object, getConfig: PropTypes.func, }; @@ -136,7 +137,7 @@ class TimeseriesVisualization extends Component { }; render() { - const { model, visData, onBrush, syncColors, palettesService } = this.props; + const { model, visData, onBrush, onFilterClick, syncColors, palettesService } = this.props; const series = get(visData, `${model.id}.series`, []); const interval = getInterval(visData, model); const yAxisIdGenerator = htmlIdGenerator('yaxis'); @@ -230,6 +231,7 @@ class TimeseriesVisualization extends Component { series={series} yAxis={yAxis} onBrush={onBrush} + onFilterClick={onFilterClick} backgroundColor={model.background_color} showGrid={Boolean(model.show_grid)} legend={Boolean(model.show_legend)} diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js index 8150d2e4d6e1b..41e6236cbc39b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js @@ -96,6 +96,7 @@ TopNVisualization.propTypes = { className: PropTypes.string, model: PropTypes.object, onBrush: PropTypes.func, + onFilterClick: PropTypes.func, onChange: PropTypes.func, visData: PropTypes.object, getConfig: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js index 7dc6a26185e2b..4b933bc81d882 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js @@ -109,6 +109,7 @@ export function visWithSplits(WrappedComponent) { model={model} visData={newVisData} onBrush={props.onBrush} + onFilterClick={props.onFilterClick} additionalLabel={additionalLabel || emptyLabel} backgroundColor={props.backgroundColor} getConfig={props.getConfig} diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index 44c1742e144be..a4d834ea8d217 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -61,6 +61,7 @@ export const TimeSeries = ({ series, yAxis, onBrush, + onFilterClick, xAxisFormatter, annotations, syncColors, @@ -118,6 +119,10 @@ export const TimeSeries = ({ onBrush(min, max, series); }; + const handleElementClick = (points) => { + onFilterClick(series, points); + }; + const getSeriesColor = useCallback( (seriesName, seriesGroupId, seriesId) => { const seriesById = series.filter((s) => s.seriesId === seriesGroupId); @@ -142,6 +147,7 @@ export const TimeSeries = ({ showLegendExtra={true} legendPosition={legendPosition} onBrushEnd={onBrushEndListener} + onElementClick={(args) => handleElementClick(args)} animateData={false} onPointerUpdate={handleCursorUpdate} theme={[ diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 66128b2abe856..3cb4faaacf25b 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; - +import uuid from 'uuid/v4'; import { TSVB_EDITOR_NAME } from './application/editor_controller'; import { PANEL_TYPES, TOOLTIP_MODES } from '../common/enums'; import { isStringTypeIndexPattern } from '../common/index_patterns_utils'; @@ -25,11 +25,11 @@ export const metricsVisDefinition = { group: VisGroups.PROMOTED, visConfig: { defaults: { - id: '61ca57f0-469d-11e7-af02-69e470af7417', + id: uuid(), type: PANEL_TYPES.TIMESERIES, series: [ { - id: '61ca57f1-469d-11e7-af02-69e470af7417', + id: uuid(), color: '#68BC00', split_mode: 'everything', palette: { @@ -38,7 +38,7 @@ export const metricsVisDefinition = { }, metrics: [ { - id: '61ca57f2-469d-11e7-af02-69e470af7417', + id: uuid(), type: 'count', }, ], @@ -75,7 +75,7 @@ export const metricsVisDefinition = { }, toExpressionAst, getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.brush]; + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; }, inspectorAdapters: {}, getUsedIndexPattern: async (params: VisParams) => { diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts index 1c1212add3d8c..479c3f47435cc 100644 --- a/src/plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -15,7 +15,6 @@ import { EditorController, TSVB_EDITOR_NAME } from './application/editor_control import { createMetricsFn } from './metrics_fn'; import { metricsVisDefinition } from './metrics_type'; import { - setSavedObjectsClient, setUISettings, setI18n, setFieldFormats, @@ -65,7 +64,6 @@ export class MetricsPlugin implements Plugin { } public start(core: CoreStart, { data }: MetricsPluginStartDependencies) { - setSavedObjectsClient(core.savedObjects); setI18n(core.i18n); setFieldFormats(data.fieldFormats); setDataStart(data); diff --git a/src/plugins/vis_type_timeseries/public/services.ts b/src/plugins/vis_type_timeseries/public/services.ts index 17c4a6c7de153..22f99f95691de 100644 --- a/src/plugins/vis_type_timeseries/public/services.ts +++ b/src/plugins/vis_type_timeseries/public/services.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { I18nStart, SavedObjectsStart, IUiSettingsClient, CoreStart } from 'src/core/public'; +import { I18nStart, IUiSettingsClient, CoreStart } from 'src/core/public'; import { createGetterSetter } from '../../kibana_utils/public'; import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; @@ -17,10 +17,6 @@ export const [getFieldFormats, setFieldFormats] = createGetterSetter< DataPublicPluginStart['fieldFormats'] >('FieldFormats'); -export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter( - 'SavedObjectsClient' -); - export const [getCoreStart, setCoreStart] = createGetterSetter('CoreStart'); export const [getDataStart, setDataStart] = createGetterSetter('DataStart'); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js index 29cf3f274dc24..f82f332df19fd 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js @@ -68,6 +68,7 @@ export function dateHistogram( bucketSize, seriesId: series.id, index: panel.use_kibana_indexes ? seriesIndex.indexPattern?.id : undefined, + panelId: panel.id, }); return next(doc); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js index a1fd242dc150e..741eb93267f4c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js @@ -32,6 +32,7 @@ describe('dateHistogram(req, panel, series)', () => { index_pattern: '*', time_field: '@timestamp', interval: '10s', + id: 'panelId', }; series = { id: 'test' }; config = { @@ -89,6 +90,7 @@ describe('dateHistogram(req, panel, series)', () => { intervalString: '10s', timeField: '@timestamp', seriesId: 'test', + panelId: 'panelId', }, }, }, @@ -130,6 +132,7 @@ describe('dateHistogram(req, panel, series)', () => { intervalString: '10s', timeField: '@timestamp', seriesId: 'test', + panelId: 'panelId', }, }, }, @@ -174,6 +177,7 @@ describe('dateHistogram(req, panel, series)', () => { intervalString: '20s', timeField: 'timestamp', seriesId: 'test', + panelId: 'panelId', }, }, }, @@ -230,6 +234,7 @@ describe('dateHistogram(req, panel, series)', () => { seriesId: 'test', bucketSize: 10, intervalString: '10s', + panelId: 'panelId', }, }, }, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.test.js index 0194e1ce3e5d9..a3a9022a67191 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.test.js @@ -11,6 +11,7 @@ import { normalizeQuery } from './normalize_query'; describe('normalizeQuery', () => { const req = 'req'; const seriesId = '61ca57f1-469d-11e7-af02-69e470af7417'; + const panelId = '39d49073-a924-426b-aa32-35acb40a9bb7'; let next; let panel; @@ -48,6 +49,7 @@ describe('normalizeQuery', () => { intervalString: '10s', bucketSize: 10, seriesId: [seriesId], + panelId, }, }, }, @@ -78,6 +80,7 @@ describe('normalizeQuery', () => { intervalString: '10s', bucketSize: 10, seriesId: [seriesId], + panelId: panelId, }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js index f0989cf0fa08b..3e883abc9e5e0 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js @@ -24,6 +24,7 @@ export function dateHistogram(req, panel, esQueryConfig, seriesIndex, capabiliti const meta = { timeField, index: panel.use_kibana_indexes ? seriesIndex.indexPattern?.id : undefined, + panelId: panel.id, }; const getDateHistogramForLastBucketMode = () => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.test.js index e2a7e7b17b548..71381f32599e3 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.test.js @@ -11,6 +11,7 @@ import { normalizeQuery } from './normalize_query'; describe('normalizeQuery', () => { const req = 'req'; const seriesId = '61ca57f1-469d-11e7-af02-69e470af7417'; + const panelId = '39d49073-a924-426b-aa32-35acb40a9bb7'; let next; let panel; @@ -56,6 +57,7 @@ describe('normalizeQuery', () => { timeField: 'order_date', intervalString: '10s', bucketSize: 10, + panelId, }, }, }, @@ -87,6 +89,7 @@ describe('normalizeQuery', () => { timeField: 'order_date', intervalString: '10s', bucketSize: 10, + panelId, }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts index 89d87da5f3d7e..5b865d451003a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts @@ -157,6 +157,7 @@ describe('buildRequestBody(req)', () => { intervalString: '10s', seriesId: 'c9b5f9c0-e403-11e6-be91-6f7688e9fac7', timeField: '@timestamp', + panelId: 'c9b5d2b0-e403-11e6-be91-6f7688e9fac7', }, }, }, diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts index d5a67f2eac064..4fd19aa45e69e 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts @@ -28,9 +28,12 @@ import { setUISettings, } from '../../services'; import { initVegaLayer, initTmsRasterLayer } from './layers'; -import { Map, NavigationControl, Style } from 'mapbox-gl'; -jest.mock('mapbox-gl', () => ({ +// @ts-expect-error +import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; + +jest.mock('mapbox-gl/dist/mapbox-gl-csp', () => ({ + setRTLTextPlugin: jest.fn(), Map: jest.fn().mockImplementation(() => ({ getLayer: () => '', removeLayer: jest.fn(), @@ -75,9 +78,10 @@ describe('vega_map_view/view', () => { setUISettings(coreStart.uiSettings); const getTmsService = jest.fn().mockReturnValue(({ - getVectorStyleSheet: (): Style => ({ + getVectorStyleSheet: () => ({ version: 8, sources: {}, + // @ts-expect-error layers: [], }), getMaxZoom: async () => 20, @@ -144,7 +148,7 @@ describe('vega_map_view/view', () => { await vegaMapView.init(); const { longitude, latitude, scrollWheelZoom } = vegaMapView._parser.mapConfig; - expect(Map).toHaveBeenCalledWith({ + expect(mapboxgl.Map).toHaveBeenCalledWith({ style: { version: 8, sources: {}, @@ -170,7 +174,7 @@ describe('vega_map_view/view', () => { await vegaMapView.init(); const { longitude, latitude, scrollWheelZoom } = vegaMapView._parser.mapConfig; - expect(Map).toHaveBeenCalledWith({ + expect(mapboxgl.Map).toHaveBeenCalledWith({ style: { version: 8, sources: {}, @@ -195,7 +199,7 @@ describe('vega_map_view/view', () => { await vegaMapView.init(); - expect(NavigationControl).toHaveBeenCalled(); + expect(mapboxgl.NavigationControl).toHaveBeenCalled(); }); }); }); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index 453e9596a2a4c..e899057819a19 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -7,9 +7,11 @@ */ import { i18n } from '@kbn/i18n'; -import { Map, Style, NavigationControl, MapboxOptions } from 'mapbox-gl'; +import type { Map, Style, MapboxOptions } from 'mapbox-gl'; import { View, parse } from 'vega'; +// @ts-expect-error +import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; import { initTmsRasterLayer, initVegaLayer } from './layers'; import { VegaBaseView } from '../vega_base_view'; import { getMapServiceSettings } from '../../services'; @@ -22,11 +24,17 @@ import { userConfiguredLayerId, vegaLayerId, } from './constants'; - import { validateZoomSettings, injectMapPropsIntoSpec } from './utils'; - import './vega_map_view.scss'; +// @ts-expect-error +import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; +// @ts-expect-error +import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; + +mapboxgl.workerUrl = mbWorkerUrl; +mapboxgl.setRTLTextPlugin(mbRtlPlugin); + async function updateVegaView(mapBoxInstance: Map, vegaView: View) { const mapCanvas = mapBoxInstance.getCanvas(); const { lat, lng } = mapBoxInstance.getCenter(); @@ -115,7 +123,7 @@ export class VegaMapView extends VegaBaseView { // In some cases, Vega may be initialized twice, e.g. after awaiting... if (!this._$container) return; - const mapBoxInstance = new Map({ + const mapBoxInstance = new mapboxgl.Map({ style, customAttribution, container: this._$container.get(0), @@ -142,7 +150,7 @@ export class VegaMapView extends VegaBaseView { private initControls(mapBoxInstance: Map) { if (this.shouldShowZoomControl) { - mapBoxInstance.addControl(new NavigationControl({ showCompass: false }), 'top-left'); + mapBoxInstance.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-left'); } // disable map rotation using right click + drag diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 3bb52eb15758a..f5bf6b59aa0ae 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -337,6 +337,14 @@ export class VisualizeEmbeddable data: { timeFieldName: this.vis.data.indexPattern?.timeFieldName!, ...event.data }, }; } + // do not trigger the filter click event if the filter bar is not visible + if ( + triggerId === VIS_EVENT_TO_TRIGGER.filter && + !this.input.id && + !this.vis.type.options.showFilterBar + ) { + return; + } getUiActions().getTrigger(triggerId).exec(context); } diff --git a/test/accessibility/apps/dashboard_panel.ts b/test/accessibility/apps/dashboard_panel.ts index 77b6cf2dbb6da..2a6c290172a9e 100644 --- a/test/accessibility/apps/dashboard_panel.ts +++ b/test/accessibility/apps/dashboard_panel.ts @@ -63,6 +63,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('dashboard panel full screen', async () => { const header = await dashboardPanelActions.getPanelHeading('[Flights] Airline Carrier'); await dashboardPanelActions.toggleContextMenu(header); + await dashboardPanelActions.clickContextMenuMoreItem(); + await testSubjects.click('embeddablePanelAction-togglePanel'); await a11y.testAppSnapshot(); }); diff --git a/test/api_integration/apis/home/sample_data.ts b/test/api_integration/apis/home/sample_data.ts index 99327901ec8c3..c681ad325e56f 100644 --- a/test/api_integration/apis/home/sample_data.ts +++ b/test/api_integration/apis/home/sample_data.ts @@ -43,7 +43,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.eql({ elasticsearchIndicesCreated: { kibana_sample_data_flights: 13059 }, - kibanaSavedObjectsLoaded: 20, + kibanaSavedObjectsLoaded: 23, }); }); diff --git a/test/api_integration/apis/search/search.ts b/test/api_integration/apis/search/search.ts index bc092dd3889bb..7ef82cd5467ab 100644 --- a/test/api_integration/apis/search/search.ts +++ b/test/api_integration/apis/search/search.ts @@ -99,26 +99,6 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body.message).to.contain('banana not found'); }); - it('should return 400 when index type is provided in OSS', async () => { - const resp = await supertest - .post(`/internal/search/es`) - .send({ - indexType: 'baad', - params: { - body: { - query: { - match_all: {}, - }, - }, - }, - }) - .expect(400); - - verifyErrorResponse(resp.body, 400); - - expect(resp.body.message).to.contain('Unsupported index pattern'); - }); - it('should return 400 with illegal ES argument', async () => { const resp = await supertest .post(`/internal/search/es`) diff --git a/test/api_integration/apis/telemetry/index.js b/test/api_integration/apis/telemetry/index.js index db95bf92cd44f..5394b54062d89 100644 --- a/test/api_integration/apis/telemetry/index.js +++ b/test/api_integration/apis/telemetry/index.js @@ -8,7 +8,6 @@ export default function ({ loadTestFile }) { describe('Telemetry', () => { - loadTestFile(require.resolve('./telemetry_local')); loadTestFile(require.resolve('./opt_in')); loadTestFile(require.resolve('./telemetry_optin_notice_seen')); }); diff --git a/test/api_integration/apis/telemetry/telemetry_local.ts b/test/api_integration/apis/telemetry/telemetry_local.ts deleted file mode 100644 index f20c6a3d129dd..0000000000000 --- a/test/api_integration/apis/telemetry/telemetry_local.ts +++ /dev/null @@ -1,331 +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 expect from '@kbn/expect'; -import supertestAsPromised from 'supertest-as-promised'; -import { omit } from 'lodash'; -import { basicUiCounters } from './__fixtures__/ui_counters'; -import { basicUsageCounters } from './__fixtures__/usage_counters'; -import type { FtrProviderContext } from '../../ftr_provider_context'; -import type { SavedObject } from '../../../../src/core/server'; -import ossRootTelemetrySchema from '../../../../src/plugins/telemetry/schema/oss_root.json'; -import ossPluginsTelemetrySchema from '../../../../src/plugins/telemetry/schema/oss_plugins.json'; -import { assertTelemetryPayload, flatKeys } from './utils'; - -async function retrieveTelemetry( - supertest: supertestAsPromised.SuperTest -) { - const { body } = await supertest - .post('/api/telemetry/v2/clusters/_stats') - .set('kbn-xsrf', 'xxx') - .send({ unencrypted: true }) - .expect(200); - - expect(body.length).to.be(1); - return body[0]; -} - -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const es = getService('es'); - const esArchiver = getService('esArchiver'); - - describe('/api/telemetry/v2/clusters/_stats', () => { - before('make sure there are some saved objects', () => esArchiver.load('saved_objects/basic')); - after('cleanup saved objects changes', () => esArchiver.unload('saved_objects/basic')); - - before('create some telemetry-data tracked indices', async () => { - await es.indices.create({ index: 'filebeat-telemetry_tests_logs' }); - }); - - after('cleanup telemetry-data tracked indices', async () => { - await es.indices.delete({ index: 'filebeat-telemetry_tests_logs' }); - }); - - describe('validate data types', () => { - let stats: Record; - - before('pull local stats', async () => { - stats = await retrieveTelemetry(supertest); - }); - - it('should pass the schema validation', () => { - try { - assertTelemetryPayload( - { root: ossRootTelemetrySchema, plugins: ossPluginsTelemetrySchema }, - stats - ); - } catch (err) { - err.message = `The telemetry schemas in 'src/plugins/telemetry/schema/' are out-of-date, please update it as required: ${err.message}`; - throw err; - } - }); - - it('should pass ad-hoc enforced validations', () => { - expect(stats.collection).to.be('local'); - expect(stats.collectionSource).to.be('local'); - expect(stats.license).to.be(undefined); // OSS cannot get the license - expect(stats.stack_stats.kibana.count).to.be.a('number'); - expect(stats.stack_stats.kibana.indices).to.be.a('number'); - expect(stats.stack_stats.kibana.os.platforms[0].platform).to.be.a('string'); - expect(stats.stack_stats.kibana.os.platforms[0].count).to.be(1); - expect(stats.stack_stats.kibana.os.platformReleases[0].platformRelease).to.be.a('string'); - expect(stats.stack_stats.kibana.os.platformReleases[0].count).to.be(1); - expect(stats.stack_stats.kibana.plugins.telemetry.opt_in_status).to.be(false); - expect(stats.stack_stats.kibana.plugins.telemetry.usage_fetcher).to.be.a('string'); - expect(stats.stack_stats.kibana.plugins.stack_management).to.be.an('object'); - expect(stats.stack_stats.kibana.plugins.ui_metric).to.be.an('object'); - expect(stats.stack_stats.kibana.plugins.ui_counters).to.be.an('object'); - expect(stats.stack_stats.kibana.plugins.application_usage).to.be.an('object'); - expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string'); - expect(stats.stack_stats.kibana.plugins.localization).to.be.an('object'); - expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true); - expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true); - expect(stats.stack_stats.kibana.plugins.csp.rulesChangedFromDefault).to.be(false); - expect(stats.stack_stats.kibana.plugins.kibana_config_usage).to.be.an('object'); - // non-default kibana configs. Configs set at 'test/api_integration/config.js'. - expect(omit(stats.stack_stats.kibana.plugins.kibana_config_usage, 'server.port')).to.eql({ - 'elasticsearch.username': '[redacted]', - 'elasticsearch.password': '[redacted]', - 'elasticsearch.hosts': '[redacted]', - 'elasticsearch.healthCheck.delay': 3600000, - 'plugins.paths': '[redacted]', - 'logging.json': false, - 'server.xsrf.disableProtection': true, - 'server.compression.referrerWhitelist': '[redacted]', - 'server.maxPayload': 1679958, - 'status.allowAnonymous': true, - 'home.disableWelcomeScreen': true, - 'data.search.aggs.shardDelay.enabled': true, - 'security.showInsecureClusterWarning': false, - 'telemetry.banner': false, - 'telemetry.url': '[redacted]', - 'telemetry.optInStatusUrl': '[redacted]', - 'telemetry.optIn': false, - 'newsfeed.service.urlRoot': '[redacted]', - 'newsfeed.service.pathTemplate': '[redacted]', - 'savedObjects.maxImportPayloadBytes': 10485760, - 'savedObjects.maxImportExportSize': 10001, - 'usageCollection.usageCounters.bufferDuration': 0, - }); - expect(stats.stack_stats.kibana.plugins.kibana_config_usage['server.port']).to.be.a( - 'number' - ); - - // Testing stack_stats.data - expect(stats.stack_stats.data).to.be.an('object'); - expect(stats.stack_stats.data).to.be.an('array'); - expect(stats.stack_stats.data[0]).to.be.an('object'); - expect(stats.stack_stats.data[0].pattern_name).to.be('filebeat'); - expect(stats.stack_stats.data[0].shipper).to.be('filebeat'); - expect(stats.stack_stats.data[0].index_count).to.be(1); - expect(stats.stack_stats.data[0].doc_count).to.be(0); - expect(stats.stack_stats.data[0].ecs_index_count).to.be(0); - expect(stats.stack_stats.data[0].size_in_bytes).to.be.a('number'); - - expect(stats.stack_stats.kibana.plugins.saved_objects_counts).to.be.an('object'); - expect(stats.stack_stats.kibana.plugins.saved_objects_counts.by_type).to.be.an('array'); - expect(stats.stack_stats.kibana.plugins.saved_objects_counts.by_type).to.eql([ - { type: 'config', count: 2 }, - { type: 'dashboard', count: 2 }, - { type: 'index-pattern', count: 2 }, - { type: 'visualization', count: 2 }, - ]); - }); - - it('should validate mandatory fields exist', () => { - const actual = flatKeys(stats); - expect(actual).to.be.an('array'); - const expected = [ - 'cluster_name', - 'cluster_stats.cluster_uuid', - 'cluster_stats.indices.analysis', - 'cluster_stats.indices.completion', - 'cluster_stats.indices.count', - 'cluster_stats.indices.docs', - 'cluster_stats.indices.fielddata', - 'cluster_stats.indices.mappings', - 'cluster_stats.indices.query_cache', - 'cluster_stats.indices.segments', - 'cluster_stats.indices.shards', - 'cluster_stats.indices.store', - 'cluster_stats.nodes.count', - 'cluster_stats.nodes.discovery_types', - 'cluster_stats.nodes.fs', - 'cluster_stats.nodes.ingest', - 'cluster_stats.nodes.jvm', - 'cluster_stats.nodes.network_types', - 'cluster_stats.nodes.os', - 'cluster_stats.nodes.packaging_types', - 'cluster_stats.nodes.plugins', - 'cluster_stats.nodes.process', - 'cluster_stats.nodes.versions', - 'cluster_stats.nodes.usage', - 'cluster_stats.status', - 'cluster_stats.timestamp', - 'cluster_uuid', - 'collection', - 'collectionSource', - 'stack_stats.kibana.count', - 'stack_stats.kibana.indices', - 'stack_stats.kibana.os', - 'stack_stats.kibana.plugins', - 'stack_stats.kibana.versions', - 'timestamp', - 'version', - ]; - - expect(expected.every((m) => actual.includes(m))).to.be.ok(); - }); - }); - - describe('UI Counters telemetry', () => { - before('Add UI Counters saved objects', () => esArchiver.load('saved_objects/ui_counters')); - after('cleanup saved objects changes', () => esArchiver.unload('saved_objects/ui_counters')); - it('returns ui counters aggregated by day', async () => { - const stats = await retrieveTelemetry(supertest); - expect(stats.stack_stats.kibana.plugins.ui_counters).to.eql(basicUiCounters); - }); - }); - - describe('Usage Counters telemetry', () => { - before('Add UI Counters saved objects', () => - esArchiver.load('saved_objects/usage_counters') - ); - after('cleanup saved objects changes', () => - esArchiver.unload('saved_objects/usage_counters') - ); - - it('returns usage counters aggregated by day', async () => { - const stats = await retrieveTelemetry(supertest); - expect(stats.stack_stats.kibana.plugins.usage_counters).to.eql(basicUsageCounters); - }); - }); - - describe('application usage limits', () => { - function createSavedObject(viewId?: string) { - return supertest - .post('/api/saved_objects/application_usage_daily') - .send({ - attributes: { - appId: 'test-app', - viewId, - minutesOnScreen: 10.33, - numberOfClicks: 10, - timestamp: new Date().toISOString(), - }, - }) - .expect(200) - .then((resp) => resp.body.id); - } - - describe('basic behaviour', () => { - let savedObjectIds: string[] = []; - before('create application usage entries', async () => { - await esArchiver.emptyKibanaIndex(); - savedObjectIds = await Promise.all([ - createSavedObject(), - createSavedObject('appView1'), - createSavedObject(), - ]); - }); - after('cleanup', async () => { - await Promise.all( - savedObjectIds.map((savedObjectId) => { - return supertest - .delete(`/api/saved_objects/application_usage_daily/${savedObjectId}`) - .expect(200); - }) - ); - }); - - it('should return application_usage data', async () => { - const stats = await retrieveTelemetry(supertest); - expect(stats.stack_stats.kibana.plugins.application_usage).to.eql({ - 'test-app': { - appId: 'test-app', - viewId: 'main', - clicks_total: 20, - clicks_7_days: 20, - clicks_30_days: 20, - clicks_90_days: 20, - minutes_on_screen_total: 20.66, - minutes_on_screen_7_days: 20.66, - minutes_on_screen_30_days: 20.66, - minutes_on_screen_90_days: 20.66, - views: [ - { - appId: 'test-app', - viewId: 'appView1', - clicks_total: 10, - clicks_7_days: 10, - clicks_30_days: 10, - clicks_90_days: 10, - minutes_on_screen_total: 10.33, - minutes_on_screen_7_days: 10.33, - minutes_on_screen_30_days: 10.33, - minutes_on_screen_90_days: 10.33, - }, - ], - }, - }); - }); - }); - - describe('10k + 1', () => { - const savedObjectIds = []; - before('create 10k + 1 entries for application usage', async () => { - await supertest - .post('/api/saved_objects/_bulk_create') - .send( - new Array(10001).fill(0).map(() => ({ - type: 'application_usage_daily', - attributes: { - appId: 'test-app', - minutesOnScreen: 1, - numberOfClicks: 1, - timestamp: new Date().toISOString(), - }, - })) - ) - .expect(200) - .then((resp) => - resp.body.saved_objects.forEach(({ id }: SavedObject) => savedObjectIds.push(id)) - ); - }); - after('clean them all', async () => { - // The SavedObjects API does not allow bulk deleting, and deleting one by one takes ages and the tests timeout - await es.deleteByQuery({ - index: '.kibana', - body: { query: { term: { type: 'application_usage_daily' } } }, - conflicts: 'proceed', - }); - }); - - it("should only use the first 10k docs for the application_usage data (they'll be rolled up in a later process)", async () => { - const stats = await retrieveTelemetry(supertest); - expect(stats.stack_stats.kibana.plugins.application_usage).to.eql({ - 'test-app': { - appId: 'test-app', - viewId: 'main', - clicks_total: 10000, - clicks_7_days: 10000, - clicks_30_days: 10000, - clicks_90_days: 10000, - minutes_on_screen_total: 10000, - minutes_on_screen_7_days: 10000, - minutes_on_screen_30_days: 10000, - minutes_on_screen_90_days: 10000, - views: [], - }, - }); - }); - }); - }); - }); -} diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz b/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz index c72f46370fa12..d65e1920b2342 100644 Binary files a/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz and b/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz differ diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json b/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json index e601c43431437..6dac52137ef01 100644 --- a/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json +++ b/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json @@ -1,148 +1,2613 @@ { "type": "index", "value": { - "index": ".kibana", - "settings": { - "index": { - "number_of_shards": "1", - "number_of_replicas": "1" + "aliases": { + ".kibana": { } }, + "index": ".kibana_1", "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "05d57e6963593484582a4de341446974", + "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "7c28a18fbac7c2a4e79449e9802ef476", + "cases-comments": "112cefc2b6737e613a8ef033234755e6", + "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", + "cases-connector-mappings": "6bc7e49411d38be4969dc6aa8bd43776", + "cases-sub-case": "2dc9dbf1fc7144e2b18fffc017358ff9", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", + "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "epm-packages": "0cbbb16506734d341a96aaed65ec6413", + "epm-packages-assets": "44621b2f6052ef966da47b7c3a00f33b", + "exception-list": "baf108c9934dda844921f692a513adae", + "exception-list-agnostic": "baf108c9934dda844921f692a513adae", + "file-upload-usage-collection-telemetry": "a34fbb8e3263d105044869264860c697", + "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "59fd74f819f028f8555776db198d2562", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "27a94b2edcb0610c6aea54a7c56d7752", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", + "ingest-agent-policies": "8df93787e2927f227dc80db8b6d309b9", + "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", + "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", + "ingest_manager_settings": "22d4d1288c2687ef6a8f6e83159b4542", + "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "3d1b76c39bfb2cc8296b024d73854724", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "9134b47593116d7953f6adba096fc463", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-job": "3bb64c31915acf93fc724af137a0891b", + "ml-module": "46ef4f0d6682636f0fff9799d6a2d7ac", + "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-session": "33157cf0119e41cd4e7a1d24266beff4", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "security-solution-signals-migration": "72761fd374ca11122ac8025a92b84fca", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "3e97beae13cdfc6d62bc1846119f7276", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "spaces-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "tag": "83d55da58f6530f7055415717ec06474", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, "dynamic": "strict", "properties": { - "config": { - "dynamic": "true", + "action": { "properties": { - "buildNum": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "executionStatus": { + "properties": { + "error": { + "properties": { + "message": { + "type": "keyword" + }, + "reason": { + "type": "keyword" + } + } + }, + "lastExecutionDate": { + "type": "date" + }, + "status": { + "type": "keyword" + } + } + }, + "meta": { + "properties": { + "versionApiKeyLastmodified": { + "type": "keyword" + } + } + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "notifyWhen": { + "type": "keyword" + }, + "params": { + "type": "flattened" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedAt": { + "type": "date" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "api_key_pending_invalidation": { + "properties": { + "apiKeyId": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector": { + "properties": { + "fields": { + "properties": { + "key": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "id": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "settings": { + "properties": { + "syncAlerts": { + "type": "boolean" + } + } + }, + "status": { "type": "keyword" }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "alertId": { + "type": "keyword" + }, + "associationType": { + "type": "keyword" + }, + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "index": { + "type": "keyword" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector": { + "properties": { + "fields": { + "properties": { + "key": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "id": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-connector-mappings": { + "properties": { + "mappings": { + "properties": { + "action_type": { + "type": "keyword" + }, + "source": { + "type": "keyword" + }, + "target": { + "type": "keyword" + } + } + } + } + }, + "cases-sub-case": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "optionsJSON": { + "index": false, + "type": "text" + }, + "panelsJSON": { + "index": false, + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "pause": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "section": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "value": { + "doc_values": false, + "index": false, + "type": "integer" + } + } + }, + "timeFrom": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "timeRestore": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "timeTo": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "artifacts": { + "properties": { + "artifactId": { + "index": false, + "type": "keyword" + }, + "policyId": { + "index": false, + "type": "keyword" + } + }, + "type": "nested" + }, + "created": { + "index": false, + "type": "date" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "enterprise_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "install_source": { + "type": "keyword" + }, + "install_started_at": { + "type": "date" + }, + "install_status": { + "type": "keyword" + }, + "install_version": { + "type": "keyword" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "package_assets": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "epm-packages-assets": { + "properties": { + "asset_path": { + "type": "keyword" + }, + "data_base64": { + "type": "binary" + }, + "data_utf8": { + "index": false, + "type": "text" + }, + "install_source": { + "type": "keyword" + }, + "media_type": { + "type": "keyword" + }, + "package_name": { + "type": "keyword" + }, + "package_version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "os_types": { + "type": "keyword" + }, + "tags": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "os_types": { + "type": "keyword" + }, + "tags": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-usage-collection-telemetry": { + "properties": { + "file_upload": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "fleet-agent-actions": { + "properties": { + "ack_data": { + "type": "text" + }, + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision": { + "type": "integer" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "policy_id": { + "type": "keyword" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision": { + "type": "integer" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "upgrade_started_at": { + "type": "date" + }, + "upgraded_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "legacyIndexPatternRef": { + "index": false, + "type": "text" + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "dynamic": "false", + "properties": { + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "dynamic": "false", + "type": "object" + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "is_default_fleet_server": { + "type": "boolean" + }, + "is_managed": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_policies": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "config_yaml": { + "type": "text" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "compiled_input": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "policy_id": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_urls": { + "type": "keyword" + } + } + }, + "inventory-view": { + "dynamic": "false", + "type": "object" + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "type": "object" + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "dynamic": "false", + "type": "object" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "dynamic": "false", + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-job": { + "properties": { + "datafeed_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "job_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "ml-module": { + "dynamic": "false", + "properties": { + "datafeeds": { + "type": "object" + }, + "defaultIndexPattern": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "description": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "jobs": { + "type": "object" + }, + "logo": { + "type": "object" + }, + "query": { + "type": "object" + }, + "title": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "monitoring-telemetry": { + "properties": { + "reportedClusterUuids": { + "type": "keyword" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-session": { + "properties": { + "appId": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "expires": { + "type": "date" + }, + "idMapping": { + "enabled": false, + "type": "object" + }, + "initialState": { + "enabled": false, + "type": "object" + }, + "name": { + "type": "keyword" + }, + "persisted": { + "type": "boolean" + }, + "realmName": { + "type": "keyword" + }, + "realmType": { + "type": "keyword" + }, + "restoreState": { + "enabled": false, + "type": "object" + }, + "sessionId": { + "type": "keyword" + }, + "status": { + "type": "keyword" + }, + "touched": { + "type": "date" + }, + "urlGeneratorId": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "security-solution-signals-migration": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "createdBy": { + "index": false, + "type": "text" + }, + "destinationIndex": { + "index": false, + "type": "keyword" + }, + "error": { + "index": false, + "type": "text" + }, + "sourceIndex": { + "type": "keyword" + }, + "status": { + "index": false, + "type": "keyword" + }, + "taskId": { + "index": false, + "type": "keyword" + }, + "updated": { + "index": false, + "type": "date" + }, + "updatedBy": { + "index": false, + "type": "text" + }, + "version": { + "type": "long" + } + } + }, + "server": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" } } - } - } - }, - "dashboard": { - "properties": { + }, "description": { "type": "text" }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { + "eqlOptions": { "properties": { - "searchSourceJSON": { + "eventCategoryField": { + "type": "text" + }, + "query": { + "type": "text" + }, + "size": { + "type": "text" + }, + "tiebreakerField": { + "type": "text" + }, + "timestampField": { "type": "text" } } }, - "optionsJSON": { + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { "type": "text" }, - "panelsJSON": { + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "indexNames": { "type": "text" }, - "refreshInterval": { + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { "properties": { - "display": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "dynamic": "false", + "properties": { + "columnId": { "type": "keyword" }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" + "columnType": { + "type": "keyword" }, - "value": { - "type": "integer" + "sortDirection": { + "type": "keyword" } } }, - "timeFrom": { + "status": { "type": "keyword" }, - "timeRestore": { - "type": "boolean" + "templateTimelineId": { + "type": "text" }, - "timeTo": { + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { "type": "keyword" }, "title": { "type": "text" }, - "uiStateJSON": { - "type": "text" + "updated": { + "type": "date" }, - "version": { - "type": "integer" + "updatedBy": { + "type": "text" } } }, - "index-pattern": { + "siem-ui-timeline-note": { "properties": { - "fieldFormatMap": { + "created": { + "type": "date" + }, + "createdBy": { "type": "text" }, - "fields": { + "eventId": { + "type": "keyword" + }, + "note": { "type": "text" }, - "intervalName": { + "timelineId": { "type": "keyword" }, - "notExpandable": { - "type": "boolean" + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" }, - "sourceFilters": { + "createdBy": { "type": "text" }, - "timeFieldName": { + "eventId": { "type": "keyword" }, - "title": { + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { "type": "text" } } }, - "search": { + "space": { "properties": { - "columns": { + "_reserved": { + "type": "boolean" + }, + "color": { "type": "keyword" }, "description": { "type": "text" }, - "hits": { - "type": "integer" + "disabledFeatures": { + "type": "keyword" }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } + "imageUrl": { + "index": false, + "type": "text" }, - "sort": { + "initials": { "type": "keyword" }, - "title": { + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaces-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "tag": { + "properties": { + "color": { "type": "text" }, - "version": { - "type": "integer" + "description": { + "type": "text" + }, + "name": { + "type": "text" } } }, - "server": { + "telemetry": { "properties": { - "uuid": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" } } }, @@ -187,28 +2652,147 @@ } } }, - "namespace": { + "type": { "type": "keyword" }, - "references": { + "ui-counter": { "properties": { - "id": { - "type": "keyword" + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" }, - "name": { + "indexName": { "type": "keyword" }, - "type": { - "type": "keyword" + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" } - }, - "type": "nested" + } }, - "type": { - "type": "keyword" + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } }, - "updated_at": { - "type": "date" + "uptime-dynamic-settings": { + "dynamic": "false", + "type": "object" }, "url": { "properties": { @@ -222,13 +2806,13 @@ "type": "date" }, "url": { - "type": "text", "fields": { "keyword": { - "type": "keyword", - "ignore_above": 2048 + "ignore_above": 2048, + "type": "keyword" } - } + }, + "type": "text" } } }, @@ -240,28 +2824,43 @@ "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, - "savedSearchId": { + "savedSearchRefName": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, "uiStateJSON": { + "index": false, "type": "text" }, "version": { "type": "integer" }, "visState": { + "index": false, "type": "text" } } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" } } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } } } -} +} \ No newline at end of file diff --git a/test/common/services/deployment.ts b/test/common/services/deployment.ts index 510124ce3d1b7..65466ca966ad2 100644 --- a/test/common/services/deployment.ts +++ b/test/common/services/deployment.ts @@ -30,14 +30,6 @@ export function DeploymentProvider({ getService }: FtrProviderContext) { return getUrl.baseUrl(config.get('servers.elasticsearch')); }, - /** - * Helper to detect an OSS licensed Kibana - * Useful for functional testing in cloud environment - */ - async isOss() { - return config.get('kbnTestServer.serverArgs').indexOf('--oss') > -1; - }, - async isCloud(): Promise { const baseUrl = this.getHostPort(); const username = config.get('servers.kibana.username'); diff --git a/test/examples/expressions_explorer/expressions.ts b/test/examples/expressions_explorer/expressions.ts index 39afa177501d5..4c240653b5fdd 100644 --- a/test/examples/expressions_explorer/expressions.ts +++ b/test/examples/expressions_explorer/expressions.ts @@ -22,7 +22,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { await retry.try(async () => { const text = await testSubjects.getVisibleText('expressionResult'); expect(text).to.be( - '{\n "type": "error",\n "error": {\n "message": "Function markdown could not be found.",\n "name": "fn not found"\n }\n}' + '{\n "type": "render",\n "as": "markdown",\n "value": {\n "content": "## expressions explorer",\n "font": {\n "type": "style",\n "spec": {\n "fontFamily": "\'Open Sans\', Helvetica, Arial, sans-serif",\n "fontWeight": "normal",\n "fontStyle": "normal",\n "textDecoration": "none",\n "textAlign": "left",\n "fontSize": "14px",\n "lineHeight": "1"\n },\n "css": "font-family:\'Open Sans\', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:left;font-size:14px;line-height:1"\n },\n "openLinksInNewTab": false\n }\n}' ); }); }); @@ -30,7 +30,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { it('renders expression', async () => { await retry.try(async () => { const text = await testSubjects.getVisibleText('expressionRender'); - expect(text).to.be('Function markdown could not be found.'); + expect(text).to.be('expressions explorer rendering'); }); }); diff --git a/test/functional/apps/home/_home.js b/test/functional/apps/home/_home.js index 056f3ec6f993c..24e672463964d 100644 --- a/test/functional/apps/home/_home.js +++ b/test/functional/apps/home/_home.js @@ -11,6 +11,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const browser = getService('browser'); const globalNav = getService('globalNav'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'header', 'home']); describe('Kibana takes you home', function describeIndexTests() { @@ -25,7 +26,8 @@ export default function ({ getService, getPageObjects }) { }); it('clicking on console on homepage should take you to console app', async () => { - await PageObjects.home.clickSynopsis('console'); + await PageObjects.common.navigateToUrl('home'); + await testSubjects.click('homeDevTools'); const url = await browser.getCurrentUrl(); expect(url.includes('/app/dev_tools#/console')).to.be(true); }); diff --git a/test/functional/apps/home/_newsfeed.ts b/test/functional/apps/home/_newsfeed.ts index 449aeea013341..5b8b5a22cf439 100644 --- a/test/functional/apps/home/_newsfeed.ts +++ b/test/functional/apps/home/_newsfeed.ts @@ -11,7 +11,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const globalNav = getService('globalNav'); - const deployment = getService('deployment'); const PageObjects = getPageObjects(['newsfeed']); describe('Newsfeed', () => { @@ -38,16 +37,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('shows all news from newsfeed', async () => { const objects = await PageObjects.newsfeed.getNewsfeedList(); - if (await deployment.isOss()) { - expect(objects).to.eql([ - '21 June 2019\nYou are functionally testing the newsfeed widget with fixtures!\nSee test/common/fixtures/plugins/newsfeed/newsfeed_simulation\nGeneric feed-viewer could go here', - '21 June 2019\nStaging too!\nHello world\nGeneric feed-viewer could go here', - ]); - } else { - // can't shim the API in cloud so going to check that at least something is rendered - // to test that the API was called and returned something that could be rendered - expect(objects.length).to.be.above(0); - } + // can't shim the API in cloud so going to check that at least something is rendered + // to test that the API was called and returned something that could be rendered + expect(objects.length).to.be.above(0); }); it('clicking on newsfeed icon should close opened newsfeed', async () => { diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index fdbc419c16241..c7a4c8c51bf44 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -27,7 +27,6 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - const deployment = getService('deployment'); const log = getService('log'); const browser = getService('browser'); const retry = getService('retry'); @@ -187,16 +186,14 @@ export default function ({ getService, getPageObjects }) { }); it('should visualize scripted field in vertical bar chart', async function () { - const isOss = await deployment.isOss(); - if (!isOss) { - await filterBar.removeAllFilters(); - await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName); - await PageObjects.header.waitUntilLoadingHasFinished(); - // verify Lens opens a visualization - expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( - 'Average of ram_Pain1' - ); - } + await filterBar.removeAllFilters(); + await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName); + await PageObjects.header.waitUntilLoadingHasFinished(); + // verify Lens opens a visualization + expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( + '@timestamp', + 'Median of ram_Pain1' + ); }); }); @@ -277,15 +274,12 @@ export default function ({ getService, getPageObjects }) { }); it('should visualize scripted field in vertical bar chart', async function () { - const isOss = await deployment.isOss(); - if (!isOss) { - await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); - await PageObjects.header.waitUntilLoadingHasFinished(); - // verify Lens opens a visualization - expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( - 'Top values of painString' - ); - } + await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); + await PageObjects.header.waitUntilLoadingHasFinished(); + // verify Lens opens a visualization + expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( + 'Top values of painString' + ); }); }); @@ -367,15 +361,12 @@ export default function ({ getService, getPageObjects }) { }); it('should visualize scripted field in vertical bar chart', async function () { - const isOss = await deployment.isOss(); - if (!isOss) { - await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); - await PageObjects.header.waitUntilLoadingHasFinished(); - // verify Lens opens a visualization - expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( - 'Top values of painBool' - ); - } + await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); + await PageObjects.header.waitUntilLoadingHasFinished(); + // verify Lens opens a visualization + expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( + 'Top values of painBool' + ); }); }); @@ -460,15 +451,10 @@ export default function ({ getService, getPageObjects }) { }); it('should visualize scripted field in vertical bar chart', async function () { - const isOss = await deployment.isOss(); - if (!isOss) { - await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); - await PageObjects.header.waitUntilLoadingHasFinished(); - // verify Lens opens a visualization - expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( - 'painDate' - ); - } + await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); + await PageObjects.header.waitUntilLoadingHasFinished(); + // verify Lens opens a visualization + expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain('painDate'); }); }); }); diff --git a/test/functional/apps/visualize/_add_to_dashboard.ts b/test/functional/apps/visualize/_add_to_dashboard.ts index 17d628db86d25..4343f8b1469d6 100644 --- a/test/functional/apps/visualize/_add_to_dashboard.ts +++ b/test/functional/apps/visualize/_add_to_dashboard.ts @@ -26,6 +26,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]); describe('Add to Dashboard', function describeIndexTests() { + before(async () => { + await PageObjects.visualize.initTests(); + }); it('adding a new metric to a new dashboard by value', async function () { await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickMetric(); diff --git a/test/functional/apps/visualize/_area_chart.ts b/test/functional/apps/visualize/_area_chart.ts index 2bad91565de72..99f65458bb606 100644 --- a/test/functional/apps/visualize/_area_chart.ts +++ b/test/functional/apps/visualize/_area_chart.ts @@ -34,6 +34,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); describe('area charts', function indexPatternCreation() { + before(async () => { + await PageObjects.visualize.initTests(); + }); const initAreaChart = async () => { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); diff --git a/test/functional/apps/visualize/_chart_types.ts b/test/functional/apps/visualize/_chart_types.ts index 4ee6e3dac21b6..f52d8f00c1e48 100644 --- a/test/functional/apps/visualize/_chart_types.ts +++ b/test/functional/apps/visualize/_chart_types.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import _ from 'lodash'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -17,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('chart types', function () { before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); }); @@ -35,6 +35,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualize.clickAggBasedVisualizations(); const expectedChartTypes = [ 'Area', + 'Coordinate Map', 'Data table', 'Gauge', 'Goal', @@ -43,6 +44,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Line', 'Metric', 'Pie', + 'Region Map', 'Tag cloud', 'Timelion', 'Vertical bar', diff --git a/test/functional/apps/visualize/_data_table.ts b/test/functional/apps/visualize/_data_table.ts index 1ff5bdcc6da78..14181c084a77f 100644 --- a/test/functional/apps/visualize/_data_table.ts +++ b/test/functional/apps/visualize/_data_table.ts @@ -28,6 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const vizName1 = 'Visualization DataTable'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.ts b/test/functional/apps/visualize/_data_table_nontimeindex.ts index 0146bb81134a7..1549f2aac0735 100644 --- a/test/functional/apps/visualize/_data_table_nontimeindex.ts +++ b/test/functional/apps/visualize/_data_table_nontimeindex.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const vizName1 = 'Visualization DataTable without time filter'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts index df219edc1d2d5..ef664bf4b3054 100644 --- a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts +++ b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts @@ -30,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const vizName1 = 'Visualization DataTable w/o time filter'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/_embedding_chart.ts b/test/functional/apps/visualize/_embedding_chart.ts index a6f0b21f96b35..93ab2987dc4a8 100644 --- a/test/functional/apps/visualize/_embedding_chart.ts +++ b/test/functional/apps/visualize/_embedding_chart.ts @@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('embedding', () => { describe('a data table', () => { before(async function () { + await PageObjects.visualize.initTests(); await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); diff --git a/test/functional/apps/visualize/_experimental_vis.ts b/test/functional/apps/visualize/_experimental_vis.ts index 7132d252cd23c..8e33285f909be 100644 --- a/test/functional/apps/visualize/_experimental_vis.ts +++ b/test/functional/apps/visualize/_experimental_vis.ts @@ -15,6 +15,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize']); describe('experimental visualizations in visualize app ', function () { + before(async () => { + await PageObjects.visualize.initTests(); + }); + describe('experimental visualizations', () => { beforeEach(async () => { log.debug('navigateToApp visualize'); diff --git a/test/functional/apps/visualize/_gauge_chart.ts b/test/functional/apps/visualize/_gauge_chart.ts index 0153e022e71b3..6dd460d4ac32b 100644 --- a/test/functional/apps/visualize/_gauge_chart.ts +++ b/test/functional/apps/visualize/_gauge_chart.ts @@ -19,6 +19,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('gauge chart', function indexPatternCreation() { + before(async () => { + await PageObjects.visualize.initTests(); + }); async function initGaugeVis() { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); diff --git a/test/functional/apps/visualize/_heatmap_chart.ts b/test/functional/apps/visualize/_heatmap_chart.ts index 660f45179631e..d71d524cc8f3b 100644 --- a/test/functional/apps/visualize/_heatmap_chart.ts +++ b/test/functional/apps/visualize/_heatmap_chart.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const vizName1 = 'Visualization HeatmapChart'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickHeatmapChart'); diff --git a/test/functional/apps/visualize/_histogram_request_start.ts b/test/functional/apps/visualize/_histogram_request_start.ts index b027dbcd57780..8b5c31701d025 100644 --- a/test/functional/apps/visualize/_histogram_request_start.ts +++ b/test/functional/apps/visualize/_histogram_request_start.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); + const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects([ 'common', @@ -24,6 +25,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('histogram agg onSearchRequestStart', function () { before(async function () { + // loading back default data + await esArchiver.load('empty_kibana'); + + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('long_window_logstash'); + + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/_inspector.ts b/test/functional/apps/visualize/_inspector.ts index e46a833fd0fd7..f83eae2fc00bc 100644 --- a/test/functional/apps/visualize/_inspector.ts +++ b/test/functional/apps/visualize/_inspector.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('inspector', function describeIndexTests() { before(async function () { + await PageObjects.visualize.initTests(); await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickVerticalBarChart(); await PageObjects.visualize.clickNewSearch(); diff --git a/test/functional/apps/visualize/_lab_mode.ts b/test/functional/apps/visualize/_lab_mode.ts index 0a099ec27eee5..d3a02a8d17291 100644 --- a/test/functional/apps/visualize/_lab_mode.ts +++ b/test/functional/apps/visualize/_lab_mode.ts @@ -13,9 +13,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); - const PageObjects = getPageObjects(['common', 'header', 'discover', 'settings']); + const PageObjects = getPageObjects(['common', 'header', 'discover', 'settings', 'visualize']); describe('visualize lab mode', () => { + before(async () => { + await PageObjects.visualize.initTests(); + }); it('disabling does not break loading saved searches', async () => { await PageObjects.common.navigateToUrl('discover', '', { useActualUrl: true }); await PageObjects.discover.saveSearch('visualize_lab_mode_test'); diff --git a/test/functional/apps/visualize/_line_chart_split_chart.ts b/test/functional/apps/visualize/_line_chart_split_chart.ts index 1b6da1b39f1e3..91c1db533cee9 100644 --- a/test/functional/apps/visualize/_line_chart_split_chart.ts +++ b/test/functional/apps/visualize/_line_chart_split_chart.ts @@ -43,7 +43,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); }; - before(initLineChart); + before(async () => { + await PageObjects.visualize.initTests(); + await initLineChart(); + }); afterEach(async () => { await inspector.close(); diff --git a/test/functional/apps/visualize/_line_chart_split_series.ts b/test/functional/apps/visualize/_line_chart_split_series.ts index b3debc13c7770..6630690b89c2c 100644 --- a/test/functional/apps/visualize/_line_chart_split_series.ts +++ b/test/functional/apps/visualize/_line_chart_split_series.ts @@ -41,7 +41,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); }; - before(initLineChart); + before(async () => { + await PageObjects.visualize.initTests(); + await initLineChart(); + }); afterEach(async () => { await inspector.close(); diff --git a/test/functional/apps/visualize/_linked_saved_searches.ts b/test/functional/apps/visualize/_linked_saved_searches.ts index 160720d27ab61..cfb20cf4b59b8 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.ts +++ b/test/functional/apps/visualize/_linked_saved_searches.ts @@ -30,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { let discoverSavedSearchUrlPath: string; before(async () => { + await PageObjects.visualize.initTests(); await PageObjects.common.navigateToApp('discover'); await filterBar.addFilter('extension.raw', 'is', 'jpg'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/test/functional/apps/visualize/_markdown_vis.ts b/test/functional/apps/visualize/_markdown_vis.ts index c8a481dbda2c3..16cdda9b610ca 100644 --- a/test/functional/apps/visualize/_markdown_vis.ts +++ b/test/functional/apps/visualize/_markdown_vis.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('markdown app in visualize app', () => { before(async function () { + await PageObjects.visualize.initTests(); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visEditor.setMarkdownTxt(markdown); diff --git a/test/functional/apps/visualize/_metric_chart.ts b/test/functional/apps/visualize/_metric_chart.ts index 8c74784ef96d8..7853a3a845bfc 100644 --- a/test/functional/apps/visualize/_metric_chart.ts +++ b/test/functional/apps/visualize/_metric_chart.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('metric chart', function () { before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickMetric'); diff --git a/test/functional/apps/visualize/_pie_chart.ts b/test/functional/apps/visualize/_pie_chart.ts index 16826b1676589..dd58ca6514c36 100644 --- a/test/functional/apps/visualize/_pie_chart.ts +++ b/test/functional/apps/visualize/_pie_chart.ts @@ -27,6 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('pie chart', function () { const vizName1 = 'Visualization PieChart'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickPieChart'); diff --git a/test/functional/apps/visualize/_point_series_options.ts b/test/functional/apps/visualize/_point_series_options.ts index d4bcc19a7c87c..b81feaf29e194 100644 --- a/test/functional/apps/visualize/_point_series_options.ts +++ b/test/functional/apps/visualize/_point_series_options.ts @@ -61,7 +61,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } describe('vlad point series', function describeIndexTests() { - before(initChart); + before(async () => { + await PageObjects.visualize.initTests(); + await initChart(); + }); describe('secondary value axis', function () { it('should show correct chart', async function () { diff --git a/test/functional/apps/visualize/_region_map.ts b/test/functional/apps/visualize/_region_map.ts index 3801d7d0cec12..916e8dbaee3a0 100644 --- a/test/functional/apps/visualize/_region_map.ts +++ b/test/functional/apps/visualize/_region_map.ts @@ -18,6 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'timePicker']); before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickRegionMap'); diff --git a/test/functional/apps/visualize/_shared_item.ts b/test/functional/apps/visualize/_shared_item.ts index a5dbe6c692978..3f9016ca2ff82 100644 --- a/test/functional/apps/visualize/_shared_item.ts +++ b/test/functional/apps/visualize/_shared_item.ts @@ -17,6 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('data-shared-item', function indexPatternCreation() { before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.common.navigateToApp('visualize'); }); diff --git a/test/functional/apps/visualize/_tag_cloud.ts b/test/functional/apps/visualize/_tag_cloud.ts index c7d864e5cfb23..a6ac324d9dc61 100644 --- a/test/functional/apps/visualize/_tag_cloud.ts +++ b/test/functional/apps/visualize/_tag_cloud.ts @@ -34,6 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const termsField = 'machine.ram'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickTagCloud'); diff --git a/test/functional/apps/visualize/_tile_map.ts b/test/functional/apps/visualize/_tile_map.ts index 3af467affa1fb..812b6a7d86802 100644 --- a/test/functional/apps/visualize/_tile_map.ts +++ b/test/functional/apps/visualize/_tile_map.ts @@ -28,6 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('tile map visualize app', function () { describe('incomplete config', function describeIndexTests() { before(async function () { + await PageObjects.visualize.initTests(); await browser.setWindowSize(1280, 1000); log.debug('navigateToApp visualize'); @@ -125,26 +126,26 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Fit data bounds should zoom to level 3', async function () { const expectedPrecision2DataTable = [ - ['-', 'dr4', '127', { lat: 40, lon: -76 }], - ['-', 'dr7', '92', { lat: 41, lon: -74 }], - ['-', '9q5', '91', { lat: 34, lon: -119 }], - ['-', '9qc', '89', { lat: 38, lon: -122 }], - ['-', 'drk', '87', { lat: 41, lon: -73 }], - ['-', 'dps', '82', { lat: 42, lon: -84 }], - ['-', 'dph', '82', { lat: 40, lon: -84 }], - ['-', 'dp3', '79', { lat: 41, lon: -88 }], - ['-', 'dpe', '78', { lat: 42, lon: -86 }], - ['-', 'dp8', '77', { lat: 43, lon: -90 }], - ['-', 'dp6', '74', { lat: 41, lon: -87 }], - ['-', 'djv', '74', { lat: 33, lon: -83 }], - ['-', '9qh', '74', { lat: 34, lon: -118 }], - ['-', 'dpq', '73', { lat: 41, lon: -81 }], - ['-', 'dpp', '73', { lat: 40, lon: -80 }], - ['-', '9y7', '73', { lat: 35, lon: -97 }], - ['-', '9vg', '73', { lat: 32, lon: -97 }], - ['-', 'drs', '71', { lat: 42, lon: -73 }], - ['-', '9ys', '71', { lat: 37, lon: -95 }], - ['-', '9yn', '71', { lat: 34, lon: -93 }], + ['-', 'dn', '1,429', { lat: 36, lon: -85 }], + ['-', 'dp', '1,418', { lat: 41, lon: -85 }], + ['-', '9y', '1,215', { lat: 36, lon: -96 }], + ['-', '9z', '1,099', { lat: 42, lon: -96 }], + ['-', 'dr', '1,076', { lat: 42, lon: -74 }], + ['-', 'dj', '982', { lat: 31, lon: -85 }], + ['-', '9v', '938', { lat: 31, lon: -96 }], + ['-', '9q', '722', { lat: 36, lon: -120 }], + ['-', '9w', '475', { lat: 36, lon: -107 }], + ['-', 'cb', '457', { lat: 46, lon: -96 }], + ['-', 'c2', '453', { lat: 47, lon: -120 }], + ['-', '9x', '420', { lat: 41, lon: -107 }], + ['-', 'dq', '399', { lat: 37, lon: -78 }], + ['-', '9r', '396', { lat: 41, lon: -120 }], + ['-', '9t', '274', { lat: 32, lon: -107 }], + ['-', 'c8', '271', { lat: 47, lon: -107 }], + ['-', 'dh', '214', { lat: 26, lon: -82 }], + ['-', 'b6', '207', { lat: 60, lon: -162 }], + ['-', 'bd', '206', { lat: 59, lon: -153 }], + ['-', 'b7', '167', { lat: 64, lon: -163 }], ]; await PageObjects.tileMap.clickMapFitDataBounds(); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 6568eab0fc1f4..690db676cb368 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -17,6 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const inspector = getService('inspector'); const retry = getService('retry'); const security = getService('security'); + const PageObjects = getPageObjects([ 'visualize', 'visualBuilder', @@ -27,12 +28,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('visual builder', function describeIndexTests() { this.tags('includeFirefox'); + + before(async () => { + await PageObjects.visualize.initTests(); + }); + beforeEach(async () => { - await security.testUser.setRoles([ - 'kibana_admin', - 'test_logstash_reader', - 'kibana_sample_admin', - ]); + await security.testUser.setRoles( + ['kibana_admin', 'test_logstash_reader', 'kibana_sample_admin'], + false + ); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisualBuilder(); await PageObjects.visualBuilder.checkVisualBuilderIsPresent(); @@ -141,7 +146,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await security.testUser.restoreDefaults(); - await esArchiver.unload('index_pattern_without_timefield'); + await esArchiver.load('empty_kibana'); + await PageObjects.visualize.initTests(); }); const switchIndexTest = async (useKibanaIndexes: boolean) => { diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/_tsvb_markdown.ts index 880255eede5aa..89db60bc7645c 100644 --- a/test/functional/apps/visualize/_tsvb_markdown.ts +++ b/test/functional/apps/visualize/_tsvb_markdown.ts @@ -11,7 +11,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { visualBuilder, timePicker } = getPageObjects(['visualBuilder', 'timePicker']); + const { visualBuilder, timePicker, visualize } = getPageObjects([ + 'visualBuilder', + 'timePicker', + 'visualize', + ]); const retry = getService('retry'); async function cleanupMarkdownData(variableName: 'variable' | 'label', checkedValue: string) { @@ -31,6 +35,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('visual builder', function describeIndexTests() { describe('markdown', () => { before(async () => { + await visualize.initTests(); await visualBuilder.resetPage(); await visualBuilder.clickMarkdown(); await timePicker.setAbsoluteRange( diff --git a/test/functional/apps/visualize/_tsvb_table.ts b/test/functional/apps/visualize/_tsvb_table.ts index 662ca59dc192d..abe3b799e4711 100644 --- a/test/functional/apps/visualize/_tsvb_table.ts +++ b/test/functional/apps/visualize/_tsvb_table.ts @@ -18,6 +18,9 @@ export default function ({ getPageObjects }: FtrProviderContext) { ]); describe('visual builder', function describeIndexTests() { + before(async () => { + await visualize.initTests(); + }); describe('table', () => { beforeEach(async () => { await visualBuilder.resetPage('Sep 22, 2015 @ 06:00:00.000', 'Sep 22, 2015 @ 11:00:00.000'); diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index 85d445bc34e6c..a0c9d806facc6 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -11,12 +11,24 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { visualize, visualBuilder } = getPageObjects(['visualBuilder', 'visualize']); + const { visualize, visualBuilder, timeToVisualize, dashboard } = getPageObjects([ + 'visualBuilder', + 'visualize', + 'timeToVisualize', + 'dashboard', + ]); + const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const filterBar = getService('filterBar'); + const elasticChart = getService('elasticChart'); const log = getService('log'); + const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); describe('visual builder', function describeIndexTests() { + before(async () => { + await visualize.initTests(); + }); beforeEach(async () => { await visualize.navigateToNewVisualization(); await visualize.clickVisualBuilder(); @@ -140,6 +152,36 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); }); + + describe('Clicking on the chart', () => { + it(`should create a filter`, async () => { + await visualBuilder.setMetricsGroupByTerms('machine.os.raw'); + await visualBuilder.clickSeriesOption(); + await testSubjects.click('visualizeSaveButton'); + + await timeToVisualize.saveFromModal('My TSVB viz 1', { + addToDashboard: 'new', + saveToLibrary: false, + }); + + await dashboard.waitForRenderComplete(); + const el = await elasticChart.getCanvas(); + // click on specific coordinates + await browser + .getActions() + .move({ x: 100, y: 110, origin: el._webElement }) + .click() + .perform(); + + await retry.try(async () => { + await testSubjects.click('applyFiltersPopoverButton'); + await testSubjects.missingOrFail('applyFiltersPopoverButton'); + }); + + const hasMachineRawFilter = await filterBar.hasFilter('machine.os.raw', 'win 7'); + expect(hasMachineRawFilter).to.be(true); + }); + }); }); }); } diff --git a/test/functional/apps/visualize/_vega_chart.ts b/test/functional/apps/visualize/_vega_chart.ts index 15d6e81c659f9..da33b390925a4 100644 --- a/test/functional/apps/visualize/_vega_chart.ts +++ b/test/functional/apps/visualize/_vega_chart.ts @@ -43,6 +43,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('vega chart in visualize app', () => { before(async () => { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); log.debug('clickVega'); diff --git a/test/functional/apps/visualize/_vertical_bar_chart.ts b/test/functional/apps/visualize/_vertical_bar_chart.ts index 5dafdd5b04010..1fe0d2f9a955b 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.ts +++ b/test/functional/apps/visualize/_vertical_bar_chart.ts @@ -19,6 +19,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('vertical bar chart', function () { + before(async () => { + await PageObjects.visualize.initTests(); + }); + const vizName1 = 'Visualization VerticalBarChart'; const initBarChart = async () => { diff --git a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts index 34f401b5afff6..5f066e96c6e7c 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts +++ b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts @@ -39,7 +39,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); }; - before(initBarChart); + before(async () => { + await PageObjects.visualize.initTests(); + await initBarChart(); + }); it('should save and load', async function () { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); diff --git a/test/functional/apps/visualize/_visualize_listing.ts b/test/functional/apps/visualize/_visualize_listing.ts index 399fa73da4bb3..90e7da1696702 100644 --- a/test/functional/apps/visualize/_visualize_listing.ts +++ b/test/functional/apps/visualize/_visualize_listing.ts @@ -18,6 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('create and delete', function () { before(async function () { + await PageObjects.visualize.initTests(); await PageObjects.visualize.gotoVisualizationLandingPage(); await PageObjects.visualize.deleteAllVisualizations(); }); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 747494a690c7e..eb224b3c9b879 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -7,32 +7,25 @@ */ import { FtrProviderContext } from '../../ftr_provider_context.d'; -import { UI_SETTINGS } from '../../../../src/plugins/data/common'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const log = getService('log'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - const deployment = getService('deployment'); - let isOss = true; describe('visualize app', () => { before(async () => { log.debug('Starting visualize before method'); await browser.setWindowSize(1280, 800); + await esArchiver.load('empty_kibana'); + await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('long_window_logstash'); - await esArchiver.load('visualize'); - await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', - }); - isOss = await deployment.isOss(); }); // TODO: Remove when vislib is removed - describe('new charts library', function () { + describe('new charts library visualize ciGroup7', function () { this.tags('ciGroup7'); before(async () => { @@ -58,7 +51,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); }); - describe('', function () { + describe('visualize ciGroup9', function () { this.tags('ciGroup9'); loadTestFile(require.resolve('./_embedding_chart')); @@ -66,14 +59,10 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_data_table')); loadTestFile(require.resolve('./_data_table_nontimeindex')); loadTestFile(require.resolve('./_data_table_notimeindex_filters')); - - // this check is not needed when the CI doesn't run anymore for the OSS - if (!isOss) { - loadTestFile(require.resolve('./_chart_types')); - } + loadTestFile(require.resolve('./_chart_types')); }); - describe('', function () { + describe('visualize ciGroup10', function () { this.tags('ciGroup10'); loadTestFile(require.resolve('./_inspector')); @@ -85,7 +74,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_metric_chart')); }); - describe('', function () { + describe('visualize ciGroup4', function () { this.tags('ciGroup4'); loadTestFile(require.resolve('./_line_chart_split_series')); @@ -98,14 +87,11 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_linked_saved_searches')); loadTestFile(require.resolve('./_visualize_listing')); loadTestFile(require.resolve('./_add_to_dashboard.ts')); - - if (isOss) { - loadTestFile(require.resolve('./_tile_map')); - loadTestFile(require.resolve('./_region_map')); - } + loadTestFile(require.resolve('./_tile_map')); + loadTestFile(require.resolve('./_region_map')); }); - describe('', function () { + describe('visualize ciGroup12', function () { this.tags('ciGroup12'); loadTestFile(require.resolve('./_tag_cloud')); diff --git a/test/functional/apps/visualize/input_control_vis/chained_controls.ts b/test/functional/apps/visualize/input_control_vis/chained_controls.ts index 7172be6c96966..18d1367b37e72 100644 --- a/test/functional/apps/visualize/input_control_vis/chained_controls.ts +++ b/test/functional/apps/visualize/input_control_vis/chained_controls.ts @@ -21,6 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { this.tags('includeFirefox'); before(async () => { + await PageObjects.visualize.initTests(); await PageObjects.common.navigateToApp('visualize'); await PageObjects.visualize.loadSavedVisualization('chained input control', { navigateToVisualize: false, diff --git a/test/functional/apps/visualize/input_control_vis/dynamic_options.ts b/test/functional/apps/visualize/input_control_vis/dynamic_options.ts index babe5a61a0cbb..633ba40bb0493 100644 --- a/test/functional/apps/visualize/input_control_vis/dynamic_options.ts +++ b/test/functional/apps/visualize/input_control_vis/dynamic_options.ts @@ -16,6 +16,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/98974 describe.skip('dynamic options', () => { + before(async () => { + await PageObjects.visualize.initTests(); + }); + describe('without chained controls', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('visualize'); diff --git a/test/functional/apps/visualize/input_control_vis/input_control_options.ts b/test/functional/apps/visualize/input_control_vis/input_control_options.ts index 2e3b5d758436e..82f003440364f 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_options.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_options.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('input control options', () => { before(async () => { + await PageObjects.visualize.initTests(); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickInputControlVis(); // set time range to time with no documents - input controls do not use time filter be default diff --git a/test/functional/apps/visualize/input_control_vis/input_control_range.ts b/test/functional/apps/visualize/input_control_vis/input_control_range.ts index caa008080b2a3..97e746ba4a4c0 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_range.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_range.ts @@ -14,10 +14,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const find = getService('find'); const security = getService('security'); + const PageObjects = getPageObjects(['visualize']); + const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']); describe('input control range', () => { before(async () => { + await PageObjects.visualize.initTests(); await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await esArchiver.load('kibana_sample_data_flights_index_pattern'); await visualize.navigateToNewVisualization(); @@ -48,10 +51,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await esArchiver.unload('kibana_sample_data_flights_index_pattern'); - // loading back default data - await esArchiver.loadIfNeeded('logstash_functional'); - await esArchiver.loadIfNeeded('long_window_logstash'); - await esArchiver.load('visualize'); await security.testUser.restoreDefaults(); }); }); diff --git a/test/functional/apps/visualize/legacy/_data_table.ts b/test/functional/apps/visualize/legacy/_data_table.ts index 41ddbd2dfc236..6613e3d13a31b 100644 --- a/test/functional/apps/visualize/legacy/_data_table.ts +++ b/test/functional/apps/visualize/legacy/_data_table.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('legacy data table visualization', function indexPatternCreation() { before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/legacy/index.ts b/test/functional/apps/visualize/legacy/index.ts index 677f1b33df2b6..187e8f3f3a663 100644 --- a/test/functional/apps/visualize/legacy/index.ts +++ b/test/functional/apps/visualize/legacy/index.ts @@ -9,19 +9,21 @@ import { FtrProviderContext } from '../../../ftr_provider_context.d'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; -export default function ({ getService, loadTestFile }: FtrProviderContext) { +export default function ({ getPageObjects, getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const log = getService('log'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['visualize']); describe('visualize with legacy visualizations', () => { before(async () => { + await PageObjects.visualize.initTests(); log.debug('Starting visualize legacy before method'); await browser.setWindowSize(1280, 800); await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('long_window_logstash'); - await esArchiver.load('visualize'); + await kibanaServer.importExport.load('visualize'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', diff --git a/test/functional/config.js b/test/functional/config.js index 1048bd72dc575..4a6791a3bc62f 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -14,6 +14,7 @@ export default async function ({ readConfigFile }) { return { testFiles: [ + require.resolve('./apps/status_page'), require.resolve('./apps/bundles'), require.resolve('./apps/console'), require.resolve('./apps/context'), @@ -23,7 +24,6 @@ export default async function ({ readConfigFile }) { require.resolve('./apps/home'), require.resolve('./apps/management'), require.resolve('./apps/saved_objects_management'), - require.resolve('./apps/status_page'), require.resolve('./apps/timelion'), require.resolve('./apps/visualize'), ], @@ -36,13 +36,15 @@ export default async function ({ readConfigFile }) { ...commonConfig.get('esTestCluster'), serverArgs: ['xpack.security.enabled=false'], }, + kbnTestServer: { ...commonConfig.get('kbnTestServer'), serverArgs: [ ...commonConfig.get('kbnTestServer.serverArgs'), - '--oss', '--telemetry.optIn=false', + '--xpack.security.enabled=false', '--savedObjects.maxImportPayloadBytes=10485760', + '--xpack.maps.showMapVisualizationTypes=true', ], }, diff --git a/test/functional/fixtures/kbn_archiver/visualize.json b/test/functional/fixtures/kbn_archiver/visualize.json new file mode 100644 index 0000000000000..758841e8d81ef --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/visualize.json @@ -0,0 +1,300 @@ +{ + "attributes": { + "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}", + "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "8.0.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzI2LDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "chained input control with dynamic options", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"chained input control with dynamic options\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759550755\",\"fieldName\":\"machine.os.raw\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559759557302\",\"fieldName\":\"geo.src\",\"parent\":\"1559759550755\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "5d2de430-87c0-11e9-a991-3b492a7c3e09", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "control_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "logstash-*", + "name": "control_1_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-06-05T18:33:07.827Z", + "version": "WzMzLDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "dynamic options input control", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"dynamic options input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759127876\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "64983230-87bf-11e9-a991-3b492a7c3e09", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "control_0_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-06-05T18:26:10.771Z", + "version": "WzMyLDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "chained input control", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"chained input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559757816862\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559757836347\",\"fieldName\":\"clientip\",\"parent\":\"1559757816862\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "68305470-87bc-11e9-a991-3b492a7c3e09", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "control_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "logstash-*", + "name": "control_1_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-06-05T18:04:48.310Z", + "version": "WzMxLDJd" +} + +{ + "attributes": { + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]", + "title": "test_index*" + }, + "id": "test_index*", + "references": [], + "type": "index-pattern", + "version": "WzI1LDJd" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "AreaChart [no date field]", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"AreaChart [no date field]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "AreaChart-no-date-field", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "test_index*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzM0LDJd" +} + +{ + "attributes": { + "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "title": "log*" + }, + "coreMigrationVersion": "8.0.0", + "id": "log*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzM1LDJd" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "AreaChart [no time filter]", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"AreaChart [no time filter]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "AreaChart-no-time-filter", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "log*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzM2LDJd" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Shared-Item Visualization AreaChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + }, + "coreMigrationVersion": "8.0.0", + "id": "Shared-Item-Visualization-AreaChart", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzI5LDJd" +} + +{ + "attributes": { + "description": "VegaMap", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "VegaMap", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\\"OriginAirportID\\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"OriginLocation\\\", \\\"Origin\\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\\"DestAirportID\\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"DestLocation\\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.origins.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\\"!selected\\\", remove: true}\\n {trigger: \\\"selected\\\", insert: \\\"selected\\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\\"@airport:mouseover\\\", update: \\\"datum\\\"}\\n {events: \\\"@airport:mouseout\\\", update: \\\"null\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n {signal: \\\"zoom*zoom*0.2+1\\\"}\\n {signal: \\\"zoom*zoom*10+1\\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\\"formula\\\", expr: \\\"{x:parent.x, y:parent.y}\\\", as: \\\"source\\\"}\\n {type: \\\"formula\\\", expr: \\\"{x:datum.x, y:datum.y}\\\", as: \\\"target\\\"}\\n {type: \\\"linkpath\\\", shape: \\\"diagonal\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\\"facetDatumElems\\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\\"path\\\"}\\n stroke: {value: \\\"black\\\"}\\n strokeWidth: {scale: \\\"lineThickness\\\", field: \\\"doc_count\\\"}\\n strokeOpacity: {scale: \\\"lineOpacity\\\", field: \\\"doc_count\\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"airportSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{title: datum.originLocation.hits.hits[0]._source.Origin + ' (' + datum.key + ')', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"},\"title\":\"[Flights] Airport Connections (Hover Over Airport)\",\"type\":\"vega\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "VegaMap", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [], + "type": "visualization", + "version": "WzM3LDJd" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Visualization AreaChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Visualization AreaChart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "Visualization-AreaChart", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzMwLDJd" +} + +{ + "attributes": { + "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "title": "logstash*" + }, + "coreMigrationVersion": "8.0.0", + "id": "logstash*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzI3LDJd" +} + +{ + "attributes": { + "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "long-window-logstash-*" + }, + "coreMigrationVersion": "8.0.0", + "id": "long-window-logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzI4LDJd" +} \ No newline at end of file diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index 05f7fb7eecb3d..f03f74ef8c61d 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -12,9 +12,7 @@ export function HomePageProvider({ getService, getPageObjects }: FtrProviderCont const testSubjects = getService('testSubjects'); const retry = getService('retry'); const find = getService('find'); - const deployment = getService('deployment'); const PageObjects = getPageObjects(['common']); - let isOss = true; class HomePage { async clickSynopsis(title: string) { @@ -72,10 +70,7 @@ export function HomePageProvider({ getService, getPageObjects }: FtrProviderCont async launchSampleDashboard(id: string) { await this.launchSampleDataSet(id); - isOss = await deployment.isOss(); - if (!isOss) { - await find.clickByLinkText('Dashboard'); - } + await find.clickByLinkText('Dashboard'); } async launchSampleDataSet(id: string) { diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 1ccea86905431..9a4c01f0f2767 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -8,6 +8,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; import { VisualizeConstants } from '../../../src/plugins/visualize/public/application/visualize_constants'; +import { UI_SETTINGS } from '../../../src/plugins/data/common'; // TODO: Remove & Refactor to use the TTV page objects interface VisualizeSaveModalArgs { @@ -23,6 +24,7 @@ type DashboardPickerOption = | 'new-dashboard-option'; export function VisualizePageProvider({ getService, getPageObjects }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); const find = getService('find'); @@ -48,6 +50,16 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide LOGSTASH_NON_TIME_BASED: 'logstash*', }; + public async initTests() { + await kibanaServer.savedObjects.clean({ types: ['visualization'] }); + await kibanaServer.importExport.load('visualize'); + + await kibanaServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', + }); + } + public async gotoVisualizationLandingPage() { await common.navigateToApp('visualize'); } diff --git a/test/plugin_functional/test_suites/doc_views/index.ts b/test/plugin_functional/test_suites/doc_views/index.ts index 2fed8e10ffc8e..e02b9ac4646f6 100644 --- a/test/plugin_functional/test_suites/doc_views/index.ts +++ b/test/plugin_functional/test_suites/doc_views/index.ts @@ -11,7 +11,8 @@ import { PluginFunctionalProviderContext } from '../../services'; export default function ({ getService, loadTestFile }: PluginFunctionalProviderContext) { const esArchiver = getService('esArchiver'); - describe('doc views', function () { + // SKIPPED: https://github.com/elastic/kibana/issues/100060 + describe.skip('doc views', function () { before(async () => { await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/discover'); }); diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index fa0c9522ef5fb..32fb98929e21c 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash +cd "$KIBANA_DIR" source src/dev/ci_setup/setup_env.sh if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then @@ -12,16 +13,50 @@ export KBN_NP_PLUGINS_BUILT=true echo " -> Ensuring all functional tests are in a ciGroup" node scripts/ensure_all_tests_in_ci_group; +echo " -> Ensuring all x-pack functional tests are in a ciGroup" +node x-pack/scripts/functional_tests --assert-none-excluded \ + --include-tag ciGroup1 \ + --include-tag ciGroup2 \ + --include-tag ciGroup3 \ + --include-tag ciGroup4 \ + --include-tag ciGroup5 \ + --include-tag ciGroup6 \ + --include-tag ciGroup7 \ + --include-tag ciGroup8 \ + --include-tag ciGroup9 \ + --include-tag ciGroup10 \ + --include-tag ciGroup11 \ + --include-tag ciGroup12 \ + --include-tag ciGroup13 \ + --include-tag ciGroupDocker + # Do not build kibana for code coverage run if [[ -z "$CODE_COVERAGE" ]] ; then - echo " -> building and extracting OSS Kibana distributable for use in functional tests" - node scripts/build --debug --oss + echo " -> building and extracting default Kibana distributable for use in functional tests" + node scripts/build --debug --no-oss echo " -> shipping metrics from build to ci-stats" node scripts/ship_ci_stats \ --metrics target/optimizer_bundle_metrics.json \ --metrics packages/kbn-ui-shared-deps/target/metrics.json - mkdir -p "$WORKSPACE/kibana-build-oss" - cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/ + linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" + installDir="$KIBANA_DIR/install/kibana" + mkdir -p "$installDir" + tar -xzf "$linuxBuild" -C "$installDir" --strip=1 + cp "$linuxBuild" "$WORKSPACE/kibana-default.tar.gz" + + mkdir -p "$WORKSPACE/kibana-build" + cp -pR install/kibana/. $WORKSPACE/kibana-build/ + + echo " -> Archive built plugins" + shopt -s globstar + tar -zcf \ + "$WORKSPACE/kibana-default-plugins.tar.gz" \ + x-pack/plugins/**/target/public \ + x-pack/test/**/target/public \ + examples/**/target/public \ + x-pack/examples/**/target/public \ + test/**/target/public + shopt -u globstar fi diff --git a/test/scripts/jenkins_build_load_testing.sh b/test/scripts/jenkins_build_load_testing.sh index a635e34bcbeda..dd9cedea9c4d4 100755 --- a/test/scripts/jenkins_build_load_testing.sh +++ b/test/scripts/jenkins_build_load_testing.sh @@ -64,8 +64,8 @@ installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 -mkdir -p "$WORKSPACE/kibana-build-xpack" -cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ +mkdir -p "$WORKSPACE/kibana-build" +cp -pR install/kibana/. $WORKSPACE/kibana-build/ echo " -> Setup env for tests" source test/scripts/jenkins_test_setup_xpack.sh diff --git a/test/scripts/jenkins_build_plugins.sh b/test/scripts/jenkins_build_plugins.sh index 962a126ab2700..131bf0121fa14 100755 --- a/test/scripts/jenkins_build_plugins.sh +++ b/test/scripts/jenkins_build_plugins.sh @@ -4,10 +4,19 @@ source src/dev/ci_setup/setup_env.sh echo " -> building kibana platform plugins" node scripts/build_kibana_platform_plugins \ - --oss \ --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ --scan-dir "$KIBANA_DIR/test/common/fixtures/plugins" \ --scan-dir "$KIBANA_DIR/examples" \ - --workers 6 \ - --verbose + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/common/fixtures/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ + --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ + --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ + --scan-dir "$XPACK_DIR/test/licensing_plugin/plugins" \ + --scan-dir "$XPACK_DIR/test/usage_collection/plugins" \ + --scan-dir "$XPACK_DIR/test/security_functional/fixtures/common" \ + --scan-dir "$XPACK_DIR/examples" \ + --workers 12 diff --git a/test/scripts/jenkins_test_setup_oss.sh b/test/scripts/jenkins_test_setup_oss.sh index 53626ce89462a..29d396667c465 100755 --- a/test/scripts/jenkins_test_setup_oss.sh +++ b/test/scripts/jenkins_test_setup_oss.sh @@ -3,11 +3,11 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]]; then - destDir="$WORKSPACE/kibana-build-oss-${TASK_QUEUE_PROCESS_ID:-$CI_PARALLEL_PROCESS_NUMBER}" + destDir="$WORKSPACE/kibana-build-${TASK_QUEUE_PROCESS_ID:-$CI_PARALLEL_PROCESS_NUMBER}" if [[ ! -d $destDir ]]; then mkdir -p $destDir - cp -pR "$WORKSPACE/kibana-build-oss/." $destDir/ + cp -pR "$WORKSPACE/kibana-build/." $destDir/ fi export KIBANA_INSTALL_DIR="$destDir" diff --git a/test/scripts/jenkins_test_setup_xpack.sh b/test/scripts/jenkins_test_setup_xpack.sh index b9227fd8ff416..31acc4f4865e2 100755 --- a/test/scripts/jenkins_test_setup_xpack.sh +++ b/test/scripts/jenkins_test_setup_xpack.sh @@ -3,11 +3,11 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]]; then - destDir="$WORKSPACE/kibana-build-xpack-${TASK_QUEUE_PROCESS_ID:-$CI_PARALLEL_PROCESS_NUMBER}" + destDir="$WORKSPACE/kibana-build-${TASK_QUEUE_PROCESS_ID:-$CI_PARALLEL_PROCESS_NUMBER}" if [[ ! -d $destDir ]]; then mkdir -p $destDir - cp -pR "$WORKSPACE/kibana-build-xpack/." $destDir/ + cp -pR "$WORKSPACE/kibana-build/." $destDir/ fi export KIBANA_INSTALL_DIR="$(realpath $destDir)" diff --git a/test/scripts/jenkins_xpack_baseline.sh b/test/scripts/jenkins_xpack_baseline.sh index 2755a6e0a705d..93363687b39a9 100755 --- a/test/scripts/jenkins_xpack_baseline.sh +++ b/test/scripts/jenkins_xpack_baseline.sh @@ -17,8 +17,8 @@ installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 -mkdir -p "$WORKSPACE/kibana-build-xpack" -cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ +mkdir -p "$WORKSPACE/kibana-build" +cp -pR install/kibana/. $WORKSPACE/kibana-build/ cd "$KIBANA_DIR" source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh deleted file mode 100755 index b5fd7492d7961..0000000000000 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash - -cd "$KIBANA_DIR" -source src/dev/ci_setup/setup_env.sh - -if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then - ./test/scripts/jenkins_xpack_build_plugins.sh -fi - -# doesn't persist, also set in kibanaPipeline.groovy -export KBN_NP_PLUGINS_BUILT=true - -echo " -> Ensuring all functional tests are in a ciGroup" -cd "$XPACK_DIR" -node scripts/functional_tests --assert-none-excluded \ - --include-tag ciGroup1 \ - --include-tag ciGroup2 \ - --include-tag ciGroup3 \ - --include-tag ciGroup4 \ - --include-tag ciGroup5 \ - --include-tag ciGroup6 \ - --include-tag ciGroup7 \ - --include-tag ciGroup8 \ - --include-tag ciGroup9 \ - --include-tag ciGroup10 \ - --include-tag ciGroup11 \ - --include-tag ciGroup12 \ - --include-tag ciGroup13 \ - --include-tag ciGroupDocker - -# Do not build kibana for code coverage run -if [[ -z "$CODE_COVERAGE" ]] ; then - echo " -> building and extracting default Kibana distributable for use in functional tests" - cd "$KIBANA_DIR" - node scripts/build --debug --no-oss - - echo " -> shipping metrics from build to ci-stats" - node scripts/ship_ci_stats \ - --metrics target/optimizer_bundle_metrics.json \ - --metrics packages/kbn-ui-shared-deps/target/metrics.json - - linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" - installDir="$KIBANA_DIR/install/kibana" - mkdir -p "$installDir" - tar -xzf "$linuxBuild" -C "$installDir" --strip=1 - cp "$linuxBuild" "$WORKSPACE/kibana-default.tar.gz" - - mkdir -p "$WORKSPACE/kibana-build-xpack" - cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ - - echo " -> Archive built plugins" - shopt -s globstar - tar -zcf \ - "$WORKSPACE/kibana-default-plugins.tar.gz" \ - x-pack/plugins/**/target/public \ - x-pack/test/**/target/public \ - examples/**/target/public \ - x-pack/examples/**/target/public \ - test/**/target/public - shopt -u globstar -fi diff --git a/test/server_integration/http/ssl_redirect/index.js b/test/server_integration/http/ssl_redirect/index.js index bcd1b6f25ea51..8abe700e26149 100644 --- a/test/server_integration/http/ssl_redirect/index.js +++ b/test/server_integration/http/ssl_redirect/index.js @@ -17,7 +17,7 @@ export default function ({ getService }) { await supertest.get('/').expect('location', url).expect(302); - await supertest.get('/').redirects(1).expect('location', '/app/home').expect(302); + await supertest.get('/').redirects(1).expect('location', '/spaces/enter').expect(302); }); }); } diff --git a/typings/elasticsearch/search.d.ts b/typings/elasticsearch/search.d.ts index c9bf3b1d8b7bc..d75f31d388176 100644 --- a/typings/elasticsearch/search.d.ts +++ b/typings/elasticsearch/search.d.ts @@ -49,7 +49,7 @@ type ValueTypeOfField = T extends Record type MaybeArray = T | T[]; -type Fields = MaybeArray; +type Fields = Exclude['body']['fields'], undefined>; type DocValueFields = MaybeArray; export type SearchHit< @@ -58,7 +58,7 @@ export type SearchHit< TDocValueFields extends DocValueFields | undefined = undefined > = Omit & (TSource extends false ? {} : { _source: TSource }) & - (TFields extends estypes.Fields + (TFields extends Fields ? { fields: Partial, unknown[]>>; } @@ -77,7 +77,7 @@ type HitsOf< > = Array< SearchHit< TOptions extends { _source: false } ? undefined : TDocument, - TOptions extends { fields: estypes.Fields } ? TOptions['fields'] : undefined, + TOptions extends { fields: Fields } ? TOptions['fields'] : undefined, TOptions extends { docvalue_fields: DocValueFields } ? TOptions['docvalue_fields'] : undefined > >; diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 74d3a67f4a1b8..63585a9f764e3 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -194,12 +194,12 @@ def runTests() { 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), 'kibana-oss-agent' : workers.functional( 'kibana-oss-tests', - { kibanaPipeline.buildOss() }, + { kibanaPipeline.buildKibana() }, ossProks() ), 'kibana-xpack-agent' : workers.functional( 'kibana-xpack-tests', - { kibanaPipeline.buildXpack() }, + { kibanaPipeline.buildKibana() }, xpackProks() ), ]) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 6cccdfaefecba..31cd129d87749 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -303,33 +303,23 @@ def doSetup() { } } -def buildOss(maxWorkers = '') { - notifyOnError { - withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { - runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") - } - } -} - def getBuildArtifactBucket() { def dir = env.ghprbPullId ? "pr-${env.ghprbPullId}" : buildState.get('checkoutInfo').branch.replace("/", "__") return "gs://ci-artifacts.kibana.dev/default-build/${dir}/${buildState.get('checkoutInfo').commit}" } -def buildXpack(maxWorkers = '', uploadArtifacts = false) { +def buildKibana(maxWorkers = '') { notifyOnError { withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { - runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") + runbld("./test/scripts/jenkins_build_kibana.sh", "Build Kibana") } - if (uploadArtifacts) { - withGcpServiceAccount.fromVaultSecret('secret/kibana-issues/dev/ci-artifacts-key', 'value') { - bash(""" - cd "${env.WORKSPACE}" - gsutil -q -m cp 'kibana-default.tar.gz' '${getBuildArtifactBucket()}/' - gsutil -q -m cp 'kibana-default-plugins.tar.gz' '${getBuildArtifactBucket()}/' - """, "Upload Default Build artifacts to GCS") - } + withGcpServiceAccount.fromVaultSecret('secret/kibana-issues/dev/ci-artifacts-key', 'value') { + bash(""" + cd "${env.WORKSPACE}" + gsutil -q -m cp 'kibana-default.tar.gz' '${getBuildArtifactBucket()}/' + gsutil -q -m cp 'kibana-default-plugins.tar.gz' '${getBuildArtifactBucket()}/' + """, "Upload Default Build artifacts to GCS") } } } @@ -443,14 +433,10 @@ def withDocker(Closure closure) { ) } -def buildOssPlugins() { +def buildPlugins() { runbld('./test/scripts/jenkins_build_plugins.sh', 'Build OSS Plugins') } -def buildXpackPlugins() { - runbld('./test/scripts/jenkins_xpack_build_plugins.sh', 'Build X-Pack Plugins') -} - def withTasks(Map params = [:], Closure closure) { catchErrors { def config = [setupWork: {}, worker: [:], parallel: 24] + params @@ -466,8 +452,7 @@ def withTasks(Map params = [:], Closure closure) { }, // There are integration tests etc that require the plugins to be built first, so let's go ahead and build them before set up the parallel workspaces - ossPlugins: { buildOssPlugins() }, - xpackPlugins: { buildXpackPlugins() }, + plugins: { buildPlugins() }, ]) config.setupWork() @@ -487,8 +472,11 @@ def allCiTasks() { tasks.check() tasks.lint() tasks.test() - tasks.functionalOss() - tasks.functionalXpack() + task { + buildKibana(16) + tasks.functionalOss() + tasks.functionalXpack() + } tasks.storybooksCi() } }, diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 1d33fd1249681..e6061de2987ba 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -55,8 +55,8 @@ def xpackCiGroupDocker() { kibanaPipeline.downloadDefaultBuildArtifacts() kibanaPipeline.bash(""" cd '${env.WORKSPACE}' - mkdir -p kibana-build-xpack - tar -xzf kibana-default.tar.gz -C kibana-build-xpack --strip=1 + mkdir -p kibana-build + tar -xzf kibana-default.tar.gz -C kibana-build --strip=1 tar -xzf kibana-default-plugins.tar.gz -C kibana """, "Extract Default Build artifacts") kibanaPipeline.xpackCiGroupProcess('Docker', true)() @@ -75,8 +75,6 @@ def functionalOss(Map params = [:]) { ] task { - kibanaPipeline.buildOss(6) - if (config.ciGroups) { ossCiGroups() } @@ -115,8 +113,6 @@ def functionalXpack(Map params = [:]) { ] task { - kibanaPipeline.buildXpack(10, true) - if (config.ciGroups) { xpackCiGroups() xpackCiGroupDocker() diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 7bc965f0aa5c8..27e3aa1df61f5 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -981,6 +981,11 @@ export class AlertsClient { ...this.apiKeyAsAlertAttributes(createdAPIKey, username), updatedBy: username, updatedAt: new Date().toISOString(), + executionStatus: { + status: 'pending', + lastExecutionDate: new Date().toISOString(), + error: null, + }, }); try { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts index 7b0d6d7b1f10b..8c0a09c74457e 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts @@ -248,6 +248,11 @@ describe('enable()', () => { }, }, ], + executionStatus: { + status: 'pending', + lastExecutionDate: '2019-02-12T21:01:22.479Z', + error: null, + }, }, { version: '123', @@ -352,6 +357,11 @@ describe('enable()', () => { }, }, ], + executionStatus: { + status: 'pending', + lastExecutionDate: '2019-02-12T21:01:22.479Z', + error: null, + }, }, { version: '123', diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 28e4a7b36e740..76d544c3bc6f5 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -10,7 +10,8 @@ "triggersActionsUi", "embeddable", "infra", - "observability" + "observability", + "ruleRegistry" ], "optionalPlugins": [ "spaces", @@ -26,8 +27,13 @@ ], "server": true, "ui": true, - "configPath": ["xpack", "apm"], - "extraPublicDirs": ["public/style/variables"], + "configPath": [ + "xpack", + "apm" + ], + "extraPublicDirs": [ + "public/style/variables" + ], "requiredBundles": [ "home", "kibanaReact", diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index e6415f76c60dc..4ec654a6c0bfd 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -39,7 +39,11 @@ describe('renderApp', () => { }); it('renders the app', () => { - const { core, config, apmRuleRegistry } = mockApmPluginContextValue; + const { + core, + config, + observabilityRuleTypeRegistry, + } = mockApmPluginContextValue; const plugins = { licensing: { license$: new Observable() }, triggersActionsUi: { actionTypeRegistry: {}, alertTypeRegistry: {} }, @@ -92,7 +96,7 @@ describe('renderApp', () => { appMountParameters: params as any, pluginsStart: startDeps as any, config, - apmRuleRegistry, + observabilityRuleTypeRegistry, }); }); diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index 17905074cfec1..11a2777f47f6a 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -12,6 +12,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router } from 'react-router-dom'; import { DefaultTheme, ThemeProvider } from 'styled-components'; +import type { ObservabilityRuleTypeRegistry } from '../../../observability/public'; import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider, @@ -26,11 +27,7 @@ import { ApmPluginContext } from '../context/apm_plugin/apm_plugin_context'; import { UrlParamsProvider } from '../context/url_params_context/url_params_context'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { ConfigSchema } from '../index'; -import { - ApmPluginSetupDeps, - ApmPluginStartDeps, - ApmRuleRegistry, -} from '../plugin'; +import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { px, units } from '../style/variables'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; @@ -77,14 +74,14 @@ export function CsmAppRoot({ deps, config, corePlugins: { embeddable, maps }, - apmRuleRegistry, + observabilityRuleTypeRegistry, }: { appMountParameters: AppMountParameters; core: CoreStart; deps: ApmPluginSetupDeps; config: ConfigSchema; corePlugins: ApmPluginStartDeps; - apmRuleRegistry: ApmRuleRegistry; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; }) { const { history } = appMountParameters; const i18nCore = core.i18n; @@ -94,7 +91,7 @@ export function CsmAppRoot({ config, core, plugins, - apmRuleRegistry, + observabilityRuleTypeRegistry, }; return ( @@ -125,14 +122,14 @@ export const renderApp = ({ appMountParameters, config, corePlugins, - apmRuleRegistry, + observabilityRuleTypeRegistry, }: { core: CoreStart; deps: ApmPluginSetupDeps; appMountParameters: AppMountParameters; config: ConfigSchema; corePlugins: ApmPluginStartDeps; - apmRuleRegistry: ApmRuleRegistry; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; }) => { const { element } = appMountParameters; @@ -151,7 +148,7 @@ export const renderApp = ({ deps={deps} config={config} corePlugins={corePlugins} - apmRuleRegistry={apmRuleRegistry} + observabilityRuleTypeRegistry={observabilityRuleTypeRegistry} />, element ); diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index acb55a02599f1..e2a0bdb6b48b1 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -13,6 +13,7 @@ import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; import 'react-vis/dist/style.css'; import { DefaultTheme, ThemeProvider } from 'styled-components'; +import type { ObservabilityRuleTypeRegistry } from '../../../observability/public'; import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { ConfigSchema } from '../'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; @@ -30,11 +31,7 @@ import { import { LicenseProvider } from '../context/license/license_context'; import { UrlParamsProvider } from '../context/url_params_context/url_params_context'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; -import { - ApmPluginSetupDeps, - ApmPluginStartDeps, - ApmRuleRegistry, -} from '../plugin'; +import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; import { setHelpExtension } from '../setHelpExtension'; @@ -112,14 +109,14 @@ export const renderApp = ({ appMountParameters, config, pluginsStart, - apmRuleRegistry, + observabilityRuleTypeRegistry, }: { coreStart: CoreStart; pluginsSetup: ApmPluginSetupDeps; appMountParameters: AppMountParameters; config: ConfigSchema; pluginsStart: ApmPluginStartDeps; - apmRuleRegistry: ApmRuleRegistry; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; }) => { const { element } = appMountParameters; const apmPluginContextValue = { @@ -127,7 +124,7 @@ export const renderApp = ({ config, core: coreStart, plugins: pluginsSetup, - apmRuleRegistry, + observabilityRuleTypeRegistry, }; // render APM feedback link in global help menu diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index 98c8b99411bc3..7e788016baad2 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -8,9 +8,19 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; import { stringify } from 'querystring'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_SEVERITY_LEVEL, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import type { ObservabilityRuleTypeRegistry } from '../../../../observability/public'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { AlertType } from '../../../common/alert_types'; -import type { ApmRuleRegistry } from '../../plugin'; + +// copied from elasticsearch_fieldnames.ts to limit page load bundle size +const SERVICE_ENVIRONMENT = 'service.environment'; +const SERVICE_NAME = 'service.name'; +const TRANSACTION_TYPE = 'transaction.type'; const format = ({ pathname, @@ -22,28 +32,32 @@ const format = ({ return `${pathname}?${stringify(query)}`; }; -export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { - apmRuleRegistry.registerType({ +export function registerApmAlerts( + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry +) { + observabilityRuleTypeRegistry.register({ id: AlertType.ErrorCount, description: i18n.translate('xpack.apm.alertTypes.errorCount.description', { defaultMessage: 'Alert when the number of errors in a service exceeds a defined threshold.', }), - format: ({ alert }) => { + format: ({ fields }) => { return { reason: i18n.translate('xpack.apm.alertTypes.errorCount.reason', { defaultMessage: `Error count is greater than {threshold} (current value is {measured}) for {serviceName}`, values: { - threshold: alert['kibana.observability.evaluation.threshold'], - measured: alert['kibana.observability.evaluation.value'], - serviceName: alert['service.name']!, + threshold: fields[ALERT_EVALUATION_THRESHOLD], + measured: fields[ALERT_EVALUATION_VALUE], + serviceName: String(fields[SERVICE_NAME][0]), }, }), link: format({ - pathname: `/app/apm/services/${alert['service.name']!}/errors`, + pathname: `/app/apm/services/${String( + fields[SERVICE_NAME][0] + )}/errors`, query: { - ...(alert['service.environment'] - ? { environment: alert['service.environment'] } + ...(fields[SERVICE_ENVIRONMENT]?.[0] + ? { environment: String(fields[SERVICE_ENVIRONMENT][0]) } : { environment: ENVIRONMENT_ALL.value }), }, }), @@ -71,7 +85,7 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { ), }); - apmRuleRegistry.registerType({ + observabilityRuleTypeRegistry.register({ id: AlertType.TransactionDuration, description: i18n.translate( 'xpack.apm.alertTypes.transactionDuration.description', @@ -80,28 +94,24 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { 'Alert when the latency of a specific transaction type in a service exceeds a defined threshold.', } ), - format: ({ alert, formatters: { asDuration } }) => ({ + format: ({ fields, formatters: { asDuration } }) => ({ reason: i18n.translate( 'xpack.apm.alertTypes.transactionDuration.reason', { defaultMessage: `Latency is above {threshold} (current value is {measured}) for {serviceName}`, values: { - threshold: asDuration( - alert['kibana.observability.evaluation.threshold'] - ), - measured: asDuration( - alert['kibana.observability.evaluation.value'] - ), - serviceName: alert['service.name']!, + threshold: asDuration(fields[ALERT_EVALUATION_THRESHOLD]), + measured: asDuration(fields[ALERT_EVALUATION_VALUE]), + serviceName: String(fields[SERVICE_NAME][0]), }, } ), link: format({ - pathname: `/app/apm/services/${alert['service.name']!}`, + pathname: `/app/apm/services/${fields[SERVICE_NAME][0]!}`, query: { - transactionType: alert['transaction.type']!, - ...(alert['service.environment'] - ? { environment: alert['service.environment'] } + transactionType: fields[TRANSACTION_TYPE][0]!, + ...(fields[SERVICE_ENVIRONMENT]?.[0] + ? { environment: String(fields[SERVICE_ENVIRONMENT][0]) } : { environment: ENVIRONMENT_ALL.value }), }, }), @@ -131,7 +141,7 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { ), }); - apmRuleRegistry.registerType({ + observabilityRuleTypeRegistry.register({ id: AlertType.TransactionErrorRate, description: i18n.translate( 'xpack.apm.alertTypes.transactionErrorRate.description', @@ -140,30 +150,24 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { 'Alert when the rate of transaction errors in a service exceeds a defined threshold.', } ), - format: ({ alert, formatters: { asPercent } }) => ({ + format: ({ fields, formatters: { asPercent } }) => ({ reason: i18n.translate( 'xpack.apm.alertTypes.transactionErrorRate.reason', { defaultMessage: `Transaction error rate is greater than {threshold} (current value is {measured}) for {serviceName}`, values: { - threshold: asPercent( - alert['kibana.observability.evaluation.threshold'], - 100 - ), - measured: asPercent( - alert['kibana.observability.evaluation.value'], - 100 - ), - serviceName: alert['service.name']!, + threshold: asPercent(fields[ALERT_EVALUATION_THRESHOLD], 100), + measured: asPercent(fields[ALERT_EVALUATION_VALUE], 100), + serviceName: String(fields[SERVICE_NAME][0]), }, } ), link: format({ - pathname: `/app/apm/services/${alert['service.name']!}`, + pathname: `/app/apm/services/${String(fields[SERVICE_NAME][0]!)}`, query: { - transactionType: alert['transaction.type']!, - ...(alert['service.environment'] - ? { environment: alert['service.environment'] } + transactionType: String(fields[TRANSACTION_TYPE][0]!), + ...(fields[SERVICE_ENVIRONMENT]?.[0] + ? { environment: String(fields[SERVICE_ENVIRONMENT][0]) } : { environment: ENVIRONMENT_ALL.value }), }, }), @@ -193,7 +197,7 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { ), }); - apmRuleRegistry.registerType({ + observabilityRuleTypeRegistry.register({ id: AlertType.TransactionDurationAnomaly, description: i18n.translate( 'xpack.apm.alertTypes.transactionDurationAnomaly.description', @@ -201,24 +205,24 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { defaultMessage: 'Alert when the latency of a service is abnormal.', } ), - format: ({ alert }) => ({ + format: ({ fields }) => ({ reason: i18n.translate( 'xpack.apm.alertTypes.transactionDurationAnomaly.reason', { defaultMessage: `{severityLevel} anomaly detected for {serviceName} (score was {measured})`, values: { - serviceName: alert['service.name'], - severityLevel: alert['kibana.rac.alert.severity.level'], - measured: alert['kibana.observability.evaluation.value'], + serviceName: String(fields[SERVICE_NAME][0]), + severityLevel: String(fields[ALERT_SEVERITY_LEVEL]), + measured: Number(fields[ALERT_EVALUATION_VALUE]), }, } ), link: format({ - pathname: `/app/apm/services/${alert['service.name']!}`, + pathname: `/app/apm/services/${String(fields[SERVICE_NAME][0])}`, query: { - transactionType: alert['transaction.type']!, - ...(alert['service.environment'] - ? { environment: alert['service.environment'] } + transactionType: String(fields[TRANSACTION_TYPE][0]), + ...(fields[SERVICE_ENVIRONMENT]?.[0] + ? { environment: String(fields[SERVICE_ENVIRONMENT][0]) } : { environment: ENVIRONMENT_ALL.value }), }, }), diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 19a567a3866bd..16ac1a35666d2 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -19,6 +19,7 @@ import { import { EuiTitle } from '@elastic/eui'; import d3 from 'd3'; import React from 'react'; +import { RULE_ID } from '@kbn/rule-data-utils/target/technical_field_names'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters'; @@ -115,7 +116,7 @@ export function ErrorDistribution({ distribution, title }: Props) { /> {getAlertAnnotations({ alerts: alerts?.filter( - (alert) => alert['rule.id'] === AlertType.ErrorCount + (alert) => alert[RULE_ID]?.[0] === AlertType.ErrorCount ), theme, })} diff --git a/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap index b1bcf561bed84..f13cce3fd9b40 100644 --- a/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap @@ -4,10 +4,6 @@ exports[`Home component should render services 1`] = ` alert['kibana.rac.alert.id']! + const collapsedAlerts = uniqBy(alerts, (alert) => alert[ALERT_ID]![0]!).map( + (alert) => { + return parseTechnicalFields(alert); + } ); return ( {collapsedAlerts.map((alert) => { - const ruleType = apmRuleRegistry.getTypeByRuleId(alert['rule.id']!); + const formatter = observabilityRuleTypeRegistry.getFormatter( + alert[RULE_ID]! + ); const formatted = { link: undefined, - reason: alert['rule.name'], - ...(ruleType?.format?.({ - alert, + reason: alert[RULE_NAME], + ...(formatter?.({ + fields: alert, formatters: { asDuration, asPercent }, }) ?? {}), }; @@ -55,7 +65,7 @@ export function AlertDetails({ alerts }: AlertDetailProps) { : undefined; return ( - + {parsedLink ? ( @@ -79,7 +89,7 @@ export function AlertDetails({ alerts }: AlertDetailProps) { diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts index f916292b7f080..d848c0d3ccd7a 100644 --- a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts @@ -14,6 +14,8 @@ import defaultIcon from '../span_icon/icons/default.svg'; import dotNetIcon from './icons/dot-net.svg'; import erlangIcon from './icons/erlang.svg'; import goIcon from './icons/go.svg'; +import iosIcon from './icons/ios.svg'; +import darkIosIcon from './icons/ios_dark.svg'; import javaIcon from './icons/java.svg'; import nodeJsIcon from './icons/nodejs.svg'; import ocamlIcon from './icons/ocaml.svg'; @@ -31,6 +33,7 @@ const agentIcons: { [key: string]: string } = { dotnet: dotNetIcon, erlang: erlangIcon, go: goIcon, + ios: iosIcon, java: javaIcon, nodejs: nodeJsIcon, ocaml: ocamlIcon, @@ -44,6 +47,7 @@ const agentIcons: { [key: string]: string } = { const darkAgentIcons: { [key: string]: string } = { ...agentIcons, + ios: darkIosIcon, php: darkPhpIcon, rum: darkRumJsIcon, rust: darkRustIcon, diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ios.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ios.svg new file mode 100644 index 0000000000000..d5b999a7ff042 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ios.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ios_dark.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ios_dark.svg new file mode 100644 index 0000000000000..16ea340ba150f --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ios_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx index 2c086dbb17222..e906707730baa 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx @@ -9,6 +9,13 @@ import { ValuesType } from 'utility-types'; import { RectAnnotation } from '@elastic/charts'; import { EuiTheme } from 'src/plugins/kibana_react/common'; import { rgba } from 'polished'; +import { + ALERT_DURATION, + RULE_ID, + ALERT_START, + ALERT_UUID, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import { parseTechnicalFields } from '../../../../../../rule_registry/common'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; type Alert = ValuesType< @@ -30,10 +37,11 @@ export function getAlertAnnotations({ theme: EuiTheme; }) { return alerts?.flatMap((alert) => { - const uuid = alert['kibana.rac.alert.uuid']!; - const start = new Date(alert['kibana.rac.alert.start']!).getTime(); - const end = start + alert['kibana.rac.alert.duration.us']! / 1000; - const color = getAlertColor({ ruleId: alert['rule.id']!, theme }); + const parsed = parseTechnicalFields(alert); + const uuid = parsed[ALERT_UUID]!; + const start = new Date(parsed[ALERT_START]!).getTime(); + const end = start + parsed[ALERT_DURATION]! / 1000; + const color = getAlertColor({ ruleId: parsed[RULE_ID]!, theme }); return [ - alert['rule.id'] === AlertType.TransactionDuration || - alert['rule.id'] === AlertType.TransactionDurationAnomaly + alert[RULE_ID]?.[0] === AlertType.TransactionDuration || + alert[RULE_ID]?.[0] === AlertType.TransactionDurationAnomaly )} /> diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 9aefa55aaaa36..7eceaf5ca8e5d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -9,6 +9,7 @@ import { EuiPanel, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useParams } from 'react-router-dom'; +import { RULE_ID } from '../../../../../../rule_registry/common/technical_rule_data_field_names'; import { AlertType } from '../../../../../common/alert_types'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asPercent } from '../../../../../common/utils/formatters'; @@ -152,7 +153,7 @@ export function TransactionErrorRateChart({ yDomain={{ min: 0, max: 1 }} customTheme={comparisonChartThem} alerts={alerts.filter( - (alert) => alert['rule.id'] === AlertType.TransactionErrorRate + (alert) => alert[RULE_ID]?.[0] === AlertType.TransactionErrorRate )} /> diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.ts b/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.ts index 025e8c2a9935d..67c27308e6658 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.ts +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.ts @@ -19,9 +19,9 @@ export function getComparisonChartTheme(theme: EuiTheme) { return { areaSeriesStyle: { area: { - fill: theme.eui.euiColorLightestShade, + fill: theme.eui.euiColorLightShade, visible: true, - opacity: 1, + opacity: 0.5, }, line: { stroke: theme.eui.euiColorMediumShade, diff --git a/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx index 175471e7ae817..ec42a11783273 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx @@ -7,8 +7,9 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import { createContext } from 'react'; +import type { ObservabilityRuleTypeRegistry } from '../../../../observability/public'; import { ConfigSchema } from '../..'; -import { ApmPluginSetupDeps, ApmRuleRegistry } from '../../plugin'; +import { ApmPluginSetupDeps } from '../../plugin'; import { MapsStartApi } from '../../../../maps/public'; export interface ApmPluginContextValue { @@ -16,7 +17,7 @@ export interface ApmPluginContextValue { config: ConfigSchema; core: CoreStart; plugins: ApmPluginSetupDeps & { maps?: MapsStartApi }; - apmRuleRegistry: ApmRuleRegistry; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; } export const ApmPluginContext = createContext({} as ApmPluginContextValue); diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index 07da5ea7f6c1f..a16f81826636b 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -7,12 +7,12 @@ import React, { ReactNode } from 'react'; import { Observable, of } from 'rxjs'; +import { createObservabilityRuleTypeRegistryMock } from '../../../../observability/public'; import { ApmPluginContext, ApmPluginContextValue } from './apm_plugin_context'; import { ConfigSchema } from '../..'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; import { createCallApmApi } from '../../services/rest/createCallApmApi'; import { MlUrlGenerator } from '../../../../ml/public'; -import { ApmRuleRegistry } from '../../plugin'; const uiSettings: Record = { [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ @@ -77,11 +77,6 @@ const mockCore = { }, }; -const mockApmRuleRegistry = ({ - getTypeByRuleId: () => undefined, - registerType: () => undefined, -} as unknown) as ApmRuleRegistry; - const mockConfig: ConfigSchema = { serviceMapEnabled: true, ui: { @@ -116,7 +111,7 @@ export const mockApmPluginContextValue = { config: mockConfig, core: mockCore, plugins: mockPlugin, - apmRuleRegistry: mockApmRuleRegistry, + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), }; export function MockApmPluginContextWrapper({ diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index f7bbe647d8e37..b493363d98f7a 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -34,19 +34,15 @@ import type { HasDataParams, ObservabilityPublicSetup, } from '../../observability/public'; -import { FormatterRuleRegistry } from '../../observability/public'; import type { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '../../triggers_actions_ui/public'; -import { apmRuleRegistrySettings } from '../common/rules/apm_rule_registry_settings'; -import type { APMRuleFieldMap } from '../common/rules/apm_rule_field_map'; import { registerApmAlerts } from './components/alerting/register_apm_alerts'; import { featureCatalogueEntry } from './featureCatalogueEntry'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; export type ApmPluginSetup = ReturnType; -export type ApmRuleRegistry = ApmPluginSetup['ruleRegistry']; export type ApmPluginStart = void; @@ -87,11 +83,6 @@ export class ApmPlugin implements Plugin { pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry); } - const apmRuleRegistry = plugins.observability.ruleRegistry.create({ - ...apmRuleRegistrySettings, - fieldMap: {} as APMRuleFieldMap, - ctor: FormatterRuleRegistry, - }); const getApmDataHelper = async () => { const { fetchObservabilityOverviewPageData, @@ -127,6 +118,8 @@ export class ApmPlugin implements Plugin { return { fetchUxOverviewDate, hasRumData }; }; + const { observabilityRuleTypeRegistry } = plugins.observability; + plugins.observability.dashboard.register({ appName: 'ux', hasData: async (params?: HasDataParams) => { @@ -187,12 +180,12 @@ export class ApmPlugin implements Plugin { appMountParameters, config, pluginsStart: pluginsStart as ApmPluginStartDeps, - apmRuleRegistry, + observabilityRuleTypeRegistry, }); }, }); - registerApmAlerts(apmRuleRegistry); + registerApmAlerts(observabilityRuleTypeRegistry); core.application.register({ id: 'ux', @@ -231,14 +224,12 @@ export class ApmPlugin implements Plugin { appMountParameters, config, corePlugins: corePlugins as ApmPluginStartDeps, - apmRuleRegistry, + observabilityRuleTypeRegistry, }); }, }); - return { - ruleRegistry: apmRuleRegistry, - }; + return {}; } public start(core: CoreStart, plugins: ApmPluginStartDeps) { toggleAppLinkInNav(core, this.initializerContext.config.get()); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts index 9a362efa90ac0..022fad6fa7840 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts @@ -7,17 +7,19 @@ import { Observable } from 'rxjs'; import { Logger } from 'kibana/server'; +import { PluginSetupContract as AlertingPluginSetupContract } from '../../../../alerting/server'; +import { RuleDataClient } from '../../../../rule_registry/server'; import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type'; import { registerTransactionDurationAnomalyAlertType } from './register_transaction_duration_anomaly_alert_type'; import { registerErrorCountAlertType } from './register_error_count_alert_type'; import { APMConfig } from '../..'; import { MlPluginSetup } from '../../../../ml/server'; import { registerTransactionErrorRateAlertType } from './register_transaction_error_rate_alert_type'; -import { APMRuleRegistry } from '../../plugin'; export interface RegisterRuleDependencies { - registry: APMRuleRegistry; + ruleDataClient: RuleDataClient; ml?: MlPluginSetup; + alerting: AlertingPluginSetupContract; config$: Observable; logger: Logger; } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 15ec5d0ef0bd0..885b22ae343d8 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -7,6 +7,11 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; @@ -21,7 +26,6 @@ import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; -import { createAPMLifecycleRuleType } from './create_apm_lifecycle_rule_type'; const paramsSchema = schema.object({ windowSize: schema.number(), @@ -34,11 +38,18 @@ const paramsSchema = schema.object({ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorCount]; export function registerErrorCountAlertType({ - registry, + alerting, + logger, + ruleDataClient, config$, }: RegisterRuleDependencies) { - registry.registerType( - createAPMLifecycleRuleType({ + const createLifecycleRuleType = createLifecycleRuleTypeFactory({ + ruleDataClient, + logger, + }); + + alerting.registerType( + createLifecycleRuleType({ id: AlertType.ErrorCount, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, @@ -146,9 +157,8 @@ export function registerErrorCountAlertType({ ? { [SERVICE_ENVIRONMENT]: environment } : {}), [PROCESSOR_EVENT]: ProcessorEvent.error, - 'kibana.observability.evaluation.value': errorCount, - 'kibana.observability.evaluation.threshold': - alertParams.threshold, + [ALERT_EVALUATION_VALUE]: errorCount, + [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold, }, }) .scheduleActions(alertTypeConfig.defaultActionGroupId, { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index 4918a6cc892b7..f77cc3ee930b1 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -8,6 +8,11 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; import { QueryContainer } from '@elastic/elasticsearch/api/types'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { parseEnvironmentUrlParam } from '../../../common/environment_filter_values'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { @@ -24,7 +29,6 @@ import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; -import { createAPMLifecycleRuleType } from './create_apm_lifecycle_rule_type'; const paramsSchema = schema.object({ serviceName: schema.string(), @@ -43,130 +47,142 @@ const paramsSchema = schema.object({ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionDuration]; export function registerTransactionDurationAlertType({ - registry, + alerting, + ruleDataClient, config$, + logger, }: RegisterRuleDependencies) { - registry.registerType( - createAPMLifecycleRuleType({ - id: AlertType.TransactionDuration, - name: alertTypeConfig.name, - actionGroups: alertTypeConfig.actionGroups, - defaultActionGroupId: alertTypeConfig.defaultActionGroupId, - validate: { - params: paramsSchema, - }, - actionVariables: { - context: [ - apmActionVariables.serviceName, - apmActionVariables.transactionType, - apmActionVariables.environment, - apmActionVariables.threshold, - apmActionVariables.triggerValue, - apmActionVariables.interval, - ], - }, - producer: 'apm', - minimumLicenseRequired: 'basic', - executor: async ({ services, params }) => { - const config = await config$.pipe(take(1)).toPromise(); - const alertParams = params; - const indices = await getApmIndices({ - config, - savedObjectsClient: services.savedObjectsClient, - }); - - const searchParams = { - index: indices['apm_oss.transactionIndices'], - body: { - size: 0, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: `now-${alertParams.windowSize}${alertParams.windowUnit}`, - }, + const createLifecycleRuleType = createLifecycleRuleTypeFactory({ + ruleDataClient, + logger, + }); + + const type = createLifecycleRuleType({ + id: AlertType.TransactionDuration, + name: alertTypeConfig.name, + actionGroups: alertTypeConfig.actionGroups, + defaultActionGroupId: alertTypeConfig.defaultActionGroupId, + validate: { + params: paramsSchema, + }, + actionVariables: { + context: [ + apmActionVariables.serviceName, + apmActionVariables.transactionType, + apmActionVariables.environment, + apmActionVariables.threshold, + apmActionVariables.triggerValue, + apmActionVariables.interval, + ], + }, + producer: 'apm', + minimumLicenseRequired: 'basic', + executor: async ({ services, params }) => { + const config = await config$.pipe(take(1)).toPromise(); + const alertParams = params; + const indices = await getApmIndices({ + config, + savedObjectsClient: services.savedObjectsClient, + }); + + const searchParams = { + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: `now-${alertParams.windowSize}${alertParams.windowUnit}`, }, }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - { term: { [SERVICE_NAME]: alertParams.serviceName } }, - { term: { [TRANSACTION_TYPE]: alertParams.transactionType } }, - ...environmentQuery(alertParams.environment), - ] as QueryContainer[], - }, + }, + { + term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction }, + }, + { term: { [SERVICE_NAME]: alertParams.serviceName } }, + { + term: { + [TRANSACTION_TYPE]: alertParams.transactionType, + }, + }, + ...environmentQuery(alertParams.environment), + ] as QueryContainer[], }, - aggs: { - latency: - alertParams.aggregationType === 'avg' - ? { avg: { field: TRANSACTION_DURATION } } - : { - percentiles: { - field: TRANSACTION_DURATION, - percents: [ - alertParams.aggregationType === '95th' ? 95 : 99, - ], - }, + }, + aggs: { + latency: + alertParams.aggregationType === 'avg' + ? { avg: { field: TRANSACTION_DURATION } } + : { + percentiles: { + field: TRANSACTION_DURATION, + percents: [ + alertParams.aggregationType === '95th' ? 95 : 99, + ], }, - }, + }, }, - }; - - const response = await alertingEsClient({ - scopedClusterClient: services.scopedClusterClient, - params: searchParams, - }); - - if (!response.aggregations) { - return {}; - } - - const { latency } = response.aggregations; - - const transactionDuration = - 'values' in latency - ? Object.values(latency.values)[0] - : latency?.value; - - const threshold = alertParams.threshold * 1000; - - if (transactionDuration && transactionDuration > threshold) { - const durationFormatter = getDurationFormatter(transactionDuration); - const transactionDurationFormatted = durationFormatter( - transactionDuration - ).formatted; - - const environmentParsed = parseEnvironmentUrlParam( - alertParams.environment - ); - - services - .alertWithLifecycle({ - id: `${AlertType.TransactionDuration}_${environmentParsed.text}`, - fields: { - [SERVICE_NAME]: alertParams.serviceName, - ...(environmentParsed.esFieldValue - ? { [SERVICE_ENVIRONMENT]: environmentParsed.esFieldValue } - : {}), - [TRANSACTION_TYPE]: alertParams.transactionType, - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - 'kibana.observability.evaluation.value': transactionDuration, - 'kibana.observability.evaluation.threshold': - alertParams.threshold * 1000, - }, - }) - .scheduleActions(alertTypeConfig.defaultActionGroupId, { - transactionType: alertParams.transactionType, - serviceName: alertParams.serviceName, - environment: environmentParsed.text, - threshold, - triggerValue: transactionDurationFormatted, - interval: `${alertParams.windowSize}${alertParams.windowUnit}`, - }); - } + }, + }; + const response = await alertingEsClient({ + scopedClusterClient: services.scopedClusterClient, + params: searchParams, + }); + + if (!response.aggregations) { return {}; - }, - }) - ); + } + + const { latency } = response.aggregations; + + const transactionDuration = + 'values' in latency ? Object.values(latency.values)[0] : latency?.value; + + const threshold = alertParams.threshold * 1000; + + if (transactionDuration && transactionDuration > threshold) { + const durationFormatter = getDurationFormatter(transactionDuration); + const transactionDurationFormatted = durationFormatter( + transactionDuration + ).formatted; + + const environmentParsed = parseEnvironmentUrlParam( + alertParams.environment + ); + + services + .alertWithLifecycle({ + id: `${AlertType.TransactionDuration}_${environmentParsed.text}`, + fields: { + [SERVICE_NAME]: alertParams.serviceName, + ...(environmentParsed.esFieldValue + ? { + [SERVICE_ENVIRONMENT]: environmentParsed.esFieldValue, + } + : {}), + [TRANSACTION_TYPE]: alertParams.transactionType, + [PROCESSOR_EVENT]: ProcessorEvent.transaction, + [ALERT_EVALUATION_VALUE]: transactionDuration, + [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold * 1000, + }, + }) + .scheduleActions(alertTypeConfig.defaultActionGroupId, { + transactionType: alertParams.transactionType, + serviceName: alertParams.serviceName, + environment: environmentParsed.text, + threshold, + triggerValue: transactionDurationFormatted, + interval: `${alertParams.windowSize}${alertParams.windowUnit}`, + }); + } + + return {}; + }, + }); + + alerting.registerType(type); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 67ff7cdb8e4e0..399fb9a216ef5 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -9,6 +9,13 @@ import { schema } from '@kbn/config-schema'; import { compact } from 'lodash'; import { ESSearchResponse } from 'typings/elasticsearch'; import { QueryContainer } from '@elastic/elasticsearch/api/types'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY_VALUE, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { ProcessorEvent } from '../../../common/processor_event'; import { getSeverity } from '../../../common/anomaly_detection'; import { @@ -29,7 +36,6 @@ import { getMLJobs } from '../service_map/get_service_anomalies'; import { apmActionVariables } from './action_variables'; import { RegisterRuleDependencies } from './register_apm_alerts'; import { parseEnvironmentUrlParam } from '../../../common/environment_filter_values'; -import { createAPMLifecycleRuleType } from './create_apm_lifecycle_rule_type'; const paramsSchema = schema.object({ serviceName: schema.maybe(schema.string()), @@ -49,11 +55,18 @@ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionDurationAnomaly]; export function registerTransactionDurationAnomalyAlertType({ - registry, + logger, + ruleDataClient, + alerting, ml, }: RegisterRuleDependencies) { - registry.registerType( - createAPMLifecycleRuleType({ + const createLifecycleRuleType = createLifecycleRuleTypeFactory({ + logger, + ruleDataClient, + }); + + alerting.registerType( + createLifecycleRuleType({ id: AlertType.TransactionDurationAnomaly, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, @@ -190,7 +203,7 @@ export function registerTransactionDurationAnomalyAlertType({ const job = mlJobs.find((j) => j.job_id === latest.job_id); if (!job) { - services.logger.warn( + logger.warn( `Could not find matching job for job id ${latest.job_id}` ); return undefined; @@ -231,10 +244,10 @@ export function registerTransactionDurationAnomalyAlertType({ : {}), [TRANSACTION_TYPE]: transactionType, [PROCESSOR_EVENT]: ProcessorEvent.transaction, - 'kibana.rac.alert.severity.level': severityLevel, - 'kibana.rac.alert.severity.value': score, - 'kibana.observability.evaluation.value': score, - 'kibana.observability.evaluation.threshold': threshold, + [ALERT_SEVERITY_LEVEL]: severityLevel, + [ALERT_SEVERITY_VALUE]: score, + [ALERT_EVALUATION_VALUE]: score, + [ALERT_EVALUATION_THRESHOLD]: threshold, }, }) .scheduleActions(alertTypeConfig.defaultActionGroupId, { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index bead17e308f06..4d6a0685fd379 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -7,6 +7,11 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { EVENT_OUTCOME, @@ -22,7 +27,6 @@ import { environmentQuery } from '../../../server/utils/queries'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; -import { createAPMLifecycleRuleType } from './create_apm_lifecycle_rule_type'; import { RegisterRuleDependencies } from './register_apm_alerts'; const paramsSchema = schema.object({ @@ -37,11 +41,18 @@ const paramsSchema = schema.object({ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionErrorRate]; export function registerTransactionErrorRateAlertType({ - registry, + alerting, + ruleDataClient, + logger, config$, }: RegisterRuleDependencies) { - registry.registerType( - createAPMLifecycleRuleType({ + const createLifecycleRuleType = createLifecycleRuleTypeFactory({ + ruleDataClient, + logger, + }); + + alerting.registerType( + createLifecycleRuleType({ id: AlertType.TransactionErrorRate, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, @@ -183,9 +194,8 @@ export function registerTransactionErrorRateAlertType({ ...(environment ? { [SERVICE_ENVIRONMENT]: environment } : {}), [TRANSACTION_TYPE]: transactionType, [PROCESSOR_EVENT]: ProcessorEvent.transaction, - 'kibana.observability.evaluation.value': errorRate, - 'kibana.observability.evaluation.threshold': - alertParams.threshold, + [ALERT_EVALUATION_VALUE]: errorRate, + [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold, }, }) .scheduleActions(alertTypeConfig.defaultActionGroupId, { diff --git a/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts index 37b3e282d0a59..ce1466bff01a9 100644 --- a/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts @@ -8,8 +8,9 @@ import { Logger } from 'kibana/server'; import { of } from 'rxjs'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import type { RuleDataClient } from '../../../../../rule_registry/server'; +import { PluginSetupContract as AlertingPluginSetupContract } from '../../../../../alerting/server'; import { APMConfig } from '../../..'; -import { APMRuleRegistry } from '../../../plugin'; export const createRuleTypeMocks = () => { let alertExecutor: (...args: any[]) => Promise; @@ -27,19 +28,16 @@ export const createRuleTypeMocks = () => { error: jest.fn(), } as unknown) as Logger; - const registry = { + const alerting = { registerType: ({ executor }) => { alertExecutor = executor; }, - } as APMRuleRegistry; + } as AlertingPluginSetupContract; const scheduleActions = jest.fn(); const services = { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - scopedRuleRegistryClient: { - bulkIndex: jest.fn(), - }, alertInstanceFactory: jest.fn(() => ({ scheduleActions })), alertWithLifecycle: jest.fn(), logger: loggerMock, @@ -47,9 +45,21 @@ export const createRuleTypeMocks = () => { return { dependencies: { - registry, + alerting, config$: mockedConfig$, logger: loggerMock, + ruleDataClient: ({ + getReader: () => { + return { + search: jest.fn(), + }; + }, + getWriter: () => { + return { + bulk: jest.fn(), + }; + }, + } as unknown) as RuleDataClient, }, services, scheduleActions, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts b/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts index 6356731cc48d1..f58452ce4d916 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts @@ -5,33 +5,30 @@ * 2.0. */ +import { ALERT_UUID } from '@kbn/rule-data-utils/target/technical_field_names'; +import { RuleDataClient } from '../../../../rule_registry/server'; import { SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; -import type { PromiseReturnType } from '../../../../observability/typings/common'; -import type { APMRuleRegistry } from '../../plugin'; import { environmentQuery, rangeQuery } from '../../utils/queries'; export async function getServiceAlerts({ - apmRuleRegistryClient, + ruleDataClient, start, end, serviceName, environment, transactionType, }: { - apmRuleRegistryClient: Exclude< - PromiseReturnType, - undefined - >; + ruleDataClient: RuleDataClient; start: number; end: number; serviceName: string; environment?: string; transactionType: string; }) { - const response = await apmRuleRegistryClient.search({ + const response = await ruleDataClient.getReader().search({ body: { query: { bool: { @@ -68,13 +65,14 @@ export async function getServiceAlerts({ size: 100, fields: ['*'], collapse: { - field: 'kibana.rac.alert.uuid', + field: ALERT_UUID, }, sort: { '@timestamp': 'desc', }, }, + allow_no_indices: true, }); - return response.events; + return response.hits.hits.map((hit) => hit.fields); } diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index e12d089855834..44334889128c4 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -16,7 +16,10 @@ import { Plugin, PluginInitializerContext, } from 'src/core/server'; -import { mapValues } from 'lodash'; +import { mapValues, once } from 'lodash'; +import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets'; +import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; +import { RuleDataClient } from '../../rule_registry/server'; import { APMConfig, APMXPackConfig } from '.'; import { mergeConfigs } from './index'; import { UI_SETTINGS } from '../../../../src/plugins/data/common'; @@ -42,10 +45,12 @@ import { } from './types'; import { registerRoutes } from './routes/register_routes'; import { getGlobalApmServerRouteRepository } from './routes/get_global_apm_server_route_repository'; -import { apmRuleRegistrySettings } from '../common/rules/apm_rule_registry_settings'; -import { apmRuleFieldMap } from '../common/rules/apm_rule_field_map'; - -export type APMRuleRegistry = ReturnType['ruleRegistry']; +import { + PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../common/elasticsearch_fieldnames'; export class APMPlugin implements @@ -124,20 +129,81 @@ export class APMPlugin registerFeaturesUsage({ licensingPlugin: plugins.licensing }); - const apmRuleRegistry = plugins.observability.ruleRegistry.create({ - ...apmRuleRegistrySettings, - fieldMap: apmRuleFieldMap, + const getCoreStart = () => + core.getStartServices().then(([coreStart]) => coreStart); + + const ready = once(async () => { + const componentTemplateName = plugins.ruleRegistry.getFullAssetName( + 'apm-mappings' + ); + + if (!plugins.ruleRegistry.isWriteEnabled()) { + return; + } + + await plugins.ruleRegistry.createOrUpdateComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + mappings: mappingFromFieldMap({ + [SERVICE_NAME]: { + type: 'keyword', + }, + [SERVICE_ENVIRONMENT]: { + type: 'keyword', + }, + [TRANSACTION_TYPE]: { + type: 'keyword', + }, + [PROCESSOR_EVENT]: { + type: 'keyword', + }, + }), + }, + }, + }); + + await plugins.ruleRegistry.createOrUpdateIndexTemplate({ + name: plugins.ruleRegistry.getFullAssetName('apm-index-template'), + body: { + index_patterns: [ + plugins.ruleRegistry.getFullAssetName('observability-apm*'), + ], + composed_of: [ + plugins.ruleRegistry.getFullAssetName( + TECHNICAL_COMPONENT_TEMPLATE_NAME + ), + componentTemplateName, + ], + }, + }); + }); + + ready().catch((err) => { + this.logger!.error(err); + }); + + const ruleDataClient = new RuleDataClient({ + alias: plugins.ruleRegistry.getFullAssetName('observability-apm'), + getClusterClient: async () => { + const coreStart = await getCoreStart(); + return coreStart.elasticsearch.client.asInternalUser; + }, + ready, }); registerRoutes({ core: { setup: core, - start: () => core.getStartServices().then(([coreStart]) => coreStart), + start: getCoreStart, }, logger: this.logger, config: currentConfig, repository: getGlobalApmServerRouteRepository(), - apmRuleRegistry, + ruleDataClient, plugins: mapValues(plugins, (value, key) => { return { setup: value, @@ -157,12 +223,16 @@ export class APMPlugin savedObjectsClient: await getInternalSavedObjectsClient(core), config: await mergedConfig$.pipe(take(1)).toPromise(), }); - registerApmAlerts({ - registry: apmRuleRegistry, - ml: plugins.ml, - config$: mergedConfig$, - logger: this.logger!.get('rule'), - }); + + if (plugins.alerting) { + registerApmAlerts({ + ruleDataClient, + alerting: plugins.alerting, + ml: plugins.ml, + config$: mergedConfig$, + logger: this.logger!.get('rule'), + }); + } return { config$: mergedConfig$, @@ -193,7 +263,6 @@ export class APMPlugin }, }); }, - ruleRegistry: apmRuleRegistry, }; } diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.ts b/x-pack/plugins/apm/server/routes/register_routes/index.ts index f792e078c528a..c9df12fd58208 100644 --- a/x-pack/plugins/apm/server/routes/register_routes/index.ts +++ b/x-pack/plugins/apm/server/routes/register_routes/index.ts @@ -39,14 +39,14 @@ export function registerRoutes({ plugins, logger, config, - apmRuleRegistry, + ruleDataClient, }: { core: APMRouteHandlerResources['core']; plugins: APMRouteHandlerResources['plugins']; logger: APMRouteHandlerResources['logger']; repository: ServerRouteRepository; config: APMRouteHandlerResources['config']; - apmRuleRegistry: APMRouteHandlerResources['apmRuleRegistry']; + ruleDataClient: APMRouteHandlerResources['ruleDataClient']; }) { const routes = repository.getRoutes(); @@ -99,7 +99,7 @@ export function registerRoutes({ }, validatedParams ), - apmRuleRegistry, + ruleDataClient, })) as any; if (Array.isArray(data)) { diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 54e59f2be7ae3..4384d2be78ca0 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -737,30 +737,15 @@ const serviceAlertsRoute = createApmServerRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, params, apmRuleRegistry }) => { - const alertsClient = context.alerting.getAlertsClient(); - + handler: async ({ context, params, ruleDataClient }) => { const { query: { start, end, environment, transactionType }, path: { serviceName }, } = params; - const apmRuleRegistryClient = await apmRuleRegistry.createScopedRuleRegistryClient( - { - alertsClient, - context, - } - ); - - if (!apmRuleRegistryClient) { - throw Boom.failedDependency( - 'xpack.ruleRegistry.unsafe.write.enabled is set to false' - ); - } - return { alerts: await getServiceAlerts({ - apmRuleRegistryClient, + ruleDataClient, start, end, serviceName, diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 602e1f3e0edb9..13bd631085aac 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -12,11 +12,11 @@ import { KibanaRequest, CoreStart, } from 'src/core/server'; +import { RuleDataClient } from '../../../rule_registry/server'; import { AlertingApiRequestHandlerContext } from '../../../alerting/server'; import { LicensingApiRequestHandlerContext } from '../../../licensing/server'; import { APMConfig } from '..'; import { APMPluginDependencies } from '../types'; -import { APMRuleRegistry } from '../plugin'; export interface ApmPluginRequestHandlerContext extends RequestHandlerContext { licensing: LicensingApiRequestHandlerContext; @@ -62,5 +62,5 @@ export interface APMRouteHandlerResources { start: () => Promise[key]['start']>; }; }; - apmRuleRegistry: APMRuleRegistry; + ruleDataClient: RuleDataClient; } diff --git a/x-pack/plugins/apm/server/types.ts b/x-pack/plugins/apm/server/types.ts index dbc220f9f6b15..a5ba4f39b32b3 100644 --- a/x-pack/plugins/apm/server/types.ts +++ b/x-pack/plugins/apm/server/types.ts @@ -7,6 +7,10 @@ import { ValuesType } from 'utility-types'; import { Observable } from 'rxjs'; import { CoreSetup, CoreStart, KibanaRequest } from 'kibana/server'; +import { + RuleRegistryPluginSetupContract, + RuleRegistryPluginStartContract, +} from '../../rule_registry/server'; import { PluginSetup as DataPluginSetup, PluginStart as DataPluginStart, @@ -115,6 +119,10 @@ interface DependencyMap { setup: DataPluginSetup; start: DataPluginStart; }; + ruleRegistry: { + setup: RuleRegistryPluginSetupContract; + start: RuleRegistryPluginStartContract; + }; } const requiredDependencies = [ @@ -126,6 +134,7 @@ const requiredDependencies = [ 'embeddable', 'infra', 'observability', + 'ruleRegistry', ] as const; const optionalDependencies = [ diff --git a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js b/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js deleted file mode 100644 index 302e45ade8d43..0000000000000 --- a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js +++ /dev/null @@ -1,264 +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. - */ - -/*disabling eslint because of these jsx-a11y errors(https://www.npmjs.com/package/eslint-plugin-jsx-a11y): -181:7 error Elements with the 'combobox' interactive role must be focusable jsx-a11y/interactive-supports-focus - 187:9 error Elements with the ARIA role "combobox" must have the following attributes defined: aria-controls,aria-expanded jsx-a11y/role-has-required-aria-props - 209:23 error Elements with the 'option' interactive role must be focusable jsx-a11y/interactive-supports-focus - 218:25 error Elements with the ARIA role "option" must have the following attributes defined: aria-selected jsx-a11y/role-has-required-aria-props -*/ -/* eslint-disable jsx-a11y/interactive-supports-focus */ -/* eslint-disable jsx-a11y/role-has-required-aria-props */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, keys } from '@elastic/eui'; - -/** - * An autocomplete component. Currently this is only used for the expression editor but in theory - * it could be extended to any autocomplete-related component. It expects these props: - * - * header: The header node - * items: The list of items for autocompletion - * onSelect: The function to invoke when an item is selected (passing in the item) - * children: Any child nodes, which should include the text input itself - * reference: A function that is passed the selected item which generates a reference node - */ -export class Autocomplete extends React.Component { - static propTypes = { - header: PropTypes.node, - items: PropTypes.array, - onSelect: PropTypes.func, - children: PropTypes.node, - reference: PropTypes.func, - }; - - constructor() { - super(); - this.state = { - isOpen: false, - isFocused: false, - isMousedOver: false, - selectedIndex: -1, - }; - - // These are used for automatically scrolling items into view when selected - this.containerRef = null; - this.itemRefs = []; - } - - componentDidUpdate(prevProps, prevState) { - if ( - this.props.items && - prevProps.items !== this.props.items && - this.props.items.length === 1 && - this.state.selectedIndex !== 0 - ) { - this.selectFirst(); - } - - if (prevState.selectedIndex !== this.state.selectedIndex) { - this.scrollIntoView(); - } - } - - selectFirst() { - this.setState({ selectedIndex: 0 }); - } - - isVisible() { - const { isOpen, isFocused, isMousedOver } = this.state; - const { items, reference } = this.props; - - // We have to check isMousedOver because the blur event fires before the click event, which - // means if we didn't keep track of isMousedOver, we wouldn't even get the click event - const visible = isOpen && (isFocused || isMousedOver); - const hasItems = items && items.length; - const hasReference = reference(this.getSelectedItem()); - - return visible && (hasItems || hasReference); - } - - getSelectedItem() { - return this.props.items && this.props.items[this.state.selectedIndex]; - } - - selectPrevious() { - const { items } = this.props; - const { selectedIndex } = this.state; - if (selectedIndex > 0) { - this.setState({ selectedIndex: selectedIndex - 1 }); - } else { - this.setState({ selectedIndex: items.length - 1 }); - } - } - - selectNext() { - const { items } = this.props; - const { selectedIndex } = this.state; - if (selectedIndex >= 0 && selectedIndex < items.length - 1) { - this.setState({ selectedIndex: selectedIndex + 1 }); - } else { - this.setState({ selectedIndex: 0 }); - } - } - - scrollIntoView() { - const { - containerRef, - itemRefs, - state: { selectedIndex }, - } = this; - const itemRef = itemRefs[selectedIndex]; - if (!containerRef || !itemRef) { - return; - } - containerRef.scrollTop = Math.max( - Math.min(containerRef.scrollTop, itemRef.offsetTop), - itemRef.offsetTop + itemRef.offsetHeight - containerRef.offsetHeight - ); - } - - onSubmit = () => { - const { selectedIndex } = this.state; - const { items, onSelect } = this.props; - onSelect(items[selectedIndex]); - this.setState({ selectedIndex: -1 }); - }; - - /** - * Handle key down events for the menu, including selecting the previous and next items, making - * the item selection, closing the menu, etc. - */ - onKeyDown = (e) => { - const { BACKSPACE, ESCAPE, TAB, ENTER, ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT } = keys; - const { key } = e; - const { items } = this.props; - const { isOpen, selectedIndex } = this.state; - - if ([ESCAPE, ARROW_LEFT, ARROW_RIGHT].includes(key)) { - this.setState({ isOpen: false }); - } - - if ([TAB, ENTER].includes(key) && isOpen && selectedIndex >= 0) { - e.preventDefault(); - this.onSubmit(); - } else if (key === ARROW_UP && items.length > 0 && isOpen) { - e.preventDefault(); - this.selectPrevious(); - } else if (key === ARROW_DOWN && items.length > 0 && isOpen) { - e.preventDefault(); - this.selectNext(); - } else if (key === BACKSPACE) { - this.setState({ isOpen: true }); - } else if (!['Shift', 'Control', 'Alt', 'Meta'].includes(key)) { - this.setState({ selectedIndex: -1 }); - } - }; - - /** - * On key press (character keys), show the menu. We don't want to willy nilly show the menu - * whenever ANY key down event happens (like arrow keys) cuz that would be just downright - * annoying. - */ - onKeyPress = () => { - this.setState({ isOpen: true }); - }; - - onFocus = () => { - this.setState({ - isFocused: true, - }); - }; - - onBlur = () => { - this.setState({ - isFocused: false, - }); - }; - - onMouseDown = () => { - this.setState({ - isOpen: false, - }); - }; - - onMouseEnter = () => { - this.setState({ - isMousedOver: true, - }); - }; - - onMouseLeave = () => { - this.setState({ isMousedOver: false }); - }; - - render() { - const { header, items, reference } = this.props; - return ( -
- {this.isVisible() ? ( - - - {items && items.length ? ( - -
(this.containerRef = ref)} - role="listbox" - > - {header} - {items.map((item, i) => ( -
(this.itemRefs[i] = ref)} - className={ - 'autocompleteItem' + - (this.state.selectedIndex === i ? ' autocompleteItem--isActive' : '') - } - onMouseEnter={() => this.setState({ selectedIndex: i })} - onClick={this.onSubmit} - role="option" - > - {item.text} -
- ))} -
-
- ) : ( - '' - )} - -
{reference(this.getSelectedItem())}
-
-
-
- ) : ( - '' - )} -
- {this.props.children} -
-
- ); - } -} diff --git a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.scss b/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.scss deleted file mode 100644 index 7f723b5549acf..0000000000000 --- a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.scss +++ /dev/null @@ -1,52 +0,0 @@ -.autocomplete { - position: relative; -} - -.autocompletePopup { - position: absolute; - top: -262px; - height: 260px; - width: 100%; -} - -.autocompleteItems { - border-right: $euiBorderThin; -} - -.autocompleteItems, -.autocompleteReference { - @include euiScrollBar; - height: 258px; - overflow: auto; -} - -.autocompleteReference { - padding: $euiSizeS $euiSizeM; - background-color: tintOrShade($euiColorLightestShade, 65%, 20%); -} - -.autocompleteItem { - padding: $euiSizeS $euiSizeM; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - font-family: $euiCodeFontFamily; - font-weight: $euiFontWeightRegular; -} - -.autocompleteItem--isActive { - color: $euiColorPrimary; - background-color: $euiFocusBackgroundColor; -} - -.autocompleteType { - padding: $euiSizeS; -} - -.autocompleteTable .euiTable { - background-color: transparent; -} - -.autocompleteDescList .euiDescriptionList__description { - margin-right: $euiSizeS; -} \ No newline at end of file diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index 41d12db3a1853..6c883b832737f 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -13,7 +13,6 @@ @import '../components/arg_form/arg_form'; @import '../components/asset_manager/asset_manager'; @import '../components/asset_picker/asset_picker'; -@import '../components/autocomplete/autocomplete'; @import '../components/clipboard/clipboard'; @import '../components/color_dot/color_dot'; @import '../components/color_palette/color_palette'; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx index b68642d184542..acf71cad3f3ba 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx @@ -59,8 +59,7 @@ const getWrapper: (name?: WorkpadNames) => ReactWrapper = (name = 'hello') => { return mount(); }; -// FLAKY: https://github.com/elastic/kibana/issues/95899 -describe.skip('', () => { +describe('', () => { test('App renders properly', () => { expect(getWrapper().html()).toMatchSnapshot(); }); diff --git a/x-pack/plugins/cases/common/api/helpers.ts b/x-pack/plugins/cases/common/api/helpers.ts index 43e292b91db4b..7ac686ce5c8dd 100644 --- a/x-pack/plugins/cases/common/api/helpers.ts +++ b/x-pack/plugins/cases/common/api/helpers.ts @@ -14,6 +14,7 @@ import { SUB_CASES_URL, CASE_PUSH_URL, SUB_CASE_USER_ACTIONS_URL, + CASE_ALERTS_URL, } from '../constants'; export const getCaseDetailsUrl = (id: string): string => { @@ -47,3 +48,7 @@ export const getSubCaseUserActionUrl = (caseID: string, subCaseId: string): stri export const getCasePushUrl = (caseId: string, connectorId: string): string => { return CASE_PUSH_URL.replace('{case_id}', caseId).replace('{connector_id}', connectorId); }; + +export const getCasesFromAlertsUrl = (alertId: string): string => { + return CASE_ALERTS_URL.replace('{alert_id}', alertId); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts index b9ec83db99f70..5435450f1bfdb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts @@ -43,6 +43,7 @@ describe('EngineLogic', () => { engineName: '', isMetaEngine: false, isSampleEngine: false, + hasSchemaErrors: false, hasSchemaConflicts: false, hasUnconfirmedSchemaFields: false, engineNotFound: false, @@ -87,28 +88,6 @@ describe('EngineLogic', () => { }); }); - describe('setIndexingStatus', () => { - describe('engine', () => { - it('should set the nested obj property to the provided value', () => { - mount({ engine: mockEngineData }); - const mockReindexJob = { - percentageComplete: 50, - numDocumentsWithErrors: 2, - activeReindexJobId: '123', - }; - EngineLogic.actions.setIndexingStatus(mockReindexJob); - - expect(EngineLogic.values).toEqual({ - ...DEFAULT_VALUES, - engine: { - ...mockEngineData, - activeReindexJob: mockReindexJob, - }, - }); - }); - }); - }); - describe('setEngineNotFound', () => { describe('engineNotFound', () => { it('should be set to the provided value', () => { @@ -228,6 +207,24 @@ describe('EngineLogic', () => { }); }); + describe('hasSchemaErrors', () => { + it('should be set based on engine.activeReindexJob.numDocumentsWithErrors', () => { + const mockSchemaEngine = { + ...mockEngineData, + activeReindexJob: { + numDocumentsWithErrors: 10, + }, + }; + mount({ engine: mockSchemaEngine }); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + engine: mockSchemaEngine, + hasSchemaErrors: true, + }); + }); + }); + describe('hasSchemaConflicts', () => { it('should be set based on engine.schemaConflicts', () => { const mockSchemaEngine = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts index 768a9e545b878..8fbf45bc85e52 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts @@ -8,7 +8,6 @@ import { kea, MakeLogicType } from 'kea'; import { HttpLogic } from '../../../shared/http'; -import { IIndexingStatus } from '../../../shared/schema/types'; import { EngineDetails, EngineTypes } from './types'; @@ -18,6 +17,7 @@ interface EngineValues { engineName: string; isMetaEngine: boolean; isSampleEngine: boolean; + hasSchemaErrors: boolean; hasSchemaConflicts: boolean; hasUnconfirmedSchemaFields: boolean; engineNotFound: boolean; @@ -26,7 +26,6 @@ interface EngineValues { interface EngineActions { setEngineData(engine: EngineDetails): { engine: EngineDetails }; setEngineName(engineName: string): { engineName: string }; - setIndexingStatus(activeReindexJob: IIndexingStatus): { activeReindexJob: IIndexingStatus }; setEngineNotFound(notFound: boolean): { notFound: boolean }; clearEngine(): void; initializeEngine(): void; @@ -37,7 +36,6 @@ export const EngineLogic = kea>({ actions: { setEngineData: (engine) => ({ engine }), setEngineName: (engineName) => ({ engineName }), - setIndexingStatus: (activeReindexJob) => ({ activeReindexJob }), setEngineNotFound: (notFound) => ({ notFound }), clearEngine: true, initializeEngine: true, @@ -55,10 +53,6 @@ export const EngineLogic = kea>({ { setEngineData: (_, { engine }) => engine, clearEngine: () => ({}), - setIndexingStatus: (state, { activeReindexJob }) => ({ - ...state, - activeReindexJob, - }), }, ], engineName: [ @@ -79,6 +73,12 @@ export const EngineLogic = kea>({ selectors: ({ selectors }) => ({ isMetaEngine: [() => [selectors.engine], (engine) => engine?.type === EngineTypes.meta], isSampleEngine: [() => [selectors.engine], (engine) => !!engine?.sample], + // Indexed engines + hasSchemaErrors: [ + () => [selectors.engine], + ({ activeReindexJob }) => activeReindexJob?.numDocumentsWithErrors > 0, + ], + // Meta engines hasSchemaConflicts: [ () => [selectors.engine], (engine) => !!(engine?.schemaConflicts && Object.keys(engine.schemaConflicts).length > 0), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx index 1781883aa6532..d913e3921bf1b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx @@ -78,6 +78,12 @@ describe('EngineNav', () => { describe('schema nav icons', () => { const myRole = { canViewEngineSchema: true }; + it('renders schema errors alert icon', () => { + setMockValues({ ...values, myRole, hasSchemaErrors: true }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineNavSchemaErrors"]')).toHaveLength(1); + }); + it('renders unconfirmed schema fields info icon', () => { setMockValues({ ...values, myRole, hasUnconfirmedSchemaFields: true }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index 530accb501c41..4738209cee4a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -70,6 +70,7 @@ export const EngineNav: React.FC = () => { dataLoading, isSampleEngine, isMetaEngine, + hasSchemaErrors, hasSchemaConflicts, hasUnconfirmedSchemaFields, engine, @@ -128,9 +129,19 @@ export const EngineNav: React.FC = () => { shouldShowActiveForSubroutes data-test-subj="EngineSchemaLink" > - + {SCHEMA_TITLE} + {hasSchemaErrors && ( + + )} {hasUnconfirmedSchemaFields && ( { to={generateEnginePath(ENGINE_RELEVANCE_TUNING_PATH)} data-test-subj="EngineRelevanceTuningLink" > - + {RELEVANCE_TUNING_TITLE} {invalidBoosts && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx new file mode 100644 index 0000000000000..ea658c741b8a0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; + +import { EmptyState } from './'; + +describe('EmptyState', () => { + it('renders', () => { + const wrapper = shallow() + .find(EuiEmptyPrompt) + .dive(); + + expect(wrapper.find('h2').text()).toEqual('Create a schema'); + expect(wrapper.find(EuiButton).prop('href')).toEqual( + expect.stringContaining('#indexing-documents-guide-schema') + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx new file mode 100644 index 0000000000000..6d7dd198d5eef --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiPanel, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DOCS_PREFIX } from '../../../routes'; + +export const EmptyState: React.FC = () => { + return ( + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.schema.empty.title', { + defaultMessage: 'Create a schema', + })} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.schema.empty.description', { + defaultMessage: + 'Create schema fields in advance, or index some documents and a schema will be created for you.', + })} +

+ } + actions={ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.schema.empty.buttonLabel', { + defaultMessage: 'Read the indexing schema guide', + })} + + } + /> +
+ ); +}; diff --git a/x-pack/plugins/apm/server/lib/alerts/create_apm_lifecycle_rule_type.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts similarity index 53% rename from x-pack/plugins/apm/server/lib/alerts/create_apm_lifecycle_rule_type.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts index 8d250a5765cce..7da44849b5bc0 100644 --- a/x-pack/plugins/apm/server/lib/alerts/create_apm_lifecycle_rule_type.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; -import { APMRuleRegistry } from '../../plugin'; - -export const createAPMLifecycleRuleType = createLifecycleRuleTypeFactory(); +export { SchemaCallouts } from './schema_callouts'; +export { SchemaTable } from './schema_table'; +export { EmptyState } from './empty_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_callouts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_callouts.test.tsx new file mode 100644 index 0000000000000..5bb08a6c8859a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_callouts.test.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__'; +import '../../../__mocks__/engine_logic.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { SchemaErrorsCallout } from '../../../../shared/schema'; + +import { + UnsearchedFieldsCallout, + UnconfirmedFieldsCallout, + ConfirmSchemaButton, +} from './schema_callouts'; + +import { SchemaCallouts } from './'; + +describe('SchemaCallouts', () => { + const values = { + hasUnconfirmedFields: false, + hasNewUnsearchedFields: false, + mostRecentIndexJob: { + hasErrors: false, + activeReindexJobId: 'some-id', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + }); + + it('renders nothing if there is nothing to call out', () => { + const wrapper = shallow(); + + expect(wrapper.text()).toBeFalsy(); + }); + + it('renders a schema errors callout if the most recent index job had errors', () => { + setMockValues({ + ...values, + mostRecentIndexJob: { + hasErrors: true, + activeReindexJobId: '12345', + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(SchemaErrorsCallout)).toHaveLength(1); + expect(wrapper.find(SchemaErrorsCallout).prop('viewErrorsPath')).toEqual( + '/engines/some-engine/schema/reindex_job/12345' + ); + }); + + it('renders an unsearched fields callout if the schema has new unconfirmed & unsearched fields', () => { + setMockValues({ + ...values, + hasUnconfirmedFields: true, + hasNewUnsearchedFields: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(UnsearchedFieldsCallout)).toHaveLength(1); + }); + + it('renders an unconfirmed fields callout if the schema has unconfirmed fields', () => { + setMockValues({ + ...values, + hasUnconfirmedFields: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(UnconfirmedFieldsCallout)).toHaveLength(1); + }); + + describe('UnsearchedFieldsCallout', () => { + it('renders an info callout about unsearched fields with a link to the relevance tuning page', () => { + const wrapper = shallow(); + + expect(wrapper.prop('title')).toEqual( + 'Recently added fields are not being searched by default' + ); + expect(wrapper.find('[data-test-subj="relevanceTuningButtonLink"]').prop('to')).toEqual( + '/engines/some-engine/relevance_tuning' + ); + }); + }); + + describe('UnconfirmedFieldsCallout', () => { + it('renders an info callout about unconfirmed fields', () => { + const wrapper = shallow(); + + expect(wrapper.prop('title')).toEqual("You've recently added new schema fields"); + }); + }); + + describe('ConfirmSchemaButton', () => { + const actions = { updateSchema: jest.fn() }; + + beforeEach(() => { + setMockValues({ isUpdating: false }); + setMockActions(actions); + }); + + it('allows users to confirm schema without changes from the callouts', () => { + const wrapper = shallow(); + + wrapper.simulate('click'); + expect(actions.updateSchema).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_callouts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_callouts.tsx new file mode 100644 index 0000000000000..50a2ee5c83abe --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_callouts.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { EuiCallOut, EuiButton, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { EuiButtonTo } from '../../../../shared/react_router_helpers'; +import { SchemaErrorsCallout } from '../../../../shared/schema'; +import { ENGINE_RELEVANCE_TUNING_PATH, ENGINE_REINDEX_JOB_PATH } from '../../../routes'; +import { generateEnginePath } from '../../engine'; + +import { SchemaLogic } from '../schema_logic'; + +export const SchemaCallouts: React.FC = () => { + const { + hasUnconfirmedFields, + hasNewUnsearchedFields, + mostRecentIndexJob: { hasErrors, activeReindexJobId }, + } = useValues(SchemaLogic); + + return ( + <> + {hasErrors && ( + <> + + + + )} + {hasUnconfirmedFields && ( + <> + {hasNewUnsearchedFields ? : } + + + )} + + ); +}; + +export const UnsearchedFieldsCallout: React.FC = () => ( + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.unsearchedFields.description', + { + defaultMessage: + 'If these new fields should be searchable, update your search settings to include them. If you want them to remain unsearchable, confirm your new field types to dismiss this alert.', + } + )} +

+ + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.unsearchedFields.searchSettingsButtonLabel', + { defaultMessage: 'Update search settings' } + )} + + + + + + +
+); + +export const UnconfirmedFieldsCallout: React.FC = () => ( + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.unconfirmedFields.description', + { + defaultMessage: + 'Set your new schema field(s) to their correct or expected types, and then confirm your field types.', + } + )} +

+ +
+); + +export const ConfirmSchemaButton: React.FC = () => { + const { updateSchema } = useActions(SchemaLogic); + const { isUpdating } = useValues(SchemaLogic); + + return ( + updateSchema()} + data-test-subj="confirmSchemaTypesButton" + > + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.schema.confirmSchemaButtonLabel', { + defaultMessage: 'Confirm types', + })} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.test.tsx new file mode 100644 index 0000000000000..c8b0bb7ddbac5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.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 { setMockValues, setMockActions } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiTable, EuiTableHeaderCell, EuiTableRow, EuiHealth } from '@elastic/eui'; + +import { SchemaFieldTypeSelect } from '../../../../shared/schema'; + +import { SchemaTable } from './'; + +describe('SchemaTable', () => { + const values = { + schema: {}, + unconfirmedFields: [], + }; + const actions = { + updateSchemaFieldType: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell).first().prop('children')).toEqual('Field name'); + expect(wrapper.find(EuiTableHeaderCell).last().prop('children')).toEqual('Field type'); + }); + + it('always renders an initial ID row (with no field type select)', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTableRow)).toHaveLength(1); + expect(wrapper.find('code').text()).toEqual('id'); + expect(wrapper.find(SchemaFieldTypeSelect)).toHaveLength(0); + }); + + it('renders subsequent table rows for each schema field', () => { + setMockValues({ + ...values, + schema: { + some_text_field: 'text', + some_number_field: 'number', + some_date_field: 'date', + some_location_field: 'geolocation', + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiTableRow)).toHaveLength(5); + + expect(wrapper.find('code').at(1).text()).toEqual('some_text_field'); + expect(wrapper.find(SchemaFieldTypeSelect).at(0).prop('fieldType')).toEqual('text'); + + expect(wrapper.find('code').last().text()).toEqual('some_location_field'); + expect(wrapper.find(SchemaFieldTypeSelect).last().prop('fieldType')).toEqual('geolocation'); + }); + + it('renders a recently added status if a field has been recently added', () => { + setMockValues({ + ...values, + schema: { + some_new_field: 'text', + }, + unconfirmedFields: ['some_new_field'], + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiHealth)).toHaveLength(1); + expect(wrapper.find(EuiHealth).childAt(0).prop('children')).toEqual('Recently added'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.tsx new file mode 100644 index 0000000000000..8fff01b268b12 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { + EuiTable, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableBody, + EuiTableRow, + EuiTableRowCell, + EuiHealth, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { SchemaFieldTypeSelect } from '../../../../shared/schema'; +import { FIELD_NAME, FIELD_TYPE } from '../../../../shared/schema/constants'; + +import { SchemaLogic } from '../schema_logic'; + +export const SchemaTable: React.FC = () => { + const { schema, unconfirmedFields } = useValues(SchemaLogic); + const { updateSchemaFieldType } = useActions(SchemaLogic); + + return ( + + + {FIELD_NAME} + + + {FIELD_TYPE} + + + + + + + id + + + + + + {Object.entries(schema).map(([fieldName, fieldType]) => { + const isRecentlyAdded = unconfirmedFields.length && unconfirmedFields.includes(fieldName); + + return ( + + + {fieldName} + + {isRecentlyAdded ? ( + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.unconfirmedFieldLabel', + { defaultMessage: 'Recently added' } + )} + + + + ) : ( + + )} + + + + + ); + })} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx index 9e8386e2e8337..1755fbea5fb7b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx @@ -5,26 +5,72 @@ * 2.0. */ +import { setMockValues, setMockActions } from '../../../../__mocks__'; import '../../../../__mocks__/react_router_history.mock'; +import '../../../../__mocks__/shallow_useeffect.mock'; +import '../../../__mocks__/engine_logic.mock'; import React from 'react'; import { useParams } from 'react-router-dom'; import { shallow } from 'enzyme'; +import { Loading } from '../../../../shared/loading'; +import { SchemaErrorsAccordion } from '../../../../shared/schema'; + import { ReindexJob } from './'; describe('ReindexJob', () => { const props = { schemaBreadcrumb: ['Engines', 'some-engine', 'Schema'], }; + const values = { + dataLoading: false, + fieldCoercionErrors: {}, + engine: { + schema: { + some_field: 'text', + }, + }, + }; + const actions = { + loadReindexJob: jest.fn(), + }; beforeEach(() => { (useParams as jest.Mock).mockReturnValueOnce({ reindexJobId: 'abc1234567890' }); + setMockValues(values); + setMockActions(actions); }); it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SchemaErrorsAccordion)).toHaveLength(1); + expect(wrapper.find(SchemaErrorsAccordion).prop('generateViewPath')).toHaveLength(1); + }); + + it('calls loadReindexJob on page load', () => { shallow(); - // TODO: Check child components + + expect(actions.loadReindexJob).toHaveBeenCalledWith('abc1234567890'); + }); + + it('renders a loading state', () => { + setMockValues({ ...values, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('renders schema errors with links to document pages', () => { + const wrapper = shallow(); + const generateViewPath = wrapper + .find(SchemaErrorsAccordion) + .prop('generateViewPath') as Function; + + expect(generateViewPath('some-document-id')).toEqual( + '/engines/some-engine/documents/some-document-id' + ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx index 19da08d446300..576b4ae11603b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx @@ -5,15 +5,24 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; +import { useActions, useValues } from 'kea'; + import { EuiPageHeader, EuiPageContentBody } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FlashMessages } from '../../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs'; +import { Loading } from '../../../../shared/loading'; +import { SchemaErrorsAccordion } from '../../../../shared/schema'; + +import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../../routes'; +import { EngineLogic, generateEnginePath } from '../../engine'; + +import { ReindexJobLogic } from './reindex_job_logic'; interface Props { schemaBreadcrumb: BreadcrumbTrail; @@ -21,6 +30,17 @@ interface Props { export const ReindexJob: React.FC = ({ schemaBreadcrumb }) => { const { reindexJobId } = useParams() as { reindexJobId: string }; + const { loadReindexJob } = useActions(ReindexJobLogic); + const { dataLoading, fieldCoercionErrors } = useValues(ReindexJobLogic); + const { + engine: { schema }, + } = useValues(EngineLogic); + + useEffect(() => { + loadReindexJob(reindexJobId); + }, [reindexJobId]); + + if (dataLoading) return ; return ( <> @@ -39,7 +59,15 @@ export const ReindexJob: React.FC = ({ schemaBreadcrumb }) => { )} /> - {reindexJobId} + + + generateEnginePath(ENGINE_DOCUMENT_DETAIL_PATH, { documentId }) + } + /> + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job_logic.test.ts new file mode 100644 index 0000000000000..b872ad3c914c6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job_logic.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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__'; +import '../../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { ReindexJobLogic } from './reindex_job_logic'; + +describe('ReindexJobLogic', () => { + const { mount } = new LogicMounter(ReindexJobLogic); + const { http } = mockHttpValues; + const { flashAPIErrors } = mockFlashMessageHelpers; + + const MOCK_RESPONSE = { + fieldCoercionErrors: { + some_erroring_field: [ + { + external_id: 'document-1', + error: "Value 'some text' cannot be parsed as a number", + }, + ], + another_erroring_field: [ + { + external_id: 'document-2', + error: "Value '123' cannot be parsed as a date", + }, + ], + }, + }; + + const DEFAULT_VALUES = { + dataLoading: true, + fieldCoercionErrors: {}, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(ReindexJobLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onLoadSuccess', () => { + it('stores fieldCoercionErrors state and sets dataLoading to false', () => { + mount({ fieldCoercionErrors: {}, dataLoading: true }); + + ReindexJobLogic.actions.onLoadSuccess(MOCK_RESPONSE); + + expect(ReindexJobLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + fieldCoercionErrors: MOCK_RESPONSE.fieldCoercionErrors, + }); + }); + }); + + describe('onLoadError', () => { + it('sets dataLoading to false', () => { + mount({ dataLoading: true }); + + ReindexJobLogic.actions.onLoadError(); + + expect(ReindexJobLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + }); + }); + }); + }); + + describe('listeners', () => { + describe('loadReindexJob', () => { + it('sets dataLoading to true', () => { + mount({ dataLoading: false }); + + ReindexJobLogic.actions.loadReindexJob('some-job-id'); + + expect(ReindexJobLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: true, + }); + }); + + it('should make an API call and then set schema state', async () => { + http.get.mockReturnValueOnce(Promise.resolve(MOCK_RESPONSE)); + mount(); + jest.spyOn(ReindexJobLogic.actions, 'onLoadSuccess'); + + ReindexJobLogic.actions.loadReindexJob('some-job-id'); + await nextTick(); + + expect(http.get).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/reindex_job/some-job-id' + ); + expect(ReindexJobLogic.actions.onLoadSuccess).toHaveBeenCalledWith(MOCK_RESPONSE); + }); + + it('handles errors', async () => { + http.get.mockReturnValueOnce(Promise.reject('error')); + mount(); + jest.spyOn(ReindexJobLogic.actions, 'onLoadError'); + + ReindexJobLogic.actions.loadReindexJob('some-bad-id'); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(ReindexJobLogic.actions.onLoadError).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job_logic.ts new file mode 100644 index 0000000000000..8cc76f7f6e94f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job_logic.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { flashAPIErrors } from '../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../shared/http'; +import { EngineLogic } from '../../engine'; + +import { ReindexJobApiResponse } from '../types'; + +export interface ReindexJobValues { + dataLoading: boolean; + fieldCoercionErrors: ReindexJobApiResponse['fieldCoercionErrors']; +} + +export interface ReindexJobActions { + loadReindexJob(id: string): string; + onLoadSuccess(response: ReindexJobApiResponse): ReindexJobApiResponse; + onLoadError(): void; +} + +export const ReindexJobLogic = kea>({ + path: ['enterprise_search', 'app_search', 'reindex_job_logic'], + actions: { + loadReindexJob: (id) => id, + onLoadSuccess: (response) => response, + onLoadError: true, + }, + reducers: { + dataLoading: [ + true, + { + loadReindexJob: () => true, + onLoadSuccess: () => false, + onLoadError: () => false, + }, + ], + fieldCoercionErrors: [ + {}, + { + onLoadSuccess: (_, { fieldCoercionErrors }) => fieldCoercionErrors, + }, + ], + }, + listeners: ({ actions }) => ({ + loadReindexJob: async (id) => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const response = await http.get(`/api/app_search/engines/${engineName}/reindex_job/${id}`); + actions.onLoadSuccess(response); + } catch (e) { + flashAPIErrors(e); + actions.onLoadError(); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_base_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_base_logic.test.ts index e5dbf97b971d9..2f5788278aa0b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_base_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_base_logic.test.ts @@ -72,6 +72,17 @@ describe('SchemaBaseLogic', () => { describe('listeners', () => { describe('loadSchema', () => { + it('sets dataLoading to true', () => { + mount({ dataLoading: false }); + + SchemaBaseLogic.actions.loadSchema(); + + expect(SchemaBaseLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: true, + }); + }); + it('should make an API call and then set schema state', async () => { http.get.mockReturnValueOnce(Promise.resolve(MOCK_RESPONSE)); mount(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx index fd6a742d00cda..23d1480e5dca9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx @@ -15,15 +15,26 @@ import { shallow } from 'enzyme'; import { EuiPageHeader, EuiButton } from '@elastic/eui'; import { Loading } from '../../../../shared/loading'; +import { SchemaAddFieldModal } from '../../../../shared/schema'; + +import { SchemaCallouts, SchemaTable, EmptyState } from '../components'; import { Schema } from './'; describe('Schema', () => { const values = { dataLoading: false, + hasSchema: true, + hasSchemaChanged: false, + isUpdating: false, + isModalOpen: false, }; const actions = { loadSchema: jest.fn(), + updateSchema: jest.fn(), + addSchemaField: jest.fn(), + openModal: jest.fn(), + closeModal: jest.fn(), }; beforeEach(() => { @@ -35,8 +46,8 @@ describe('Schema', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBe(false); - // TODO: Check for schema components + expect(wrapper.find(SchemaCallouts)).toHaveLength(1); + expect(wrapper.find(SchemaTable)).toHaveLength(1); }); it('calls loadSchema on mount', () => { @@ -52,14 +63,79 @@ describe('Schema', () => { expect(wrapper.find(Loading)).toHaveLength(1); }); - it('renders page action buttons', () => { - const wrapper = shallow() - .find(EuiPageHeader) - .dive() - .children() - .dive(); + it('renders an empty state', () => { + setMockValues({ ...values, hasSchema: false }); + const wrapper = shallow(); + + expect(wrapper.find(EmptyState)).toHaveLength(1); + }); + + describe('page action buttons', () => { + const subject = () => + shallow() + .find(EuiPageHeader) + .dive() + .children() + .dive(); + + it('renders', () => { + const wrapper = subject(); + expect(wrapper.find(EuiButton)).toHaveLength(2); + }); + + it('renders loading/disabled state when schema is updating', () => { + setMockValues({ isUpdating: true }); + const wrapper = subject(); + + expect(wrapper.find('[data-test-subj="updateSchemaButton"]').prop('isLoading')).toBe(true); + expect(wrapper.find('[data-test-subj="addSchemaFieldModalButton"]').prop('disabled')).toBe( + true + ); + }); + + describe('add button', () => { + it('opens the add schema field modal', () => { + const wrapper = subject(); + + wrapper.find('[data-test-subj="addSchemaFieldModalButton"]').simulate('click'); + expect(actions.openModal).toHaveBeenCalled(); + }); + }); + + describe('update button', () => { + describe('when nothing on the page has changed', () => { + it('is disabled', () => { + const wrapper = subject(); + + expect(wrapper.find('[data-test-subj="updateSchemaButton"]').prop('disabled')).toBe(true); + }); + }); + + describe('when schema has been changed locally', () => { + it('is enabled', () => { + setMockValues({ ...values, hasSchemaChanged: true }); + const wrapper = subject(); + + expect(wrapper.find('[data-test-subj="updateSchemaButton"]').prop('disabled')).toBe( + false + ); + }); + + it('calls updateSchema on click', () => { + setMockValues({ ...values, hasSchemaChanged: true }); + const wrapper = subject(); + + wrapper.find('[data-test-subj="updateSchemaButton"]').simulate('click'); + expect(actions.updateSchema).toHaveBeenCalled(); + }); + }); + }); + }); + + it('renders a modal that lets a user add a new schema field', () => { + setMockValues({ isModalOpen: true }); + const wrapper = shallow(); - expect(wrapper.find(EuiButton)).toHaveLength(2); - // TODO: Expect click actions + expect(wrapper.find(SchemaAddFieldModal)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx index 21dd52b04f4a7..7bc995b16468a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx @@ -14,12 +14,18 @@ import { i18n } from '@kbn/i18n'; import { FlashMessages } from '../../../../shared/flash_messages'; import { Loading } from '../../../../shared/loading'; +import { SchemaAddFieldModal } from '../../../../shared/schema'; +import { SchemaCallouts, SchemaTable, EmptyState } from '../components'; import { SchemaLogic } from '../schema_logic'; export const Schema: React.FC = () => { - const { loadSchema } = useActions(SchemaLogic); - const { dataLoading } = useValues(SchemaLogic); + const { loadSchema, updateSchema, addSchemaField, openModal, closeModal } = useActions( + SchemaLogic + ); + const { dataLoading, isUpdating, hasSchema, hasSchemaChanged, isModalOpen } = useValues( + SchemaLogic + ); useEffect(() => { loadSchema(); @@ -38,13 +44,24 @@ export const Schema: React.FC = () => { { defaultMessage: 'Add new fields or change the types of existing ones.' } )} rightSideItems={[ - + updateSchema()} + data-test-subj="updateSchemaButton" + > {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.schema.updateSchemaButtonLabel', { defaultMessage: 'Update types' } )} , - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.schema.createSchemaFieldButtonLabel', { defaultMessage: 'Create a schema field' } @@ -53,7 +70,13 @@ export const Schema: React.FC = () => { ]} /> - TODO + + + {hasSchema ? : } + {isModalOpen && ( + + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx index 10f80d9a6345a..cf4e1a2d6cabc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx @@ -10,6 +10,7 @@ import React, { Fragment } from 'react'; import { useValues, useActions } from 'kea'; import { EuiCallOut, EuiSpacer, EuiGlobalToastList } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FLASH_MESSAGE_TYPES, DEFAULT_TOAST_TIMEOUT } from './constants'; import { FlashMessagesLogic } from './flash_messages_logic'; @@ -25,7 +26,14 @@ export const Callouts: React.FC = ({ children }) => { const { messages } = useValues(FlashMessagesLogic); return ( -
+
{messages.map(({ type, message, description }, index) => ( { expect(wrapper.find(EuiSelect).prop('disabled')).toEqual(true); }); + + it('passes arbitrary props', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiSelect).prop('aria-label')).toEqual('Test label'); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx index 8dfd87f4015d6..fb6c0f2047b12 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx @@ -23,10 +23,12 @@ export const SchemaFieldTypeSelect: React.FC = ({ fieldType, updateExistingFieldType, disabled, + ...rest }) => { const fieldTypeOptions = Object.values(SchemaType).map((type) => ({ value: type, text: type })); return ( { {SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER} - + {SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER} @@ -58,6 +58,7 @@ export const SchemaFieldsTable: React.FC = () => { fieldName={fieldName} fieldType={filteredSchemaFields[fieldName]} updateExistingFieldType={updateExistingFieldType} + aria-labelledby="schemaDataType" /> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx index 528065da23af6..dda3eeea54926 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -120,14 +120,6 @@ describe('SourceRouter', () => { }); describe('reset state', () => { - it('does not reset state when switching between source tree views', () => { - mockLocation.pathname = `/sources/${contentSource.id}`; - shallow(); - unmountHandler(); - - expect(resetSourceState).not.toHaveBeenCalled(); - }); - it('resets state when leaving source tree', () => { mockLocation.pathname = '/home'; shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index cd20e32def16d..d5d6c8e541e4f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -55,12 +55,12 @@ export const SourceRouter: React.FC = () => { useEffect(() => { initializeSource(sourceId); - return () => { - // We only want to reset the state when leaving the source section. Otherwise there is an unwanted flash of UI. - if (!pathname.includes(sourceId)) resetSourceState(); - }; }, [pathname]); + useEffect(() => { + return resetSourceState; + }, []); + if (dataLoading) return ; const { diff --git a/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js b/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js index 6b16c955c396e..6a58e863aed5f 100644 --- a/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js +++ b/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js @@ -286,7 +286,7 @@ describe('createChunks', () => { expect(chunks[1].length).toBe(2); }); - test('should break features into chunks containing only single feature when feature size is greater then maxChunkCharCount', () => { + test('should break features into chunks containing only single feature when feature size is greater than maxChunkCharCount', () => { const maxChunkCharCount = GEOMETRY_COLLECTION_DOC_CHARS * 0.8; const chunks = createChunks(features, ES_FIELD_TYPES.GEO_SHAPE, maxChunkCharCount); expect(chunks.length).toBe(5); diff --git a/x-pack/plugins/file_upload/server/capabilities.test.ts b/x-pack/plugins/file_upload/server/capabilities.test.ts new file mode 100644 index 0000000000000..2fc666c837961 --- /dev/null +++ b/x-pack/plugins/file_upload/server/capabilities.test.ts @@ -0,0 +1,267 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setupCapabilities } from './capabilities'; +import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; +import { Capabilities, CoreStart } from 'kibana/server'; +import { securityMock } from '../../security/server/mocks'; + +describe('setupCapabilities', () => { + it('registers a capabilities provider for the file upload feature', () => { + const coreSetup = coreMock.createSetup(); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1); + const [provider] = coreSetup.capabilities.registerProvider.mock.calls[0]; + expect(provider()).toMatchInlineSnapshot(` + Object { + "fileUpload": Object { + "show": true, + }, + } + `); + }); + + it('registers a capabilities switcher that returns unaltered capabilities when security is disabled', async () => { + const coreSetup = coreMock.createSetup(); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "fileUpload": Object { + "show": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + }); + + it('registers a capabilities switcher that returns unaltered capabilities when default capabilities are requested', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(true); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, true)).resolves.toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "fileUpload": Object { + "show": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + + expect(security.authz.mode.useRbacForRequest).not.toHaveBeenCalled(); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); + }); + + it('registers a capabilities switcher that disables capabilities for underprivileged users', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(true); + + const mockCheckPrivileges = jest.fn().mockResolvedValue({ hasAllRequested: false }); + security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "fileUpload": Object { + "show": false, + }, + } + `); + + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledTimes(1); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(request); + }); + + it('registers a capabilities switcher that enables capabilities for privileged users', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(true); + + const mockCheckPrivileges = jest.fn().mockResolvedValue({ hasAllRequested: true }); + security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "fileUpload": Object { + "show": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledTimes(1); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(request); + }); + + it('registers a capabilities switcher that disables capabilities for unauthenticated requests', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(true); + const mockCheckPrivileges = jest + .fn() + .mockRejectedValue(new Error('this should not have been called')); + security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest({ auth: { isAuthenticated: false } }); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "fileUpload": Object { + "show": false, + }, + } + `); + + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); + }); + + it('registers a capabilities switcher that skips privilege check for requests not using rbac', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(false); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "fileUpload": Object { + "show": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/file_upload/server/capabilities.ts b/x-pack/plugins/file_upload/server/capabilities.ts new file mode 100644 index 0000000000000..17880b98150d6 --- /dev/null +++ b/x-pack/plugins/file_upload/server/capabilities.ts @@ -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 { CoreSetup } from 'kibana/server'; +import { checkFileUploadPrivileges } from './check_privileges'; +import { StartDeps } from './types'; + +export const setupCapabilities = ( + core: Pick, 'capabilities' | 'getStartServices'> +) => { + core.capabilities.registerProvider(() => { + return { + fileUpload: { + show: true, + }, + }; + }); + + core.capabilities.registerSwitcher(async (request, capabilities, useDefaultCapabilities) => { + if (useDefaultCapabilities) { + return capabilities; + } + const [, { security }] = await core.getStartServices(); + + // Check the bare minimum set of privileges required to get some utility out of this feature + const { hasImportPermission } = await checkFileUploadPrivileges({ + authorization: security?.authz, + request, + checkCreateIndexPattern: true, + checkHasManagePipeline: false, + }); + + if (!hasImportPermission) { + return { + fileUpload: { + show: false, + }, + }; + } + + return capabilities; + }); +}; diff --git a/x-pack/plugins/file_upload/server/check_privileges.ts b/x-pack/plugins/file_upload/server/check_privileges.ts new file mode 100644 index 0000000000000..42cc53f693fec --- /dev/null +++ b/x-pack/plugins/file_upload/server/check_privileges.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaRequest } from 'kibana/server'; +import { AuthorizationServiceSetup, CheckPrivilegesPayload } from '../../security/server'; + +interface Deps { + request: KibanaRequest; + authorization?: Pick< + AuthorizationServiceSetup, + 'mode' | 'actions' | 'checkPrivilegesDynamicallyWithRequest' + >; + checkHasManagePipeline: boolean; + checkCreateIndexPattern: boolean; + indexName?: string; +} + +export const checkFileUploadPrivileges = async ({ + request, + authorization, + checkHasManagePipeline, + checkCreateIndexPattern, + indexName, +}: Deps) => { + const requiresAuthz = authorization?.mode.useRbacForRequest(request) ?? false; + + if (!authorization || !requiresAuthz) { + return { hasImportPermission: true }; + } + + if (!request.auth.isAuthenticated) { + return { hasImportPermission: false }; + } + + const checkPrivilegesPayload: CheckPrivilegesPayload = { + elasticsearch: { + cluster: checkHasManagePipeline ? ['manage_pipeline'] : [], + index: indexName ? { [indexName]: ['create', 'create_index'] } : {}, + }, + }; + if (checkCreateIndexPattern) { + checkPrivilegesPayload.kibana = [ + authorization.actions.savedObject.get('index-pattern', 'create'), + ]; + } + + const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(request); + const checkPrivilegesResp = await checkPrivileges(checkPrivilegesPayload); + + return { hasImportPermission: checkPrivilegesResp.hasAllRequested }; +}; diff --git a/x-pack/plugins/file_upload/server/plugin.ts b/x-pack/plugins/file_upload/server/plugin.ts index 5a4b59fe4f5e6..80fe041207110 100644 --- a/x-pack/plugins/file_upload/server/plugin.ts +++ b/x-pack/plugins/file_upload/server/plugin.ts @@ -13,6 +13,7 @@ import { initFileUploadTelemetry } from './telemetry'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { UI_SETTING_MAX_FILE_SIZE, MAX_FILE_SIZE } from '../common'; import { StartDeps } from './types'; +import { setupCapabilities } from './capabilities'; interface SetupDeps { usageCollection: UsageCollectionSetup; @@ -28,6 +29,8 @@ export class FileUploadPlugin implements Plugin { async setup(coreSetup: CoreSetup, plugins: SetupDeps) { fileUploadRoutes(coreSetup, this._logger); + setupCapabilities(coreSetup); + coreSetup.uiSettings.register({ [UI_SETTING_MAX_FILE_SIZE]: { name: i18n.translate('xpack.fileUpload.maxFileSizeUiSetting.name', { diff --git a/x-pack/plugins/file_upload/server/routes.ts b/x-pack/plugins/file_upload/server/routes.ts index 847a57afb391c..3033f8300712c 100644 --- a/x-pack/plugins/file_upload/server/routes.ts +++ b/x-pack/plugins/file_upload/server/routes.ts @@ -22,8 +22,8 @@ import { analyzeFile } from './analyze_file'; import { updateTelemetry } from './telemetry'; import { importFileBodySchema, importFileQuerySchema, analyzeFileQuerySchema } from './schemas'; -import { CheckPrivilegesPayload } from '../../security/server'; import { StartDeps } from './types'; +import { checkFileUploadPrivileges } from './check_privileges'; function importData( client: IScopedClusterClient, @@ -60,29 +60,15 @@ export function fileUploadRoutes(coreSetup: CoreSetup, logge const [, pluginsStart] = await coreSetup.getStartServices(); const { indexName, checkCreateIndexPattern, checkHasManagePipeline } = request.query; - const authorizationService = pluginsStart.security?.authz; - const requiresAuthz = authorizationService?.mode.useRbacForRequest(request) ?? false; - - if (!authorizationService || !requiresAuthz) { - return response.ok({ body: { hasImportPermission: true } }); - } - - const checkPrivilegesPayload: CheckPrivilegesPayload = { - elasticsearch: { - cluster: checkHasManagePipeline ? ['manage_pipeline'] : [], - index: indexName ? { [indexName]: ['create', 'create_index'] } : {}, - }, - }; - if (checkCreateIndexPattern) { - checkPrivilegesPayload.kibana = [ - authorizationService.actions.savedObject.get('index-pattern', 'create'), - ]; - } - - const checkPrivileges = authorizationService.checkPrivilegesDynamicallyWithRequest(request); - const checkPrivilegesResp = await checkPrivileges(checkPrivilegesPayload); + const { hasImportPermission } = await checkFileUploadPrivileges({ + authorization: pluginsStart.security?.authz, + request, + indexName, + checkCreateIndexPattern, + checkHasManagePipeline, + }); - return response.ok({ body: { hasImportPermission: checkPrivilegesResp.hasAllRequested } }); + return response.ok({ body: { hasImportPermission } }); } catch (e) { logger.warn(`Unable to check import permission, error: ${e.message}`); return response.ok({ body: { hasImportPermission: false } }); diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 03584a48ff17c..7117973baa139 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -15,13 +15,6 @@ export interface FleetConfigType { registryProxyUrl?: string; agents: { enabled: boolean; - tlsCheckDisabled: boolean; - pollingRequestTimeout: number; - maxConcurrentConnections: number; - kibana: { - host?: string[] | string; - ca_sha256?: string; - }; elasticsearch: { host?: string; ca_sha256?: string; @@ -29,8 +22,6 @@ export interface FleetConfigType { fleet_server?: { hosts?: string[]; }; - agentPolicyRolloutRateLimitIntervalMs: number; - agentPolicyRolloutRateLimitRequestPerInterval: number; }; agentPolicies?: PreconfiguredAgentPolicy[]; packages?: PreconfiguredPackage[]; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts index 5d53425607361..7f0b71de779dc 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts @@ -14,19 +14,10 @@ export const createConfigurationMock = (): FleetConfigType => { registryProxyUrl: '', agents: { enabled: true, - tlsCheckDisabled: true, - pollingRequestTimeout: 1000, - maxConcurrentConnections: 100, - kibana: { - host: '', - ca_sha256: '', - }, elasticsearch: { host: '', ca_sha256: '', }, - agentPolicyRolloutRateLimitIntervalMs: 100, - agentPolicyRolloutRateLimitRequestPerInterval: 1000, }, }; }; diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 4bc2bea1e58b6..a94f274b202ad 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -30,6 +30,10 @@ export const createAppContextStartContractMock = (): FleetAppContext => { security: securityMock.createStart(), logger: loggingSystemMock.create().get(), isProductionMode: true, + configInitialValue: { + agents: { enabled: true, elasticsearch: {} }, + enabled: true, + }, kibanaVersion: '8.0.0', kibanaBranch: 'master', }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 61c3a83242c57..0fdc6ef651ed1 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -6,7 +6,6 @@ */ import type { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import type { CoreSetup, CoreStart, @@ -110,6 +109,7 @@ export interface FleetAppContext { encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; security?: SecurityPluginStart; config$?: Observable; + configInitialValue: FleetConfigType; savedObjects: SavedObjectsServiceStart; isProductionMode: PluginInitializerContext['env']['mode']['prod']; kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; @@ -189,6 +189,7 @@ export class FleetPlugin implements AsyncPlugin { private licensing$!: Observable; private config$: Observable; + private configInitialValue: FleetConfigType; private cloud: CloudSetup | undefined; private logger: Logger | undefined; @@ -204,15 +205,15 @@ export class FleetPlugin this.kibanaVersion = this.initializerContext.env.packageInfo.version; this.kibanaBranch = this.initializerContext.env.packageInfo.branch; this.logger = this.initializerContext.logger.get(); + this.configInitialValue = this.initializerContext.config.get(); } - public async setup(core: CoreSetup, deps: FleetSetupDeps) { + public setup(core: CoreSetup, deps: FleetSetupDeps) { this.httpSetup = core.http; this.licensing$ = deps.licensing.license$; this.encryptedSavedObjectsSetup = deps.encryptedSavedObjects; this.cloud = deps.cloud; - - const config = await this.config$.pipe(first()).toPromise(); + const config = this.configInitialValue; registerSavedObjects(core.savedObjects, deps.encryptedSavedObjects); registerEncryptedSavedObjects(deps.encryptedSavedObjects); @@ -279,13 +280,14 @@ export class FleetPlugin } } - public async start(core: CoreStart, plugins: FleetStartDeps): Promise { - await appContextService.start({ + public start(core: CoreStart, plugins: FleetStartDeps): FleetStartContract { + appContextService.start({ elasticsearch: core.elasticsearch, data: plugins.data, encryptedSavedObjectsStart: plugins.encryptedSavedObjects, encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup, security: plugins.security, + configInitialValue: this.configInitialValue, config$: this.config$, savedObjects: core.savedObjects, isProductionMode: this.isProductionMode, diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index f0991ab01a6ed..511a0abecbc18 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -14,6 +14,7 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/s import { esKuery } from '../../../../../../src/plugins/data/server'; import type { ESSearchResponse as SearchResponse } from '../../../../../../typings/elasticsearch'; import type { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types'; +import { IngestManagerError } from '../../errors'; import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; import { agentPolicyService } from '../agent_policy'; import { escapeSearchQueryPhrase } from '../saved_object'; @@ -28,10 +29,13 @@ export async function listEnrollmentApiKeys( page?: number; perPage?: number; kuery?: string; + query?: ReturnType; showInactive?: boolean; } ): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { const { page = 1, perPage = 20, kuery } = options; + const query = + options.query ?? (kuery && esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kuery))); const res = await esClient.search>({ index: ENROLLMENT_API_KEYS_INDEX, @@ -40,9 +44,7 @@ export async function listEnrollmentApiKeys( sort: 'created_at:desc', track_total_hits: true, ignore_unavailable: true, - body: kuery - ? { query: esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kuery)) } - : undefined, + body: query ? { query } : undefined, }); // @ts-expect-error @elastic/elasticsearch @@ -159,7 +161,7 @@ export async function generateEnrollmentAPIKey( const { items } = await listEnrollmentApiKeys(esClient, { page: page++, perPage: 100, - kuery: `policy_id:"${agentPolicyId}" AND name:${providedKeyName.replace(/ /g, '\\ ')}*`, + query: getQueryForExistingKeyNameOnPolicy(agentPolicyId, providedKeyName), }); if (items.length === 0) { hasMore = false; @@ -176,7 +178,7 @@ export async function generateEnrollmentAPIKey( k.name?.replace(providedKeyName, '').trim().match(uuidRegex) ) ) { - throw new Error( + throw new IngestManagerError( i18n.translate('xpack.fleet.serverError.enrollmentKeyDuplicate', { defaultMessage: 'An enrollment key named {providedKeyName} already exists for agent policy {agentPolicyId}', @@ -254,6 +256,29 @@ export async function generateEnrollmentAPIKey( }; } +function getQueryForExistingKeyNameOnPolicy(agentPolicyId: string, providedKeyName: string) { + const query = { + bool: { + filter: [ + { + bool: { + should: [{ match_phrase: { policy_id: agentPolicyId } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ query_string: { fields: ['name'], query: `(${providedKeyName}) *` } }], + minimum_should_match: 1, + }, + }, + ], + }, + }; + + return query; +} + export async function getEnrollmentAPIKeyById(esClient: ElasticsearchClient, apiKeyId: string) { const res = await esClient.search({ index: ENROLLMENT_API_KEYS_INDEX, diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 954308a980861..82ec0aad52651 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -7,7 +7,6 @@ import type { Observable } from 'rxjs'; import { BehaviorSubject } from 'rxjs'; -import { first } from 'rxjs/operators'; import { kibanaPackageJson } from '@kbn/utils'; import type { KibanaRequest } from 'src/core/server'; import type { @@ -44,7 +43,7 @@ class AppContextService { private httpSetup?: HttpServiceSetup; private externalCallbacks: ExternalCallbacksStorage = new Map(); - public async start(appContext: FleetAppContext) { + public start(appContext: FleetAppContext) { this.data = appContext.data; this.esClient = appContext.elasticsearch.client.asInternalUser; this.encryptedSavedObjects = appContext.encryptedSavedObjectsStart?.getClient(); @@ -60,7 +59,7 @@ class AppContextService { if (appContext.config$) { this.config$ = appContext.config$; - const initialValue = await this.config$.pipe(first()).toPromise(); + const initialValue = appContext.configInitialValue; this.configSubject$ = new BehaviorSubject(initialValue); this.config$.subscribe(this.configSubject$); } 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 0a755228e0b98..9e04d75f36a16 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -77,8 +77,6 @@ export const pkgToPkgKey = ({ name, version }: { name: string; version: string } export async function fetchList(params?: SearchParams): Promise { const registryUrl = getRegistryUrl(); const url = new URL(`${registryUrl}/search`); - const kibanaVersion = appContextService.getKibanaVersion().split('-')[0]; // may be x.y.z-SNAPSHOT - const kibanaBranch = appContextService.getKibanaBranch(); if (params) { if (params.category) { url.searchParams.set('category', params.category); @@ -88,10 +86,7 @@ export async function fetchList(params?: SearchParams): Promise { return getResponse(`${registryUrl}${filePath}`); } +function setKibanaVersion(url: URL) { + const kibanaVersion = appContextService.getKibanaVersion().split('-')[0]; // may be x.y.z-SNAPSHOT + const kibanaBranch = appContextService.getKibanaBranch(); + + // on master, request all packages regardless of version + if (kibanaVersion && kibanaBranch !== 'master') { + url.searchParams.set('kibana.version', kibanaVersion); + } +} + export async function fetchCategories(params?: CategoriesParams): Promise { const registryUrl = getRegistryUrl(); const url = new URL(`${registryUrl}/categories`); @@ -154,6 +159,8 @@ export async function fetchCategories(params?: CategoriesParams): Promise { }; const simulateTypeChar = async (text: string) => { - await waitFor(() => - getSearchProps(component).onKeyUpCapture({ currentTarget: { value: text } }) - ); + await waitFor(() => getSearchProps(component).onInput({ currentTarget: { value: text } })); }; const getDisplayedOptionsTitle = () => { diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index dfe73512279ed..5234b4e7b0ad5 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -398,8 +398,7 @@ export function SearchBar({ } searchProps={{ - onKeyUpCapture: (e: React.KeyboardEvent) => - setSearchValue(e.currentTarget.value), + onInput: (e: React.UIEvent) => setSearchValue(e.currentTarget.value), 'data-test-subj': 'nav-search-input', inputRef: setSearchRef, compressed: true, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index f9e04b3d1772c..06b7739e03c54 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -173,7 +173,7 @@ export const LegendControls = ({ const errors = !boundsValidRange ? [ i18n.translate('xpack.infra.legnedControls.boundRangeError', { - defaultMessage: 'Minimum must be smaller then the maximum', + defaultMessage: 'Minimum must be smaller than the maximum', }), ] : []; diff --git a/x-pack/plugins/lens/public/assets/globe_illustration.tsx b/x-pack/plugins/lens/public/assets/globe_illustration.tsx new file mode 100644 index 0000000000000..af2f2c7a48e46 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/globe_illustration.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const GlobeIllustration = ({ title, titleId, ...props }: Omit) => ( + + + + + +); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index 3296ddac7bc29..9bf03025e400f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -15,6 +15,7 @@ import { Action } from './state_management'; import { DragContext, DragDropIdentifier } from '../../drag_drop'; import { StateSetter, FramePublicAPI, DatasourceDataPanelProps, Datasource } from '../../types'; import { Query, Filter } from '../../../../../../src/plugins/data/public'; +import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; interface DataPanelWrapperProps { datasourceState: unknown; @@ -29,6 +30,7 @@ interface DataPanelWrapperProps { filters: Filter[]; dropOntoWorkspace: (field: DragDropIdentifier) => void; hasSuggestionForField: (field: DragDropIdentifier) => boolean; + plugins: { uiActions: UiActionsStart }; } export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { @@ -56,6 +58,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { showNoDataPopover: props.showNoDataPopover, dropOntoWorkspace: props.dropOntoWorkspace, hasSuggestionForField: props.hasSuggestionForField, + uiActions: props.plugins.uiActions, }; const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 362787ea91c4f..91b59664ada83 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -332,6 +332,7 @@ export function EditorFrame(props: EditorFrameProps) { showNoDataPopover={props.showNoDataPopover} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} + plugins={props.plugins} /> } configPanel={ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.scss new file mode 100644 index 0000000000000..ca1a62cae64d6 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.scss @@ -0,0 +1,18 @@ +@import '../../../mixins'; + +.lnsVisualizeGeoFieldWorkspacePanel__dragDrop { + padding: $euiSizeXXL ($euiSizeXL * 2); + border: $euiBorderThin; + border-radius: $euiBorderRadius; + + &.lnsDragDrop-isDropTarget { + @include lnsDroppable; + @include lnsDroppableActive; + + } + + &.lnsDragDrop-isActiveDropTarget { + @include lnsDroppableActiveHover; + + } +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.tsx new file mode 100644 index 0000000000000..b50b6463c5a25 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiPageContentBody, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + UiActionsStart, + VISUALIZE_GEO_FIELD_TRIGGER, +} from '../../../../../../../src/plugins/ui_actions/public'; +import { getVisualizeGeoFieldMessage } from '../../../utils'; +import { DragDrop } from '../../../drag_drop'; +import { GlobeIllustration } from '../../../assets/globe_illustration'; +import './geo_field_workspace_panel.scss'; + +interface Props { + fieldType: string; + fieldName: string; + indexPatternId: string; + uiActions: UiActionsStart; +} + +const dragDropIdentifier = { + id: 'lnsGeoFieldWorkspace', + humanData: { + label: i18n.translate('xpack.lens.geoFieldWorkspace.dropZoneLabel', { + defaultMessage: 'drop zone to open in maps', + }), + }, +}; + +const dragDropOrder = [1, 0, 0, 0]; + +export function GeoFieldWorkspacePanel(props: Props) { + function onDrop() { + props.uiActions.getTrigger(VISUALIZE_GEO_FIELD_TRIGGER).exec({ + indexPatternId: props.indexPatternId, + fieldName: props.fieldName, + }); + } + + return ( + + +

+ {getVisualizeGeoFieldMessage(props.fieldType)} +

+ + +

+ + + +

+
+
+
+ ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index a31146e500434..3d5d9a6d84d81 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -338,17 +338,22 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ ); }; - return ( - + const dragDropContext = useContext(DragContext); + + const renderDragDrop = () => { + const customWorkspaceRenderer = + activeDatasourceId && + datasourceMap[activeDatasourceId]?.getCustomWorkspaceRenderer && + dragDropContext.dragging + ? datasourceMap[activeDatasourceId].getCustomWorkspaceRenderer!( + datasourceStates[activeDatasourceId].state, + dragDropContext.dragging + ) + : undefined; + + return customWorkspaceRenderer ? ( + customWorkspaceRenderer() + ) : ( + ); + }; + + return ( + + {renderDragDrop()} ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 849baa93652cc..46dc326a015a8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -44,7 +44,7 @@ export interface EditorFrameStartPlugins { embeddable?: EmbeddableStart; dashboard?: DashboardStart; expressions: ExpressionsStart; - uiActions?: UiActionsStart; + uiActions: UiActionsStart; charts: ChartsPluginSetup; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 6c5116436dddb..eeec7871a262c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -22,6 +22,7 @@ import { documentField } from './document_field'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { indexPatternFieldEditorPluginMock } from '../../../../../src/plugins/index_pattern_field_editor/public/mocks'; import { getFieldByNameFactory } from './pure_helpers'; +import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; const fieldsOne = [ { @@ -267,6 +268,7 @@ describe('IndexPattern Data Panel', () => { showNoDataPopover: jest.fn(), dropOntoWorkspace: jest.fn(), hasSuggestionForField: jest.fn(() => false), + uiActions: uiActionsPluginMock.createStartContract(), }; }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 4839d9388253b..a0a6b30e541a7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -41,6 +41,7 @@ import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; import { esQuery, IIndexPattern } from '../../../../../src/plugins/data/public'; import { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public'; +import { VISUALIZE_GEO_FIELD_TRIGGER } from '../../../../../src/plugins/ui_actions/public'; export type Props = Omit, 'core'> & { data: DataPublicPluginStart; @@ -73,6 +74,8 @@ const supportedFieldTypes = new Set([ 'ip_range', 'histogram', 'document', + 'geo_point', + 'geo_shape', ]); const fieldTypeNames: Record = { @@ -83,6 +86,8 @@ const fieldTypeNames: Record = { date: i18n.translate('xpack.lens.datatypes.date', { defaultMessage: 'date' }), ip: i18n.translate('xpack.lens.datatypes.ipAddress', { defaultMessage: 'IP' }), histogram: i18n.translate('xpack.lens.datatypes.histogram', { defaultMessage: 'histogram' }), + geo_point: i18n.translate('xpack.lens.datatypes.geoPoint', { defaultMessage: 'geo_point' }), + geo_shape: i18n.translate('xpack.lens.datatypes.geoShape', { defaultMessage: 'geo_shape' }), }; // Wrapper around esQuery.buildEsQuery, handling errors (e.g. because a query can't be parsed) by @@ -121,6 +126,7 @@ export function IndexPatternDataPanel({ showNoDataPopover, dropOntoWorkspace, hasSuggestionForField, + uiActions, }: Props) { const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state; const onChangeIndexPattern = useCallback( @@ -233,6 +239,7 @@ export function IndexPatternDataPanel({ existenceFetchTimeout={state.existenceFetchTimeout} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} + uiActions={uiActions} /> )} @@ -286,6 +293,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ charts, dropOntoWorkspace, hasSuggestionForField, + uiActions, }: Omit & { data: DataPublicPluginStart; core: CoreStart; @@ -310,7 +318,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ isMetaAccordionOpen: false, }); const currentIndexPattern = indexPatterns[currentIndexPatternId]; - const allFields = currentIndexPattern.fields; + const visualizeGeoFieldTrigger = uiActions.getTrigger(VISUALIZE_GEO_FIELD_TRIGGER); + const allFields = visualizeGeoFieldTrigger + ? currentIndexPattern.fields + : currentIndexPattern.fields.filter(({ type }) => type !== 'geo_point' && type !== 'geo_shape'); const clearLocalState = () => setLocalState((s) => ({ ...s, nameFilter: '', typeFilter: [] })); const hasSyncedExistingFields = existingFields[currentIndexPattern.title]; const availableFieldTypes = uniq(allFields.map(({ type }) => type)).filter( @@ -807,6 +818,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ hasSuggestionForField={hasSuggestionForField} editField={editField} removeField={removeField} + uiActions={uiActions} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index dcc11ea426117..cf9f7c0c559e4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -17,6 +17,7 @@ import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { IndexPattern } from './types'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { documentField } from './document_field'; +import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; const chartsThemeService = chartPluginMock.createSetupContract().theme; @@ -109,6 +110,7 @@ describe('IndexPattern Field Item', () => { itemIndex: 0, dropOntoWorkspace: () => {}, hasSuggestionForField: () => false, + uiActions: uiActionsPluginMock.createStartContract(), }; data.fieldFormats = ({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index fce4fcda14cfc..013bb46500d0d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -54,6 +54,9 @@ import { BucketedAggregation, FieldStatsResponse } from '../../common'; import { IndexPattern, IndexPatternField, DraggedField } from './types'; import { LensFieldIcon } from './lens_field_icon'; import { trackUiEvent } from '../lens_ui_telemetry'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { VisualizeGeoFieldButton } from './visualize_geo_field_button'; +import { getVisualizeGeoFieldMessage } from '../utils'; import { debouncedComponent } from '../debounced_component'; @@ -75,6 +78,7 @@ export interface FieldItemProps { editField?: (name: string) => void; removeField?: (name: string) => void; hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; + uiActions: UiActionsStart; } interface State { @@ -149,7 +153,13 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { function fetchData() { // Range types don't have any useful stats we can show - if (state.isLoading || field.type === 'document' || field.type.includes('range')) { + if ( + state.isLoading || + field.type === 'document' || + field.type.includes('range') || + field.type === 'geo_point' || + field.type === 'geo_shape' + ) { return; } @@ -392,6 +402,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { removeField, hasSuggestionForField, hideDetails, + uiActions, } = props; const chartTheme = chartsThemeService.useChartsTheme(); @@ -467,6 +478,21 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { ); + } else if (field.type === 'geo_point' || field.type === 'geo_shape') { + return ( + <> + {panelHeader} + + {getVisualizeGeoFieldMessage(field.type)} + + + + + ); } else if ( (!props.histogram || props.histogram.buckets.length === 0) && (!props.topValues || props.topValues.buckets.length === 0) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx index ee0011ad0390c..13d5d25bc2ea2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx @@ -14,6 +14,7 @@ import { NoFieldsCallout } from './no_fields_callout'; import { IndexPatternField } from './types'; import { FieldItemSharedProps, FieldsAccordion } from './fields_accordion'; import { DatasourceDataPanelProps } from '../types'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; const PAGINATION_SIZE = 50; export type FieldGroups = Record< @@ -55,6 +56,7 @@ export const FieldList = React.memo(function FieldList({ hasSuggestionForField, editField, removeField, + uiActions, }: { exists: (field: IndexPatternField) => boolean; fieldGroups: FieldGroups; @@ -72,6 +74,7 @@ export const FieldList = React.memo(function FieldList({ hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; editField?: (name: string) => void; removeField?: (name: string) => void; + uiActions: UiActionsStart; }) { const [pageSize, setPageSize] = useState(PAGINATION_SIZE); const [scrollContainer, setScrollContainer] = useState(undefined); @@ -155,6 +158,7 @@ export const FieldList = React.memo(function FieldList({ groupIndex={0} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} + uiActions={uiActions} /> )) )} @@ -206,6 +210,7 @@ export const FieldList = React.memo(function FieldList({ defaultNoFieldsMessage={fieldGroup.defaultNoFieldsMessage} /> } + uiActions={uiActions} />
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx index c8c48e2accf9b..6270b94abf565 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx @@ -15,6 +15,7 @@ import { IndexPattern } from './types'; import { FieldItem } from './field_item'; import { FieldsAccordion, FieldsAccordionProps, FieldItemSharedProps } from './fields_accordion'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; describe('Fields Accordion', () => { let defaultProps: FieldsAccordionProps; @@ -76,6 +77,7 @@ describe('Fields Accordion', () => { groupIndex: 0, dropOntoWorkspace: () => {}, hasSuggestionForField: () => false, + uiActions: uiActionsPluginMock.createStartContract(), }; }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index 42de7cb328b13..9f5409f9837f4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -24,6 +24,7 @@ import { Query, Filter } from '../../../../../src/plugins/data/public'; import { DatasourceDataPanelProps } from '../types'; import { IndexPattern } from './types'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; export interface FieldItemSharedProps { core: DatasourceDataPanelProps['core']; @@ -57,6 +58,7 @@ export interface FieldsAccordionProps { hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; editField?: (name: string) => void; removeField?: (name: string) => void; + uiActions: UiActionsStart; } export const FieldsAccordion = memo(function InnerFieldsAccordion({ @@ -80,6 +82,7 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({ hasSuggestionForField, editField, removeField, + uiActions, }: FieldsAccordionProps) { const renderField = useCallback( (field: IndexPatternField, index) => ( @@ -95,6 +98,7 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({ hasSuggestionForField={hasSuggestionForField} editField={editField} removeField={removeField} + uiActions={uiActions} /> ), [ @@ -106,6 +110,7 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({ groupIndex, editField, removeField, + uiActions, ] ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index a556c6ce0c095..f8bc84643bcab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -15,6 +15,7 @@ import { DataPublicPluginStart, } from '../../../../../src/plugins/data/public'; import { Datasource, EditorFrameSetup } from '../types'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; export interface IndexPatternDatasourceSetupPlugins { expressions: ExpressionsSetup; @@ -26,6 +27,7 @@ export interface IndexPatternDatasourceSetupPlugins { export interface IndexPatternDatasourceStartPlugins { data: DataPublicPluginStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; + uiActions: UiActionsStart; } export class IndexPatternDatasource { @@ -44,20 +46,23 @@ export class IndexPatternDatasource { getTimeScaleFunction, getSuffixFormatter, } = await import('../async_services'); - return core.getStartServices().then(([coreStart, { data, indexPatternFieldEditor }]) => { - data.fieldFormats.register([getSuffixFormatter(data.fieldFormats.deserialize)]); - expressions.registerFunction(getTimeScaleFunction(data)); - expressions.registerFunction(counterRate); - expressions.registerFunction(renameColumns); - expressions.registerFunction(formatColumn); - return getIndexPatternDatasource({ - core: coreStart, - storage: new Storage(localStorage), - data, - charts, - indexPatternFieldEditor, - }); - }) as Promise; + return core + .getStartServices() + .then(([coreStart, { data, indexPatternFieldEditor, uiActions }]) => { + data.fieldFormats.register([getSuffixFormatter(data.fieldFormats.deserialize)]); + expressions.registerFunction(getTimeScaleFunction(data)); + expressions.registerFunction(counterRate); + expressions.registerFunction(renameColumns); + expressions.registerFunction(formatColumn); + return getIndexPatternDatasource({ + core: coreStart, + storage: new Storage(localStorage), + data, + charts, + indexPatternFieldEditor, + uiActions, + }); + }) as Promise; }); } } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index c291c7ab3eac0..b1ff7b36b47a3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -17,6 +17,7 @@ import { getFieldByNameFactory } from './pure_helpers'; import { operationDefinitionMap, getErrorMessages } from './operations'; import { createMockedReferenceOperation } from './operations/mocks'; import { indexPatternFieldEditorPluginMock } from 'src/plugins/index_pattern_field_editor/public/mocks'; +import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; jest.mock('./loader'); jest.mock('../id_generator'); @@ -172,6 +173,7 @@ describe('IndexPattern Data Source', () => { data: dataPluginMock.createStartContract(), charts: chartPluginMock.createSetupContract(), indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), }); baseState = { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 81eb46e816715..8fb0994c42fb9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -45,7 +45,7 @@ import { import { isDraggedField, normalizeOperationDataType } from './utils'; import { LayerPanel } from './layerpanel'; import { IndexPatternColumn, getErrorMessages, IncompleteColumn } from './operations'; -import { IndexPatternPrivateState, IndexPatternPersistedState } from './types'; +import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/public'; @@ -53,6 +53,9 @@ import { mergeLayer } from './state_helpers'; import { Datasource, StateSetter } from '../types'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { deleteColumn, isReferenced } from './operations'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { GeoFieldWorkspacePanel } from '../editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel'; +import { DraggingIdentifier } from '../drag_drop'; export { OperationType, IndexPatternColumn, deleteColumn } from './operations'; @@ -78,12 +81,14 @@ export function getIndexPatternDatasource({ data, charts, indexPatternFieldEditor, + uiActions, }: { core: CoreStart; storage: IStorageWrapper; data: DataPublicPluginStart; charts: ChartsPluginSetup; indexPatternFieldEditor: IndexPatternFieldEditorStart; + uiActions: UiActionsStart; }) { const uiSettings = core.uiSettings; const onIndexPatternLoadError = (err: Error) => @@ -197,6 +202,7 @@ export function getIndexPatternDatasource({ indexPatternFieldEditor={indexPatternFieldEditor} {...props} core={core} + uiActions={uiActions} /> , domElement @@ -320,6 +326,34 @@ export function getIndexPatternDatasource({ getDropProps, onDrop, + getCustomWorkspaceRenderer: (state: IndexPatternPrivateState, dragging: DraggingIdentifier) => { + if (dragging.field === undefined || dragging.indexPatternId === undefined) { + return undefined; + } + + const draggedField = dragging as DraggingIdentifier & { + field: IndexPatternField; + indexPatternId: string; + }; + const geoFieldType = + draggedField.field.esTypes && + draggedField.field.esTypes.find((esType) => { + return ['geo_point', 'geo_shape'].includes(esType); + }); + return geoFieldType + ? () => { + return ( + + ); + } + : undefined; + }, + // Reset the temporary invalid state when closing the editor, but don't // update the state if it's not needed updateStateOnCloseDimension: ({ state, layerId, columnId }) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/visualize_geo_field_button.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/visualize_geo_field_button.tsx new file mode 100644 index 0000000000000..3101fb12b933a --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/visualize_geo_field_button.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, { MouseEvent, useEffect, useState } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + visualizeGeoFieldTrigger, + VISUALIZE_GEO_FIELD_TRIGGER, + UiActionsStart, +} from '../../../../../src/plugins/ui_actions/public'; + +interface Props { + indexPatternId: string; + fieldName: string; + uiActions: UiActionsStart; +} + +export function VisualizeGeoFieldButton(props: Props) { + const [href, setHref] = useState(undefined); + + async function loadHref() { + const actions = await props.uiActions.getTriggerCompatibleActions(VISUALIZE_GEO_FIELD_TRIGGER, { + indexPatternId: props.indexPatternId, + fieldName: props.fieldName, + }); + const triggerOptions = { + indexPatternId: props.indexPatternId, + fieldName: props.fieldName, + trigger: visualizeGeoFieldTrigger, + }; + const loadedHref = actions.length ? await actions[0].getHref?.(triggerOptions) : undefined; + setHref(loadedHref); + } + + useEffect( + () => { + loadHref(); + }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + [] + ); + + function onClick(event: MouseEvent) { + event.preventDefault(); + props.uiActions.getTrigger(VISUALIZE_GEO_FIELD_TRIGGER).exec({ + indexPatternId: props.indexPatternId, + fieldName: props.fieldName, + }); + } + + return ( + <> + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + + + + ); +} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 94b4433a82551..51d679e7c40e5 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -33,6 +33,7 @@ import type { LensResizeActionData, LensToggleActionData, } from './datatable_visualization/components/types'; +import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; export type ErrorCallback = (e: { message: string }) => void; @@ -213,6 +214,10 @@ export interface Datasource { } ) => { dropTypes: DropType[]; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; + getCustomWorkspaceRenderer?: ( + state: T, + dragging: DraggingIdentifier + ) => undefined | (() => JSX.Element); updateStateOnCloseDimension?: (props: { layerId: string; columnId: string; @@ -267,6 +272,7 @@ export interface DatasourceDataPanelProps { filters: Filter[]; dropOntoWorkspace: (field: DragDropIdentifier) => void; hasSuggestionForField: (field: DragDropIdentifier) => boolean; + uiActions: UiActionsStart; } interface SharedDimensionProps { @@ -343,7 +349,7 @@ export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProp dropType: DropType; }; -export type FieldOnlyDataType = 'document' | 'ip' | 'histogram'; +export type FieldOnlyDataType = 'document' | 'ip' | 'histogram' | 'geo_point' | 'geo_shape'; export type DataType = 'string' | 'number' | 'date' | 'boolean' | FieldOnlyDataType; // An operation represents a column in a table, not any information diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index ab95141431541..2d8cfee2185fa 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -42,3 +42,10 @@ export const desanitizeFilterContext = ( } return result; }; + +export function getVisualizeGeoFieldMessage(fieldType: string) { + return i18n.translate('xpack.lens.visualizeGeoFieldMessage', { + defaultMessage: `Lens cannot visualize {fieldType} fields`, + values: { fieldType }, + }); +} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 1616a4c141476..d5120cfae973c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -27,6 +27,8 @@ const columnSortOrder = { boolean: 4, number: 5, histogram: 6, + geo_point: 7, + geo_shape: 8, }; /** diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index 90f4825b97d43..325ed48113966 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -13,7 +13,7 @@ import { EntryMatch, EntryNested, OsTypeArray, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; export const DATE_NOW = '2020-04-20T15:25:31.830Z'; export const OLD_DATE_RELATIVE_TO_DATE_NOW = '2020-04-19T15:25:31.830Z'; diff --git a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts index fa073b3b4cfb6..ae0cfbfbfc425 100644 --- a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts +++ b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryMatchAny } from '@kbn/securitysolution-io-ts-utils'; +import { EntryMatchAny } from '@kbn/securitysolution-io-ts-list-types'; import { getEntryMatchExcludeMock, getEntryMatchMock } from '../schemas/types/entry_match.mock'; import { diff --git a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts index 0fa069ba51013..eda81f91cd983 100644 --- a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts +++ b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts @@ -15,7 +15,7 @@ import { entriesMatch, entriesMatchAny, entriesNested, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { Filter } from '../../../../../src/plugins/data/common'; import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../schemas'; diff --git a/x-pack/plugins/lists/common/exceptions/utils.ts b/x-pack/plugins/lists/common/exceptions/utils.ts index 689687e44256a..f5881c1d3cbf4 100644 --- a/x-pack/plugins/lists/common/exceptions/utils.ts +++ b/x-pack/plugins/lists/common/exceptions/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts index 2b007f01b56eb..c83691ead2ee6 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts @@ -11,15 +11,13 @@ import { ExceptionListTypeEnum, ListOperatorEnum as OperatorEnum, Type, - exactCheck, exceptionListType, - foldLeftRight, - getPaths, listOperator as operator, osType, osTypeArrayOrUndefined, type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('Common schemas', () => { describe('operator', () => { diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index eb84ee07981f3..612b7ea559e4a 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -8,18 +8,20 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; -import { DefaultNamespace, NonEmptyString } from '@kbn/securitysolution-io-ts-utils'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { DefaultNamespace } from '@kbn/securitysolution-io-ts-list-types'; /** * @deprecated Directly use the type from the package and not from here */ export { + DefaultNamespace, Type, OsType, OsTypeArray, listOperator as operator, NonEmptyEntriesArray, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; export const list_id = NonEmptyString; export type ListId = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts index 30f3acc8a164a..e6287a87c86ef 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts @@ -7,12 +7,8 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { - CommentsArray, - exactCheck, - foldLeftRight, - getPaths, -} from '@kbn/securitysolution-io-ts-utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { CommentsArray } from '@kbn/securitysolution-io-ts-list-types'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts index af58c61dbaf9f..322e31aacd040 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts @@ -9,7 +9,6 @@ import * as t from 'io-ts'; import { CreateCommentsArray, DefaultCreateCommentsArray, - DefaultUuid, EntriesArray, OsTypeArray, Tags, @@ -20,7 +19,8 @@ import { nonEmptyEndpointEntriesArray, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { DefaultUuid } from '@kbn/securitysolution-io-ts-types'; import { ItemId } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts index 1bb58d6195e7c..7e8d16663cf5d 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts @@ -7,12 +7,8 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { - CommentsArray, - exactCheck, - foldLeftRight, - getPaths, -} from '@kbn/securitysolution-io-ts-utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { CommentsArray } from '@kbn/securitysolution-io-ts-list-types'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index da5630ef3f002..d37c7f7aa67b2 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -9,7 +9,6 @@ import * as t from 'io-ts'; import { CreateCommentsArray, DefaultCreateCommentsArray, - DefaultUuid, EntriesArray, NamespaceType, OsTypeArray, @@ -21,7 +20,8 @@ import { nonEmptyEntriesArray, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { DefaultUuid } from '@kbn/securitysolution-io-ts-types'; import { ItemId, list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index 42955ddbd7017..91b3a98bdd5ac 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -7,7 +7,6 @@ import * as t from 'io-ts'; import { - DefaultUuid, DefaultVersionNumber, DefaultVersionNumberDecoded, NamespaceType, @@ -19,7 +18,8 @@ import { name, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { DefaultUuid } from '@kbn/securitysolution-io-ts-types'; import { ListId, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts index 867b441960a2c..d11bd03ced916 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id, meta } from '@kbn/securitysolution-io-ts-utils'; +import { id, meta } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts index 8ac36cc3ad28e..5fa9da0cdc597 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -14,7 +14,7 @@ import { meta, name, type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { deserializer, serializer } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts index b8ff0834e8fb8..0b714885437a8 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { item_id } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts index cc188bf52d75c..5c6fc9c158b3b 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-list-types'; import { item_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts index b816c08beb363..2d1d00a6759cf 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts index 5b4aa63d2d090..9cb46b3e36f45 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, valueOrUndefined } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts index 003dfdc6bd466..0d6bbc73a2571 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts @@ -6,7 +6,8 @@ */ import * as t from 'io-ts'; -import { DefaultStringBooleanFalse, id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; +import { DefaultStringBooleanFalse } from '@kbn/securitysolution-io-ts-types'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts index d3c18f0d1c485..47bb1b70ad8b7 100644 --- a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, namespace_type } from '../common/schemas'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts index 13f45a070b2b7..06b28ea6cbb4e 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-utils'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; import { filter, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts index 4abceb4b3592d..d92bfbec02f5a 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts @@ -7,13 +7,15 @@ import * as t from 'io-ts'; import { - DefaultNamespaceArray, - DefaultNamespaceArrayTypeDecoded, EmptyStringArray, EmptyStringArrayDecoded, NonEmptyStringArray, StringToPositiveNumber, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-types'; +import { + DefaultNamespaceArray, + DefaultNamespaceArrayTypeDecoded, +} from '@kbn/securitysolution-io-ts-list-types'; import { sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts index ea5b5c5aafdb6..6cf31c56ea599 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts @@ -6,11 +6,8 @@ */ import * as t from 'io-ts'; -import { - DefaultNamespaceArray, - NamespaceTypeArray, - StringToPositiveNumber, -} from '@kbn/securitysolution-io-ts-utils'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; +import { DefaultNamespaceArray, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { filter, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts index 6adf53d0eda86..e0d072780bbf8 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-utils'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; import { cursor, filter, list_id, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts index bf6a68d97a58e..4d929d581370c 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-utils'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; import { cursor, filter, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts index 85644ff556443..cef803ffa5e45 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { type } from '@kbn/securitysolution-io-ts-utils'; +import { type } from '@kbn/securitysolution-io-ts-list-types'; import { RequiredKeepUndefined } from '../../types'; import { deserializer, list_id, serializer } from '../common/schemas'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts index edea4f161f248..2989919421a3c 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id, meta } from '@kbn/securitysolution-io-ts-utils'; +import { id, meta } from '@kbn/securitysolution-io-ts-list-types'; import { _version, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts index 144bf9c0f28a0..eea4ba9fc87d7 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { description, id, meta, name } from '@kbn/securitysolution-io-ts-utils'; +import { description, id, meta, name } from '@kbn/securitysolution-io-ts-list-types'; import { _version, version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts index 116c70012c17e..3f221b473f432 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { item_id } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts index a0bd46b30d2f6..9094296e56196 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-list-types'; import { item_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts index fc8a6ee43a5a2..9a361e04900ed 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts index 450719f42ad4a..0bfa99ee078a1 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts index e07e2de1a4b80..5d850b19c4d11 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; export const readListSchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts index d9e602419d61d..011ff24b7fa22 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts @@ -20,7 +20,7 @@ import { nonEmptyEntriesArray, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts index f3b87c5ff5925..1c751dd3a8c83 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts @@ -21,7 +21,7 @@ import { nonEmptyEntriesArray, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _version, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts index c8b354eff4d9e..c58c1c253a8c4 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts @@ -17,7 +17,7 @@ import { name, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _version, list_id, namespace_type, version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts index 84916f15a59f6..f24902a12d3b7 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id, meta } from '@kbn/securitysolution-io-ts-utils'; +import { id, meta } from '@kbn/securitysolution-io-ts-list-types'; import { _version, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts index 6f520d399d577..230853e69fae4 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { description, id, meta, name, version } from '@kbn/securitysolution-io-ts-utils'; +import { description, id, meta, name, version } from '@kbn/securitysolution-io-ts-list-types'; import { _version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts index 769cfb3548ced..0b6f8a7640529 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts @@ -20,7 +20,7 @@ import { tags, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _versionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts index 880c2d4f89e4f..7bfc2af9863e2 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts @@ -18,7 +18,7 @@ import { tags, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _versionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts index 1f105afac8b44..3f11718bc42e6 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts @@ -14,7 +14,7 @@ import { type, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _versionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.ts index 58abe94772ff6..21504d64fdeaa 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.ts @@ -16,7 +16,7 @@ import { type, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _versionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/types/comment.mock.ts b/x-pack/plugins/lists/common/schemas/types/comment.mock.ts index 75b4b6a431ac3..5963cb4947a85 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Comment, CommentsArray } from '@kbn/securitysolution-io-ts-utils'; +import { Comment, CommentsArray } from '@kbn/securitysolution-io-ts-list-types'; import { DATE_NOW, ID, USER } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts index 2d8dd7b462258..868c43fe5d6da 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CreateComment, CreateCommentsArray } from '@kbn/securitysolution-io-ts-utils'; +import { CreateComment, CreateCommentsArray } from '@kbn/securitysolution-io-ts-list-types'; export const getCreateCommentsMock = (): CreateComment => ({ comment: 'some comments', diff --git a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts index ee43a0b26ad54..caa62c55c93bb 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts index 3e26d261f44ca..6165184d2a404 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryExists } from '@kbn/securitysolution-io-ts-utils'; +import { EntryExists } from '@kbn/securitysolution-io-ts-list-types'; import { EXISTS, FIELD, OPERATOR } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts index 7eadfcdf3454c..1cdc86d95ed88 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryList } from '@kbn/securitysolution-io-ts-utils'; +import { EntryList } from '@kbn/securitysolution-io-ts-list-types'; import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts index bc0eb3b5c4f85..efcd1e0877d1b 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryMatch } from '@kbn/securitysolution-io-ts-utils'; +import { EntryMatch } from '@kbn/securitysolution-io-ts-list-types'; import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts index 74c3abbaa5881..60613fc72baba 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryMatchAny } from '@kbn/securitysolution-io-ts-utils'; +import { EntryMatchAny } from '@kbn/securitysolution-io-ts-list-types'; import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts index 320664bd2f833..17e0cbd25901c 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryMatchWildcard } from '@kbn/securitysolution-io-ts-utils'; +import { EntryMatchWildcard } from '@kbn/securitysolution-io-ts-list-types'; import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts index f1d0a2bc76926..2497c3d4c3ce2 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryNested } from '@kbn/securitysolution-io-ts-utils'; +import { EntryNested } from '@kbn/securitysolution-io-ts-list-types'; import { NESTED, NESTED_FIELD } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts index dea0f9a08fc4c..783b595850bc5 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { UpdateComment, UpdateCommentsArray } from '@kbn/securitysolution-io-ts-utils'; +import { UpdateComment, UpdateCommentsArray } from '@kbn/securitysolution-io-ts-list-types'; import { ID } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 38eb5aeee8cd2..bc9d0ca8d7b94 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -42,7 +42,7 @@ export { osTypeArray, OsTypeArray, Type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; export { ListSchema, diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index 28d7469d18910..0ece28d409bd5 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import styled from 'styled-components'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx index 5f094a64c3660..94c3bff8f4cf9 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { AutocompleteStart } from 'src/plugins/data/public'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListType } from '../../../../common'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index a698feb93722c..4ec152e155e39 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { addIdToItem } from '@kbn/securitysolution-utils'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { @@ -444,3 +444,6 @@ export const ExceptionBuilderComponent = ({ }; ExceptionBuilderComponent.displayName = 'ExceptionBuilder'; + +// eslint-disable-next-line import/no-default-export +export default ExceptionBuilderComponent; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts index 6d3bdd09c93ea..18d607d6807fc 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts @@ -7,7 +7,8 @@ import uuid from 'uuid'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; -import { OsTypeArray, validate } from '@kbn/securitysolution-io-ts-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx index 833034aa0a542..551889e4a821d 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx @@ -5,6 +5,32 @@ * 2.0. */ -export { BuilderEntryItem } from './entry_renderer'; -export { BuilderExceptionListItemComponent } from './exception_item_renderer'; -export { ExceptionBuilderComponent, OnChangeProps } from './exception_items_renderer'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import React, { Suspense, lazy } from 'react'; + +// Note: Only use import type/export type here to avoid pulling anything non-lazy into the main plugin and increasing the plugin size +import type { ExceptionBuilderProps } from './exception_items_renderer'; +export type { OnChangeProps } from './exception_items_renderer'; + +interface ExtraProps { + dataTestSubj: string; + idAria: string; +} + +/** + * This lazy load allows the exception builder to pull everything out into a plugin chunk. + * You want to be careful of not directly importing/exporting things from exception_items_renderer + * unless you use a import type, and/or a export type to ensure full type erasure + */ +const ExceptionBuilderComponentLazy = lazy(() => import('./exception_items_renderer')); +export const getExceptionBuilderComponentLazy = ( + props: ExceptionBuilderProps & ExtraProps +): JSX.Element => ( + }> + + +); diff --git a/x-pack/plugins/lists/public/exceptions/transforms.test.ts b/x-pack/plugins/lists/public/exceptions/transforms.test.ts index c5c43b16d6428..b2a1efc1d2c1d 100644 --- a/x-pack/plugins/lists/public/exceptions/transforms.test.ts +++ b/x-pack/plugins/lists/public/exceptions/transforms.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Entry, EntryMatch, EntryNested } from '@kbn/securitysolution-io-ts-utils'; +import { Entry, EntryMatch, EntryNested } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema } from '../../common/schemas/response/exception_list_item_schema'; import { UpdateExceptionListItemSchema } from '../../common/schemas/request/update_exception_list_item_schema'; diff --git a/x-pack/plugins/lists/public/exceptions/transforms.ts b/x-pack/plugins/lists/public/exceptions/transforms.ts index 468dfc00ca852..50ce1b6e33a4b 100644 --- a/x-pack/plugins/lists/public/exceptions/transforms.ts +++ b/x-pack/plugins/lists/public/exceptions/transforms.ts @@ -8,7 +8,7 @@ import { flow } from 'fp-ts/lib/function'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; -import { +import type { CreateExceptionListItemSchema, EntriesArray, Entry, diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index a2842d81a7292..0cad700b2b598 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ExceptionListType, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { ExceptionListType, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { CreateExceptionListItemSchema, diff --git a/x-pack/plugins/lists/public/exceptions/utils.ts b/x-pack/plugins/lists/public/exceptions/utils.ts index 6324fdf1df420..c840a25b2a103 100644 --- a/x-pack/plugins/lists/public/exceptions/utils.ts +++ b/x-pack/plugins/lists/public/exceptions/utils.ts @@ -6,7 +6,7 @@ */ import { get } from 'lodash/fp'; -import { NamespaceType, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; import { diff --git a/x-pack/plugins/lists/public/lists/types.ts b/x-pack/plugins/lists/public/lists/types.ts index 6708620439803..ad82a63163ce3 100644 --- a/x-pack/plugins/lists/public/lists/types.ts +++ b/x-pack/plugins/lists/public/lists/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { HttpStart } from '../../../../../src/core/public'; diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts index 76ff8b1728922..9c42df6abd7bd 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -6,17 +6,17 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { CreateEndpointListItemSchemaDecoded, createEndpointListItemSchema, exceptionListItemSchema, } from '../../common/schemas'; -import { getExceptionListClient } from './utils/get_exception_list_client'; +import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; import { validateExceptionListSize } from './validate'; export const createEndpointListItemRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts index 23f098f7e9457..599870c226564 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -6,12 +6,13 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { ENDPOINT_LIST_URL } from '../../common/constants'; -import { buildSiemResponse, transformError } from '../siem_server_deps'; import { createEndpointListSchema } from '../../common/schemas'; +import { buildSiemResponse } from './utils'; import { getExceptionListClient } from './utils/get_exception_list_client'; /** diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index 4bcb41c666f56..81260584e8a50 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -6,16 +6,17 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { CreateExceptionListItemSchemaDecoded, createExceptionListItemSchema, exceptionListItemSchema, } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; import { getExceptionListClient } from './utils/get_exception_list_client'; import { endpointDisallowedFields } from './endpoint_disallowed_fields'; import { validateEndpointExceptionItemEntries, validateExceptionListSize } from './validate'; diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts index 12c887a16c318..1a35bdb008662 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts @@ -6,16 +6,17 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { CreateExceptionListSchemaDecoded, createExceptionListSchema, exceptionListSchema, } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; import { getExceptionListClient } from './utils/get_exception_list_client'; export const createExceptionListRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/create_list_index_route.ts index 12fe586a07cc0..3b0d34b8952a1 100644 --- a/x-pack/plugins/lists/server/routes/create_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_index_route.ts @@ -6,12 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; -import { buildSiemResponse, transformError } from '../siem_server_deps'; import { LIST_INDEX } from '../../common/constants'; import { acknowledgeSchema } from '../../common/schemas'; +import { buildSiemResponse } from './utils'; + import { getListClient } from '.'; export const createListIndexRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_list_item_route.ts index 2e3c944af0df8..4df121af4c1ba 100644 --- a/x-pack/plugins/lists/server/routes/create_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_item_route.ts @@ -6,12 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { createListItemSchema, listItemSchema } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getListClient } from '.'; export const createListItemRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/create_list_route.ts b/x-pack/plugins/lists/server/routes/create_list_route.ts index 4346d519c9003..dabbd690bba21 100644 --- a/x-pack/plugins/lists/server/routes/create_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_route.ts @@ -6,12 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { CreateListSchemaDecoded, createListSchema, listSchema } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getListClient } from '.'; export const createListRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts index 195384356f40b..59d91f6234176 100644 --- a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts @@ -6,17 +6,22 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { DeleteEndpointListItemSchemaDecoded, deleteEndpointListItemSchema, exceptionListItemSchema, } from '../../common/schemas'; -import { getErrorMessageExceptionListItem, getExceptionListClient } from './utils'; +import { + buildRouteValidation, + buildSiemResponse, + getErrorMessageExceptionListItem, + getExceptionListClient, +} from './utils'; export const deleteEndpointListItemRoute = (router: ListsPluginRouter): void => { router.delete( diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts index ddcd1cf9b7180..ce4f91ffc671a 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts @@ -6,17 +6,22 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { DeleteExceptionListItemSchemaDecoded, deleteExceptionListItemSchema, exceptionListItemSchema, } from '../../common/schemas'; -import { getErrorMessageExceptionListItem, getExceptionListClient } from './utils'; +import { + buildRouteValidation, + buildSiemResponse, + getErrorMessageExceptionListItem, + getExceptionListClient, +} from './utils'; export const deleteExceptionListItemRoute = (router: ListsPluginRouter): void => { router.delete( diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts index f11deef5cb0c8..eeeb5fb44c16a 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts @@ -6,17 +6,22 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { DeleteExceptionListSchemaDecoded, deleteExceptionListSchema, exceptionListSchema, } from '../../common/schemas'; -import { getErrorMessageExceptionList, getExceptionListClient } from './utils'; +import { + buildRouteValidation, + buildSiemResponse, + getErrorMessageExceptionList, + getExceptionListClient, +} from './utils'; export const deleteExceptionListRoute = (router: ListsPluginRouter): void => { router.delete( diff --git a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts index efad16c37a2dc..22c56a21df419 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts @@ -6,12 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_INDEX } from '../../common/constants'; -import { buildSiemResponse, transformError } from '../siem_server_deps'; import { acknowledgeSchema } from '../../common/schemas'; +import { buildSiemResponse } from './utils'; + import { getListClient } from '.'; /** diff --git a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts index a07035fc50d9c..197590ecb142c 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts @@ -6,12 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { deleteListItemSchema, listItemArraySchema, listItemSchema } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getListClient } from '.'; export const deleteListItemRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts index 65faa54b20cc7..78235584bc0cd 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { EntriesArray, validate } from '@kbn/securitysolution-io-ts-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { ExceptionListItemSchema, FoundExceptionListSchema, @@ -21,6 +22,8 @@ import { getSavedObjectType } from '../services/exception_lists/utils'; import { ExceptionListClient } from '../services/exception_lists/exception_list_client'; import { escapeQuotes } from '../services/utils/escape_query'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getExceptionListClient, getListClient } from '.'; export const deleteListRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/export_exception_list_route.ts b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts index 30f7a16e6100d..3d82cbac47a88 100644 --- a/x-pack/plugins/lists/server/routes/export_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts @@ -5,12 +5,13 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; + import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { exportExceptionListQuerySchema } from '../../common/schemas'; -import { getExceptionListClient } from './utils'; +import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; export const exportExceptionListRoute = (router: ListsPluginRouter): void => { router.get( diff --git a/x-pack/plugins/lists/server/routes/export_list_item_route.ts b/x-pack/plugins/lists/server/routes/export_list_item_route.ts index e07b78a23c7e0..13a2aa9beea05 100644 --- a/x-pack/plugins/lists/server/routes/export_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/export_list_item_route.ts @@ -7,11 +7,14 @@ import { Stream } from 'stream'; +import { transformError } from '@kbn/securitysolution-es-utils'; + import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { exportListItemQuerySchema } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getListClient } from '.'; export const exportListItemRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts index ee5245982dc0b..cbf3c320c407a 100644 --- a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts @@ -6,17 +6,17 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { FindEndpointListItemSchemaDecoded, findEndpointListItemSchema, foundExceptionListItemSchema, } from '../../common/schemas'; -import { getExceptionListClient } from './utils'; +import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; export const findEndpointListItemRoute = (router: ListsPluginRouter): void => { router.get( diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index 82988a7cbeb76..45ce1dbb87fba 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -6,17 +6,17 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { FindExceptionListItemSchemaDecoded, findExceptionListItemSchema, foundExceptionListItemSchema, } from '../../common/schemas'; -import { getExceptionListClient } from './utils'; +import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; export const findExceptionListItemRoute = (router: ListsPluginRouter): void => { router.get( diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts index 4b188b4dca4e2..0181bfed5b857 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts @@ -6,17 +6,17 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { FindExceptionListSchemaDecoded, findExceptionListSchema, foundExceptionListSchema, } from '../../common/schemas'; -import { getExceptionListClient } from './utils'; +import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; export const findExceptionListRoute = (router: ListsPluginRouter): void => { router.get( diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts index a904d7f84733d..c64dfd561e0e3 100644 --- a/x-pack/plugins/lists/server/routes/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts @@ -6,10 +6,10 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { FindListItemSchemaDecoded, findListItemSchema, @@ -17,7 +17,7 @@ import { } from '../../common/schemas'; import { decodeCursor } from '../services/utils'; -import { getListClient } from './utils'; +import { buildRouteValidation, buildSiemResponse, getListClient } from './utils'; export const findListItemRoute = (router: ListsPluginRouter): void => { router.get( diff --git a/x-pack/plugins/lists/server/routes/find_list_route.ts b/x-pack/plugins/lists/server/routes/find_list_route.ts index c5f1b58c1e957..19c20515ef5f2 100644 --- a/x-pack/plugins/lists/server/routes/find_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_route.ts @@ -6,14 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { findListSchema, foundListSchema } from '../../common/schemas'; import { decodeCursor } from '../services/utils'; -import { getListClient } from './utils'; +import { buildRouteValidation, buildSiemResponse, getListClient } from './utils'; export const findListRoute = (router: ListsPluginRouter): void => { router.get( diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts index 070764b0e1e77..77d9623f40a23 100644 --- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -7,13 +7,14 @@ import { schema } from '@kbn/config-schema'; import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { importListItemQuerySchema, listSchema } from '../../common/schemas'; import { ConfigType } from '../config'; +import { buildRouteValidation, buildSiemResponse } from './utils'; import { createStreamFromBuffer } from './utils/create_stream_from_buffer'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts index 53fd7c65c8ab8..ce4ff71a1d886 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -6,12 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { listItemSchema, patchListItemSchema } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getListClient } from '.'; export const patchListItemRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts index f139fb72c3066..3f2427b30f2be 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -6,12 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { listSchema, patchListSchema } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getListClient } from '.'; export const patchListRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts index c78a4a435e5b4..72cfe38090cd8 100644 --- a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts @@ -6,17 +6,22 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { ReadEndpointListItemSchemaDecoded, exceptionListItemSchema, readEndpointListItemSchema, } from '../../common/schemas'; -import { getErrorMessageExceptionListItem, getExceptionListClient } from './utils'; +import { + buildRouteValidation, + buildSiemResponse, + getErrorMessageExceptionListItem, + getExceptionListClient, +} from './utils'; export const readEndpointListItemRoute = (router: ListsPluginRouter): void => { router.get( diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts index fd92543fa85a7..3563645f554bb 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts @@ -6,17 +6,22 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { ReadExceptionListItemSchemaDecoded, exceptionListItemSchema, readExceptionListItemSchema, } from '../../common/schemas'; -import { getErrorMessageExceptionListItem, getExceptionListClient } from './utils'; +import { + buildRouteValidation, + buildSiemResponse, + getErrorMessageExceptionListItem, + getExceptionListClient, +} from './utils'; export const readExceptionListItemRoute = (router: ListsPluginRouter): void => { router.get( diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts index 3d4e831f4a2da..f82c397e67d2b 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts @@ -6,17 +6,22 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { ReadExceptionListSchemaDecoded, exceptionListSchema, readExceptionListSchema, } from '../../common/schemas'; -import { getErrorMessageExceptionList, getExceptionListClient } from './utils'; +import { + buildRouteValidation, + buildSiemResponse, + getErrorMessageExceptionList, + getExceptionListClient, +} from './utils'; export const readExceptionListRoute = (router: ListsPluginRouter): void => { router.get( diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts index 467348669bc0b..619600f3a7ee1 100644 --- a/x-pack/plugins/lists/server/routes/read_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts @@ -6,12 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_INDEX } from '../../common/constants'; -import { buildSiemResponse, transformError } from '../siem_server_deps'; import { listItemIndexExistSchema } from '../../common/schemas'; +import { buildSiemResponse } from './utils'; + import { getListClient } from '.'; export const readListIndexRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_list_item_route.ts index fd216197f91b5..2355a393d4a77 100644 --- a/x-pack/plugins/lists/server/routes/read_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_item_route.ts @@ -6,12 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { listItemArraySchema, listItemSchema, readListItemSchema } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getListClient } from '.'; export const readListItemRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/read_list_route.ts b/x-pack/plugins/lists/server/routes/read_list_route.ts index 56acb1e043bd5..e66774998d554 100644 --- a/x-pack/plugins/lists/server/routes/read_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_route.ts @@ -6,12 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { listSchema, readListSchema } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getListClient } from '.'; export const readListRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/read_privileges_route.ts b/x-pack/plugins/lists/server/routes/read_privileges_route.ts index 798f8631668a9..8c7faa7f7eb9d 100644 --- a/x-pack/plugins/lists/server/routes/read_privileges_route.ts +++ b/x-pack/plugins/lists/server/routes/read_privileges_route.ts @@ -5,13 +5,13 @@ * 2.0. */ +import { readPrivileges, transformError } from '@kbn/securitysolution-es-utils'; import { merge } from 'lodash/fp'; import type { ListsPluginRouter } from '../types'; import { LIST_PRIVILEGES_URL } from '../../common/constants'; -import { buildSiemResponse, readPrivileges, transformError } from '../siem_server_deps'; -import { getListClient } from './utils'; +import { buildSiemResponse, getListClient } from './utils'; export const readPrivilegesRoute = (router: ListsPluginRouter): void => { router.get( diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts index 9f445f4e3c114..9468fd2e8c226 100644 --- a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -6,16 +6,18 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { UpdateEndpointListItemSchemaDecoded, exceptionListItemSchema, updateEndpointListItemSchema, } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getExceptionListClient } from '.'; export const updateEndpointListItemRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index 6a87af6c666bb..6fbb1b7de80af 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -6,10 +6,10 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { UpdateExceptionListItemSchemaDecoded, exceptionListItemSchema, @@ -17,6 +17,8 @@ import { } from '../../common/schemas'; import { updateExceptionListItemValidate } from '../../common/schemas/request/update_exception_list_item_validation'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getExceptionListClient } from '.'; export const updateExceptionListItemRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index a6b99579d87ad..cf670b28cee56 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -6,17 +6,22 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { EXCEPTION_LIST_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { UpdateExceptionListSchemaDecoded, exceptionListSchema, updateExceptionListSchema, } from '../../common/schemas'; -import { getErrorMessageExceptionList, getExceptionListClient } from './utils'; +import { + buildRouteValidation, + buildSiemResponse, + getErrorMessageExceptionList, + getExceptionListClient, +} from './utils'; export const updateExceptionListRoute = (router: ListsPluginRouter): void => { router.put( diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts index e2905c1a00a11..f806b3f5d09d7 100644 --- a/x-pack/plugins/lists/server/routes/update_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.ts @@ -6,12 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_ITEM_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { listItemSchema, updateListItemSchema } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getListClient } from '.'; export const updateListItemRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts index d69c110aa129b..25457d7cdb333 100644 --- a/x-pack/plugins/lists/server/routes/update_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -6,12 +6,14 @@ */ import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; import { LIST_URL } from '../../common/constants'; -import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { listSchema, updateListSchema } from '../../common/schemas'; +import { buildRouteValidation, buildSiemResponse } from './utils'; + import { getListClient } from '.'; export const updateListRoute = (router: ListsPluginRouter): void => { diff --git a/x-pack/plugins/lists/server/routes/utils/build_siem_response.ts b/x-pack/plugins/lists/server/routes/utils/build_siem_response.ts new file mode 100644 index 0000000000000..fe76de24aa7b9 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/utils/build_siem_response.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CustomHttpResponseOptions, KibanaResponseFactory } from 'src/core/server'; + +/** + * Copied from x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts + * We cannot put this in kbn package just yet as the types from 'src/core/server' aren't a kbn package yet and this would pull in a lot of copied things. + * TODO: Once more types are moved into kbn package we can move this into a kbn package. + */ +const statusToErrorMessage = ( + statusCode: number +): + | 'Bad Request' + | 'Unauthorized' + | 'Forbidden' + | 'Not Found' + | 'Conflict' + | 'Internal Error' + | '(unknown error)' => { + switch (statusCode) { + case 400: + return 'Bad Request'; + case 401: + return 'Unauthorized'; + case 403: + return 'Forbidden'; + case 404: + return 'Not Found'; + case 409: + return 'Conflict'; + case 500: + return 'Internal Error'; + default: + return '(unknown error)'; + } +}; + +/** + * Copied from x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts + * We cannot put this in kbn package just yet as the types from 'src/core/server' aren't a kbn package yet and this would pull in a lot of copied things. + * TODO: Once more types are moved into kbn package we can move this into a kbn package. + */ +export class SiemResponseFactory { + constructor(private response: KibanaResponseFactory) {} + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + error({ statusCode, body, headers }: CustomHttpResponseOptions) { + // KibanaResponse is not exported so we cannot use a return type here and that is why the linter is turned off above + const contentType: CustomHttpResponseOptions['headers'] = { + 'content-type': 'application/json', + }; + const defaultedHeaders: CustomHttpResponseOptions['headers'] = { + ...contentType, + ...(headers ?? {}), + }; + + return this.response.custom({ + body: Buffer.from( + JSON.stringify({ + message: body ?? statusToErrorMessage(statusCode), + status_code: statusCode, + }) + ), + headers: defaultedHeaders, + statusCode, + }); + } +} + +/** + * Copied from x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts + * We cannot put this in kbn package just yet as the types from 'src/core/server' aren't a kbn package yet and this would pull in a lot of copied things. + * TODO: Once more types are moved into kbn package we can move this into a kbn package. + */ +export const buildSiemResponse = (response: KibanaResponseFactory): SiemResponseFactory => + new SiemResponseFactory(response); diff --git a/x-pack/plugins/lists/server/routes/utils/index.ts b/x-pack/plugins/lists/server/routes/utils/index.ts index 0149770e695b9..f035ae5dbfe9b 100644 --- a/x-pack/plugins/lists/server/routes/utils/index.ts +++ b/x-pack/plugins/lists/server/routes/utils/index.ts @@ -9,3 +9,5 @@ export * from './get_error_message_exception_list_item'; export * from './get_error_message_exception_list'; export * from './get_list_client'; export * from './get_exception_list_client'; +export * from './route_validation'; +export * from './build_siem_response'; diff --git a/x-pack/plugins/lists/server/routes/utils/route_validation.test.ts b/x-pack/plugins/lists/server/routes/utils/route_validation.test.ts new file mode 100644 index 0000000000000..9e6064d192d40 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/utils/route_validation.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { RouteValidationResultFactory } from 'src/core/server'; + +import { buildRouteValidation } from './route_validation'; + +/** + * Copied from x-pack/plugins/security_solution/server/utils/build_validation/route_validation.test.ts + * TODO: Once we can move this into a kbn package because the types such as RouteValidationResultFactory are in packages, then please do. + */ +describe('Route Validation with ', () => { + describe('buildRouteValidation', () => { + const schema = rt.exact( + rt.type({ + ids: rt.array(rt.string), + }) + ); + type Schema = rt.TypeOf; + + /** + * If your schema is using exact all the way down then the validation will + * catch any additional keys that should not be present within the validation + * when the route_validation uses the exact check. + */ + const deepSchema = rt.exact( + rt.type({ + topLevel: rt.exact( + rt.type({ + secondLevel: rt.exact( + rt.type({ + thirdLevel: rt.string, + }) + ), + }) + ), + }) + ); + type DeepSchema = rt.TypeOf; + + const validationResult: RouteValidationResultFactory = { + badRequest: jest.fn().mockImplementation((e) => e), + ok: jest.fn().mockImplementation((validatedInput) => validatedInput), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('return validation error', () => { + const input: Omit & { id: string } = { id: 'someId' }; + const result = buildRouteValidation(schema)(input, validationResult); + + expect(result).toEqual('Invalid value "undefined" supplied to "ids"'); + }); + + test('return validated input', () => { + const input: Schema = { ids: ['someId'] }; + const result = buildRouteValidation(schema)(input, validationResult); + + expect(result).toEqual(input); + }); + + test('returns validation error if given extra keys on input for an array', () => { + const input: Schema & { somethingExtra: string } = { + ids: ['someId'], + somethingExtra: 'hello', + }; + const result = buildRouteValidation(schema)(input, validationResult); + expect(result).toEqual('invalid keys "somethingExtra"'); + }); + + test('return validation input for a deep 3rd level object', () => { + const input: DeepSchema = { topLevel: { secondLevel: { thirdLevel: 'hello' } } }; + const result = buildRouteValidation(deepSchema)(input, validationResult); + expect(result).toEqual(input); + }); + + test('return validation error for a deep 3rd level object that has an extra key value of "somethingElse"', () => { + const input: DeepSchema & { + topLevel: { secondLevel: { thirdLevel: string; somethingElse: string } }; + } = { + topLevel: { secondLevel: { somethingElse: 'extraKey', thirdLevel: 'hello' } }, + }; + const result = buildRouteValidation(deepSchema)(input, validationResult); + expect(result).toEqual('invalid keys "somethingElse"'); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/routes/utils/route_validation.ts b/x-pack/plugins/lists/server/routes/utils/route_validation.ts new file mode 100644 index 0000000000000..8e74760d6d15f --- /dev/null +++ b/x-pack/plugins/lists/server/routes/utils/route_validation.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.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 { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils'; + +import { + RouteValidationError, + RouteValidationFunction, + RouteValidationResultFactory, +} from '../../../../../../src/core/server'; + +type RequestValidationResult = + | { + value: T; + error?: undefined; + } + | { + value?: undefined; + error: RouteValidationError; + }; + +/** + * Copied from x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts + * This really should be in @kbn/securitysolution-io-ts-utils rather than copied yet again, however, this has types + * from a lot of places such as RouteValidationResultFactory from core/server which in turn can pull in @kbn/schema + * which cannot work on the front end and @kbn/securitysolution-io-ts-utils works on both front and backend. + * + * TODO: Figure out a way to move this function into a package rather than copying it/forking it within plugins + */ +export const buildRouteValidation = >( + schema: T +): RouteValidationFunction => ( + inputValue: unknown, + validationResult: RouteValidationResultFactory +): RequestValidationResult => + pipe( + schema.decode(inputValue), + (decoded) => exactCheck(inputValue, decoded), + fold>( + (errors: rt.Errors) => validationResult.badRequest(formatErrors(errors).join()), + (validatedInput: A) => validationResult.ok(validatedInput) + ) + ); diff --git a/x-pack/plugins/lists/server/routes/validate.ts b/x-pack/plugins/lists/server/routes/validate.ts index 005d9e85f4853..2577770cf32ef 100644 --- a/x-pack/plugins/lists/server/routes/validate.ts +++ b/x-pack/plugins/lists/server/routes/validate.ts @@ -8,14 +8,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { fold } from 'fp-ts/lib/Either'; +import { exactCheck, formatErrors, validate } from '@kbn/securitysolution-io-ts-utils'; import { NamespaceType, NonEmptyEntriesArray, - exactCheck, - formatErrors, nonEmptyEndpointEntriesArray, - validate, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListClient } from '../services/exception_lists/exception_list_client'; import { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants'; diff --git a/x-pack/plugins/lists/server/saved_objects/migrations.ts b/x-pack/plugins/lists/server/saved_objects/migrations.ts index 316c5f1311774..485bd493f309e 100644 --- a/x-pack/plugins/lists/server/saved_objects/migrations.ts +++ b/x-pack/plugins/lists/server/saved_objects/migrations.ts @@ -13,7 +13,7 @@ import { OsTypeArray, entriesNested, entry, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; import { ExceptionListSoSchema } from '../schemas/saved_objects'; diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts index 696434a616c53..42788c15736b7 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts @@ -12,7 +12,7 @@ import { metaOrUndefined, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { esDataTypeUnion } from '../common/schemas'; import { diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts index c69abaf785dec..4383e93346291 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts @@ -15,7 +15,7 @@ import { type, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { deserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts index 1f49943a910bc..383b6f339bb58 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { metaOrUndefined, updated_at, updated_by } from '@kbn/securitysolution-io-ts-utils'; +import { metaOrUndefined, updated_at, updated_by } from '@kbn/securitysolution-io-ts-list-types'; import { esDataTypeUnion } from '../common/schemas'; diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts index fbeac92c66bdd..fe73d0fb9207f 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts @@ -12,7 +12,7 @@ import { nameOrUndefined, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; export const updateEsListSchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts index 8ac88a1610ea7..c787f70bfa675 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts @@ -12,7 +12,7 @@ import { metaOrUndefined, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { binaryOrUndefined, diff --git a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts index a060ebda04a46..536269b9c0ae2 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts @@ -15,7 +15,7 @@ import { type, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { deserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts b/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts index f6d2e891a60d0..c1f480e50c8f7 100644 --- a/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts +++ b/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts @@ -19,7 +19,7 @@ import { osTypeArray, tags, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { immutableOrUndefined, diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts index ef4ceb2f12922..5f2587fc1e986 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts @@ -14,7 +14,7 @@ import { Name, NamespaceType, Tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListSchema, Immutable, ListId, Version } from '../../../common/schemas'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts index 5f88244171f6a..0bcc888a4c313 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts @@ -17,7 +17,7 @@ import { NamespaceType, OsTypeArray, Tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema, ItemId, ListId } from '../../../common/schemas'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts index afe9106e28d82..201cb9544a8f3 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListSchema, ListIdOrUndefined } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts index d0e1d2283cc6f..9f735fd51c7f2 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { Id, IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { Id, IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema, ItemIdOrUndefined } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts index d9ec08b818f2d..b08872eac8e01 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectsClientContract } from '../../../../../../src/core/server/'; import { ListId } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 0954a55d44dcc..576b0c4d25aa0 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -10,7 +10,6 @@ import { CreateCommentsArray, Description, DescriptionOrUndefined, - EmptyStringArrayDecoded, EntriesArray, ExceptionListItemType, ExceptionListItemTypeOrUndefined, @@ -23,12 +22,15 @@ import { NameOrUndefined, NamespaceType, NamespaceTypeArray, - NonEmptyStringArrayDecoded, OsTypeArray, Tags, TagsOrUndefined, UpdateCommentsArray, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { + EmptyStringArrayDecoded, + NonEmptyStringArrayDecoded, +} from '@kbn/securitysolution-io-ts-types'; import { FilterOrUndefined, diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts index 82ea5a4f104c5..dfe7a97d0b2f3 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { NamespaceTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectType } from '../../../common/types'; import { diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts index cb9c16ffe3c7b..b75520614150b 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { FilterOrUndefined, diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts index 6721aff5b0c1e..ad4646a57a5ca 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts @@ -6,12 +6,11 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; +import { Id, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { EmptyStringArrayDecoded, - Id, - NamespaceTypeArray, NonEmptyStringArrayDecoded, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-types'; import { SavedObjectType, diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts index 342e03160b45b..928190efbf531 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectsClientContract, diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts index cf469baa46370..be612868abe48 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectsClientContract, diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts index 69d9b87227bca..3daa2e9157b5d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts @@ -15,7 +15,7 @@ import { NamespaceType, OsTypeArray, TagsOrUndefined, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListSchema, diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts index 041008a06f3df..0d9ba8d8fefcc 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts @@ -17,7 +17,7 @@ import { OsTypeArray, TagsOrUndefined, UpdateCommentsArrayOrUndefined, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index 1322f153bf3bd..12fe8eabd4f6a 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -16,7 +16,7 @@ import { UpdateCommentsArrayOrUndefined, exceptionListItemType, exceptionListType, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectType, diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index 3c51f56c7916a..ebeef3e90933d 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { ElasticsearchClient } from 'kibana/server'; -import { IdOrUndefined, MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-utils'; +import { IdOrUndefined, MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-list-types'; import { DeserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts index 5928260ab94ac..00956a7c3c3fa 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { ElasticsearchClient } from 'kibana/server'; -import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-utils'; +import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-list-types'; import { transformListItemToElasticQuery } from '../utils'; import { DeserializerOrUndefined, SerializerOrUndefined } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts index 4fcb2656d2ba7..c08e683aafa1c 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id } from '@kbn/securitysolution-io-ts-utils'; +import { Id } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemSchema } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts index ccbe8d6fe7925..1adcf45e85748 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemArraySchema } from '../../../common/schemas'; import { getQueryFilterFromTypeValue } from '../utils'; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts index aca8deac24817..a1653cb31ce16 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id } from '@kbn/securitysolution-io-ts-utils'; +import { Id } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemSchema } from '../../../common/schemas'; import { transformElasticToListItem } from '../utils'; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts index 083dca2ea9410..a190f9388bef3 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemArraySchema } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts index 5a4d55172af23..0fcb958940d9b 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemArraySchema } from '../../../common/schemas'; import { diff --git a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts index d6d8f66770653..2b525fde6a428 100644 --- a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { SearchListItemArraySchema } from '../../../common/schemas'; import { diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index 91c38dd3f331c..4f1a19430aeda 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id, MetaOrUndefined } from '@kbn/securitysolution-io-ts-utils'; +import { Id, MetaOrUndefined } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemSchema, _VersionOrUndefined } from '../../../common/schemas'; import { transformListItemToElasticQuery } from '../utils'; diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts index 8a05e4667a290..b3ce823f9ac29 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -8,7 +8,7 @@ import { Readable } from 'stream'; import { ElasticsearchClient } from 'kibana/server'; -import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-utils'; +import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-list-types'; import { createListIfItDoesNotExist } from '../lists/create_list_if_it_does_not_exist'; import { diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index 6b0954f3fcc9d..d139ef3ea4bb1 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -13,7 +13,7 @@ import { MetaOrUndefined, Name, Type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { encodeHitVersion } from '../utils/encode_hit_version'; import { diff --git a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts index 483810a9b1c43..71094a5ab49de 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts @@ -6,7 +6,13 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Description, Id, MetaOrUndefined, Name, Type } from '@kbn/securitysolution-io-ts-utils'; +import { + Description, + Id, + MetaOrUndefined, + Name, + Type, +} from '@kbn/securitysolution-io-ts-list-types'; import { DeserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts index 0e140544fa47d..a215044b92b4c 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id } from '@kbn/securitysolution-io-ts-utils'; +import { Id } from '@kbn/securitysolution-io-ts-list-types'; import { ListSchema } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts index a248f81449bfc..7ff17bc2ee553 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id } from '@kbn/securitysolution-io-ts-utils'; +import { Id } from '@kbn/securitysolution-io-ts-list-types'; import { ListSchema } from '../../../common/schemas'; import { transformElasticToList } from '../utils/transform_elastic_to_list'; diff --git a/x-pack/plugins/lists/server/services/lists/list_client.ts b/x-pack/plugins/lists/server/services/lists/list_client.ts index 0b9bfbed28d83..a602bcf943808 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client.ts @@ -6,6 +6,17 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { + createBootstrapIndex, + deleteAllIndex, + deletePolicy, + deleteTemplate, + getIndexExists, + getPolicyExists, + getTemplateExists, + setPolicy, + setTemplate, +} from '@kbn/securitysolution-es-utils'; import { FoundListItemSchema, @@ -40,17 +51,6 @@ import { searchListItemByValues, updateListItem, } from '../../services/items'; -import { - createBootstrapIndex, - deleteAllIndex, - deletePolicy, - deleteTemplate, - getIndexExists, - getPolicyExists, - getTemplateExists, - setPolicy, - setTemplate, -} from '../../siem_server_deps'; import listsItemsPolicy from '../items/list_item_policy.json'; import listPolicy from './list_policy.json'; diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts index b684511ff679c..b4fe52019ec7b 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts @@ -17,7 +17,7 @@ import { Name, NameOrUndefined, Type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { DeserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index 4917fec7397ea..374c3cd0e2def 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -11,7 +11,7 @@ import { Id, MetaOrUndefined, NameOrUndefined, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { decodeVersion } from '../utils/decode_version'; import { encodeHitVersion } from '../utils/encode_hit_version'; diff --git a/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts b/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts index e408f7d33b548..80b10142d553a 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { getSearchEsListItemMock } from '../../schemas/elastic_response/search_es_list_item_schema.mock'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; diff --git a/x-pack/plugins/lists/server/services/utils/find_source_type.ts b/x-pack/plugins/lists/server/services/utils/find_source_type.ts index 00a6985b2c751..e69eecbbe3129 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_type.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_type.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type, type } from '@kbn/securitysolution-io-ts-utils'; +import { Type, type } from '@kbn/securitysolution-io-ts-list-types'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; diff --git a/x-pack/plugins/lists/server/services/utils/find_source_value.ts b/x-pack/plugins/lists/server/services/utils/find_source_value.ts index c12f4bdfcdb9f..7990481c3e3db 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_value.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_value.ts @@ -6,7 +6,7 @@ */ import Mustache from 'mustache'; -import { type } from '@kbn/securitysolution-io-ts-utils'; +import { type } from '@kbn/securitysolution-io-ts-list-types'; import { DeserializerOrUndefined } from '../../../common/schemas'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts index 0ece97b21d5b7..6a30cb5d6a847 100644 --- a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts @@ -6,7 +6,7 @@ */ import { isEmpty, isObject } from 'lodash/fp'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; export type QueryFilterType = [ { term: Record }, diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts index f02ae17fa0293..902fc17039792 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { SearchListItemArraySchema } from '../../../common/schemas'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts index 3e27bd24517e4..1cbf72e8eb653 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemArraySchema } from '../../../common/schemas'; import { ErrorWithStatusCode } from '../../error_with_status_code'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts index 32eb885871cb1..fc97bef54b0a6 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { SerializerOrUndefined } from '../../../common/schemas'; import { diff --git a/x-pack/plugins/lists/server/siem_server_deps.ts b/x-pack/plugins/lists/server/siem_server_deps.ts deleted file mode 100644 index a4263d089e84c..0000000000000 --- a/x-pack/plugins/lists/server/siem_server_deps.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { - transformError, - deleteTemplate, - deletePolicy, - deleteAllIndex, - setPolicy, - setTemplate, - buildSiemResponse, - getTemplateExists, - getPolicyExists, - createBootstrapIndex, - getIndexExists, - buildRouteValidation, - readPrivileges, -} from '../../security_solution/server'; - -export { formatErrors } from '../../security_solution/common'; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js index 22b8a86158a74..a0908035c1480 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js @@ -421,7 +421,7 @@ describe('createExtentFilter', () => { }); }); - it('should make left longitude greater then right longitude when area crosses 180 meridian east to west', () => { + it('should make left longitude greater than right longitude when area crosses 180 meridian east to west', () => { const mapExtent = { maxLat: 39, maxLon: 200, @@ -440,7 +440,7 @@ describe('createExtentFilter', () => { }); }); - it('should make left longitude greater then right longitude when area crosses 180 meridian west to east', () => { + it('should make left longitude greater than right longitude when area crosses 180 meridian west to east', () => { const mapExtent = { maxLat: 39, maxLon: -100, diff --git a/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts index 5de38d3f28851..be197becc6a9d 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts @@ -20,7 +20,7 @@ export function isTotalHitsGreaterThan(totalHits: TotalHits, value: number) { if (value > totalHits.value) { throw new Error( i18n.translate('xpack.maps.totalHits.lowerBoundPrecisionExceeded', { - defaultMessage: `Unable to determine if total hits is greater than value. Total hits precision is lower then value. Total hits: {totalHitsString}, value: {value}. Ensure _search.body.track_total_hits is at least as large as value.`, + defaultMessage: `Unable to determine if total hits is greater than value. Total hits precision is lower than value. Total hits: {totalHitsString}, value: {value}. Ensure _search.body.track_total_hits is at least as large as value.`, values: { totalHitsString: JSON.stringify(totalHits, null, ''), value, diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index aa1527d48654e..6a31e25197210 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; -import 'mapbox-gl/dist/mapbox-gl.css'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { AppLeaveAction, AppMountParameters } from 'kibana/public'; diff --git a/x-pack/plugins/ml/server/routes/saved_objects.ts b/x-pack/plugins/ml/server/routes/saved_objects.ts index b1e1aaf30b12b..c93730517cc11 100644 --- a/x-pack/plugins/ml/server/routes/saved_objects.ts +++ b/x-pack/plugins/ml/server/routes/saved_objects.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization, SavedObjectsRouteDeps } from '../types'; import { checksFactory, syncSavedObjectsFactory } from '../saved_objects'; @@ -13,8 +12,8 @@ import { jobsAndSpaces, jobsAndCurrentSpace, syncJobObjects, - jobTypeSchema, canDeleteJobSchema, + jobTypeSchema, } from './schemas/saved_objects'; import { spacesUtilsProvider } from '../lib/spaces_utils'; @@ -308,7 +307,7 @@ export function savedObjectsRoutes( { path: '/api/ml/saved_objects/can_delete_job/{jobType}', validate: { - params: schema.object({ jobType: jobTypeSchema }), + params: jobTypeSchema, body: canDeleteJobSchema, }, options: { diff --git a/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts b/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts index 94ee71a1bae19..85f56c1ffb412 100644 --- a/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts +++ b/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts @@ -7,19 +7,21 @@ import { schema } from '@kbn/config-schema'; -export const jobTypeSchema = schema.oneOf([ +export const jobTypeLiterals = schema.oneOf([ schema.literal('anomaly-detector'), schema.literal('data-frame-analytics'), ]); +export const jobTypeSchema = schema.object({ jobType: jobTypeLiterals }); + export const jobsAndSpaces = schema.object({ - jobType: jobTypeSchema, + jobType: jobTypeLiterals, jobIds: schema.arrayOf(schema.string()), spaces: schema.arrayOf(schema.string()), }); export const jobsAndCurrentSpace = schema.object({ - jobType: jobTypeSchema, + jobType: jobTypeLiterals, jobIds: schema.arrayOf(schema.string()), }); diff --git a/x-pack/plugins/monitoring/common/types/es.ts b/x-pack/plugins/monitoring/common/types/es.ts index 81a45695002e2..f324164b09302 100644 --- a/x-pack/plugins/monitoring/common/types/es.ts +++ b/x-pack/plugins/monitoring/common/types/es.ts @@ -266,6 +266,19 @@ export interface ElasticsearchLegacySource { }; metrics?: { beat?: { + cgroup?: { + memory: { + id: string; + mem: { + limit: { + bytes: number; + }; + usage: { + bytes: number; + }; + }; + }; + }; memstats?: { memory_alloc?: number; }; diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js index 37731f4994d2b..f58047ad6e253 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js @@ -30,7 +30,22 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; import { SetupModeFeature } from '../../../../common/enums'; -function getColumns(alerts, setupMode) { +function getColumns(alerts, setupMode, cgroup) { + const memoryField = cgroup + ? { + name: i18n.translate('xpack.monitoring.apm.instances.cgroupMemoryUsageTitle', { + defaultMessage: 'Memory Usage (cgroup)', + }), + field: 'cgroup_memory', + render: (value) => formatMetric(value, 'byte'), + } + : { + name: i18n.translate('xpack.monitoring.apm.instances.allocatedMemoryTitle', { + defaultMessage: 'Allocated Memory', + }), + field: 'memory', + render: (value) => formatMetric(value, 'byte'), + }; return [ { name: i18n.translate('xpack.monitoring.apm.instances.nameTitle', { @@ -112,13 +127,7 @@ function getColumns(alerts, setupMode) { }, }), }, - { - name: i18n.translate('xpack.monitoring.apm.instances.allocatedMemoryTitle', { - defaultMessage: 'Allocated Memory', - }), - field: 'memory', - render: (value) => formatMetric(value, 'byte'), - }, + memoryField, { name: i18n.translate('xpack.monitoring.apm.instances.versionTitle', { defaultMessage: 'Version', @@ -166,7 +175,7 @@ export function ApmServerInstances({ apms, alerts, setupMode }) { diff --git a/x-pack/plugins/monitoring/server/lib/apm/_apm_stats.js b/x-pack/plugins/monitoring/server/lib/apm/_apm_stats.js index 0dfcbfff834d8..64450405b3268 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/_apm_stats.js +++ b/x-pack/plugins/monitoring/server/lib/apm/_apm_stats.js @@ -7,6 +7,11 @@ import { get } from 'lodash'; +const getMemPath = (cgroup) => + cgroup + ? 'beats_stats.metrics.beat.cgroup.memory.mem.usage.bytes' + : 'beats_stats.metrics.beat.memstats.rss'; + export const getDiffCalculation = (max, min) => { // no need to test max >= 0, but min <= 0 which is normal for a derivative after restart // because we are aggregating/collapsing on ephemeral_ids @@ -21,13 +26,11 @@ export const apmAggFilterPath = [ 'aggregations.total', 'aggregations.min_events_total.value', 'aggregations.max_events_total.value', - 'aggregations.min_mem_rss_total.value', - 'aggregations.max_mem_rss_total.value', - 'aggregations.max_mem_total_total.value', + 'aggregations.min_mem_total.value', + 'aggregations.max_mem_total.value', 'aggregations.versions.buckets', ]; - -export const apmUuidsAgg = (maxBucketSize) => ({ +export const apmUuidsAgg = (maxBucketSize, cgroup) => ({ total: { cardinality: { field: 'beats_stats.beat.uuid', @@ -55,19 +58,14 @@ export const apmUuidsAgg = (maxBucketSize) => ({ field: 'beats_stats.metrics.libbeat.pipeline.events.total', }, }, - min_mem_rss: { + min_mem: { min: { - field: 'beats_stats.metrics.beat.memstats.rss', - }, - }, - max_mem_rss: { - max: { - field: 'beats_stats.metrics.beat.memstats.rss', + field: getMemPath(cgroup), }, }, - max_mem_total: { + max_mem: { max: { - field: 'beats_stats.metrics.beat.memstats.memory_total', + field: getMemPath(cgroup), }, }, }, @@ -82,19 +80,14 @@ export const apmUuidsAgg = (maxBucketSize) => ({ buckets_path: 'ephemeral_ids>max_events', }, }, - min_mem_rss_total: { - sum_bucket: { - buckets_path: 'ephemeral_ids>min_mem_rss', - }, - }, - max_mem_rss_total: { + min_mem_total: { sum_bucket: { - buckets_path: 'ephemeral_ids>max_mem_rss', + buckets_path: 'ephemeral_ids>min_mem', }, }, - max_mem_total_total: { + max_mem_total: { sum_bucket: { - buckets_path: 'ephemeral_ids>max_mem_total', + buckets_path: 'ephemeral_ids>max_mem', }, }, }); @@ -104,16 +97,14 @@ export const apmAggResponseHandler = (response) => { const eventsTotalMax = get(response, 'aggregations.max_events_total.value', 0); const eventsTotalMin = get(response, 'aggregations.min_events_total.value', 0); - const memRssMax = get(response, 'aggregations.max_mem_rss_total.value', 0); - const memRssMin = get(response, 'aggregations.min_mem_rss_total.value', 0); - const memTotal = get(response, 'aggregations.max_mem_total_total.value', 0); + const memMax = get(response, 'aggregations.max_mem_total.value', 0); + const memMin = get(response, 'aggregations.min_mem_total.value', 0); const versions = get(response, 'aggregations.versions.buckets', []).map(({ key }) => key); return { apmTotal, totalEvents: getDiffCalculation(eventsTotalMax, eventsTotalMin), - memRss: getDiffCalculation(memRssMax, memRssMin), - memTotal, + memRss: getDiffCalculation(memMax, memMin), versions, }; }; diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts b/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts index 05c52a56da930..b606eaf5fe793 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts @@ -88,6 +88,7 @@ export function handleResponse(response: ElasticsearchResponse, start: number, e memory: hit._source.beats_stats?.metrics?.beat?.memstats?.memory_alloc ?? hit._source.beat?.stats?.memstats?.memory?.alloc, + cgroup_memory: hit._source.beats_stats?.metrics?.beat?.cgroup?.memory.mem.usage.bytes, version: stats?.beat?.version, time_of_last_event: hit._source.beats_stats?.timestamp ?? hit._source['@timestamp'], }); @@ -122,6 +123,7 @@ export async function getApms(req: LegacyRequest, apmIndexPattern: string, clust 'hits.hits._source.beats_stats.metrics.libbeat.output.read.errors', 'hits.hits._source.beats_stats.metrics.libbeat.output.write.errors', 'hits.hits._source.beats_stats.metrics.beat.memstats.memory_alloc', + 'hits.hits._source.beats_stats.metrics.beat.cgroup.memory.mem.usage.bytes', 'hits.hits._source.beat.stats.beat.uuid', 'hits.hits._source.beat.stats.beat.name', 'hits.hits._source.beat.stats.beat.host', diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.js b/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.js index 3ece0af0369fd..45bbe35ebd59d 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.js @@ -13,13 +13,12 @@ import { apmAggResponseHandler, apmUuidsAgg, apmAggFilterPath } from './_apm_sta import { getTimeOfLastEvent } from './_get_time_of_last_event'; export function handleResponse(clusterUuid, response) { - const { apmTotal, totalEvents, memRss, memTotal, versions } = apmAggResponseHandler(response); + const { apmTotal, totalEvents, memRss, versions } = apmAggResponseHandler(response); // combine stats const stats = { totalEvents, memRss, - memTotal, apms: { total: apmTotal, }, @@ -39,6 +38,7 @@ export function getApmsForClusters(req, apmIndexPattern, clusters) { const end = req.payload.timeRange.max; const config = req.server.config(); const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); + const cgroup = config.get('monitoring.ui.container.apm.enabled'); return Promise.all( clusters.map(async (cluster) => { @@ -55,7 +55,7 @@ export function getApmsForClusters(req, apmIndexPattern, clusters) { clusterUuid, metric: ApmMetric.getMetricFields(), // override default of BeatMetric.getMetricFields }), - aggs: apmUuidsAgg(maxBucketSize), + aggs: apmUuidsAgg(maxBucketSize, cgroup), }, }; diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_stats.js b/x-pack/plugins/monitoring/server/lib/apm/get_stats.js index df199fe3c3685..7dd1b652254cb 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_stats.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_stats.js @@ -30,6 +30,7 @@ export async function getStats(req, apmIndexPattern, clusterUuid) { const start = moment.utc(req.payload.timeRange.min).valueOf(); const end = moment.utc(req.payload.timeRange.max).valueOf(); const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); + const cgroup = config.get('monitoring.ui.container.apm.enabled'); const params = { index: apmIndexPattern, @@ -42,7 +43,7 @@ export async function getStats(req, apmIndexPattern, clusterUuid) { end, clusterUuid, }), - aggs: apmUuidsAgg(maxBucketSize), + aggs: apmUuidsAgg(maxBucketSize, cgroup), }, }; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js index bae4edb201428..53afa4c3f01b4 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js @@ -44,6 +44,7 @@ export function apmInstancesRoute(server) { return { stats, apms, + cgroup: req.server.config().get('monitoring.ui.container.apm.enabled'), }; } catch (err) { return handleError(err, req); diff --git a/x-pack/plugins/observability/common/rules/observability_rule_field_map.ts b/x-pack/plugins/observability/common/rules/observability_rule_field_map.ts deleted file mode 100644 index 370f5d4ef79f2..0000000000000 --- a/x-pack/plugins/observability/common/rules/observability_rule_field_map.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ecsFieldMap, pickWithPatterns } from '../../../rule_registry/common'; - -export const observabilityRuleFieldMap = { - ...pickWithPatterns(ecsFieldMap, 'host.name', 'service.name'), - 'kibana.observability.evaluation.value': { - type: 'scaled_float' as const, - scaling_factor: 1000, - }, - 'kibana.observability.evaluation.threshold': { - type: 'scaled_float' as const, - scaling_factor: 1000, - }, -}; - -export type ObservabilityRuleFieldMap = typeof observabilityRuleFieldMap; diff --git a/x-pack/plugins/observability/common/utils/formatters/duration.ts b/x-pack/plugins/observability/common/utils/formatters/duration.ts index 6bbeb44ef06af..481005332cc30 100644 --- a/x-pack/plugins/observability/common/utils/formatters/duration.ts +++ b/x-pack/plugins/observability/common/utils/formatters/duration.ts @@ -201,6 +201,9 @@ export function asDuration( const formatter = getDurationFormatter(value); return formatter(value, { defaultValue, extended }).formatted; } + +export type AsDuration = typeof asDuration; + /** * Convert a microsecond value to decimal milliseconds. Normally we use * `asDuration`, but this is used in places like tables where we always want diff --git a/x-pack/plugins/observability/common/utils/formatters/formatters.ts b/x-pack/plugins/observability/common/utils/formatters/formatters.ts index 3c307f64fa0a9..9bdccc7e9edfe 100644 --- a/x-pack/plugins/observability/common/utils/formatters/formatters.ts +++ b/x-pack/plugins/observability/common/utils/formatters/formatters.ts @@ -47,6 +47,8 @@ export function asPercent( return numeral(decimal).format('0.0%'); } +export type AsPercent = typeof asPercent; + export function asDecimalOrInteger(value: number) { // exact 0 or above 10 should not have decimal if (value === 0 || value >= 10) { diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 0ee978c75d6c0..52d5493ae69a4 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -15,7 +15,8 @@ "requiredPlugins": [ "data", "alerting", - "ruleRegistry" + "ruleRegistry", + "triggersActionsUi" ], "ui": true, "server": true, diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx index c0b51652a7d0e..9182a0e8196c8 100644 --- a/x-pack/plugins/observability/public/application/application.test.tsx +++ b/x-pack/plugins/observability/public/application/application.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { Observable } from 'rxjs'; import { AppMountParameters, CoreStart } from 'src/core/public'; import { ObservabilityPublicPluginsStart } from '../plugin'; -import { createObservabilityRuleRegistryMock } from '../rules/observability_rule_registry_mock'; +import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock'; import { renderApp } from './'; describe('renderApp', () => { @@ -58,7 +58,7 @@ describe('renderApp', () => { core, plugins, appMountParameters: params, - observabilityRuleRegistry: createObservabilityRuleRegistryMock(), + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), }); unmount(); }).not.toThrowError(); diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 8607b57b42666..460aa6c35bdb8 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -18,11 +18,12 @@ import { import { PluginContext } from '../context/plugin_context'; import { usePluginContext } from '../hooks/use_plugin_context'; import { useRouteParams } from '../hooks/use_route_params'; -import { ObservabilityPublicPluginsStart, ObservabilityRuleRegistry } from '../plugin'; +import { ObservabilityPublicPluginsStart } from '../plugin'; import { HasDataContextProvider } from '../context/has_data_context'; import { Breadcrumbs, routes } from '../routes'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { ConfigSchema } from '..'; +import { ObservabilityRuleTypeRegistry } from '../rules/create_observability_rule_type_registry'; function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumbs) { return breadcrumbs.map(({ text }) => text).reverse(); @@ -72,12 +73,12 @@ export const renderApp = ({ core, plugins, appMountParameters, - observabilityRuleRegistry, + observabilityRuleTypeRegistry, }: { config: ConfigSchema; core: CoreStart; plugins: ObservabilityPublicPluginsStart; - observabilityRuleRegistry: ObservabilityRuleRegistry; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; appMountParameters: AppMountParameters; }) => { const { element, history } = appMountParameters; @@ -94,7 +95,7 @@ export const renderApp = ({ ReactDOM.render( diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index d41f131ef521b..67fede05f3ced 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -14,7 +14,8 @@ import * as hasDataHook from '../../../../hooks/use_has_data'; import * as pluginContext from '../../../../hooks/use_plugin_context'; import { HasDataContextValue } from '../../../../context/has_data_context'; import { AppMountParameters, CoreStart } from 'kibana/public'; -import { ObservabilityPublicPluginsStart, ObservabilityRuleRegistry } from '../../../../plugin'; +import { ObservabilityPublicPluginsStart } from '../../../../plugin'; +import { createObservabilityRuleTypeRegistryMock } from '../../../../rules/observability_rule_type_registry_mock'; jest.mock('react-router-dom', () => ({ useLocation: () => ({ @@ -41,10 +42,7 @@ describe('APMSection', () => { } as unknown) as CoreStart, appMountParameters: {} as AppMountParameters, config: { unsafe: { alertingExperience: { enabled: true } } }, - observabilityRuleRegistry: ({ - registerType: jest.fn(), - getTypeByRuleId: jest.fn(), - } as unknown) as ObservabilityRuleRegistry, + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), plugins: ({ data: { query: { @@ -62,7 +60,7 @@ describe('APMSection', () => { })); }); - it('renders transaction stat less then 1k', () => { + it('renders transaction stat less than 1k', () => { const resp = { appLink: '/app/apm', stats: { diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts index b4b03b2194ef2..f3853fa5a91da 100644 --- a/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts +++ b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts @@ -8,19 +8,19 @@ import { formatDuration } from './format_duration'; describe('formatDuration(seconds)', () => { - it('should work for less then a minute', () => { + it('should work for less than a minute', () => { expect(formatDuration(56)).toBe('56s'); }); - it('should work for less then a hour', () => { + it('should work for less than a hour', () => { expect(formatDuration(2000)).toBe('33m 20s'); }); - it('should work for less then a day', () => { + it('should work for less than a day', () => { expect(formatDuration(74566)).toBe('20h 42m'); }); - it('should work for more then a day', () => { + it('should work for more than a day', () => { expect(formatDuration(86400 * 3 + 3600 * 4)).toBe('3d 4h'); expect(formatDuration(86400 * 419 + 3600 * 6)).toBe('419d 6h'); }); diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx index fa4d1a744e3ea..b4227cc122dde 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx @@ -7,7 +7,6 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; -import { createObservabilityRuleRegistryMock } from '../../../../rules/observability_rule_registry_mock'; import { HasDataContextValue } from '../../../../context/has_data_context'; import * as fetcherHook from '../../../../hooks/use_fetcher'; import * as hasDataHook from '../../../../hooks/use_has_data'; @@ -16,6 +15,7 @@ import { ObservabilityPublicPluginsStart } from '../../../../plugin'; import { render } from '../../../../utils/test_helper'; import { UXSection } from './'; import { response } from './mock_data/ux.mock'; +import { createObservabilityRuleTypeRegistryMock } from '../../../../rules/observability_rule_type_registry_mock'; jest.mock('react-router-dom', () => ({ useLocation: () => ({ @@ -55,7 +55,7 @@ describe('UXSection', () => { }, }, } as unknown) as ObservabilityPublicPluginsStart, - observabilityRuleRegistry: createObservabilityRuleRegistryMock(), + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), })); }); it('renders with core web vitals', () => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index d95fad758565b..cf51c4614e543 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -43,7 +43,7 @@ describe('ExploratoryView', () => { await waitFor(() => { screen.getByText(/open in lens/i); - screen.getByRole('heading', { name: /exploratory view/i }); + screen.getByRole('heading', { name: /analyze data/i }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index 9d051e89e1a38..8f2f30185d37f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -33,7 +33,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {

{DataViewLabels[series.reportType] ?? i18n.translate('xpack.observability.expView.heading.label', { - defaultMessage: 'Exploratory view', + defaultMessage: 'Analyze data', })}{' '} ({ useLocation: () => ({ @@ -39,7 +39,7 @@ describe('useTimeRange', () => { }, }, } as unknown) as ObservabilityPublicPluginsStart, - observabilityRuleRegistry: createObservabilityRuleRegistryMock(), + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), })); jest.spyOn(kibanaUISettings, 'useKibanaUISettings').mockImplementation(() => ({ from: '2020-10-08T05:00:00.000Z', @@ -81,7 +81,7 @@ describe('useTimeRange', () => { }, }, } as unknown) as ObservabilityPublicPluginsStart, - observabilityRuleRegistry: createObservabilityRuleRegistryMock(), + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), })); }); it('returns ranges and absolute times from kibana default settings', () => { diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index a011d1fc2c414..8dd2f6a57eefe 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -62,4 +62,5 @@ export { getApmTraceUrl } from './utils/get_apm_trace_url'; export { createExploratoryViewUrl } from './components/shared/exploratory_view/configurations/utils'; export type { SeriesUrl } from './components/shared/exploratory_view/types'; -export { FormatterRuleRegistry } from './rules/formatter_rule_registry'; +export type { ObservabilityRuleTypeRegistry } from './rules/create_observability_rule_type_registry'; +export { createObservabilityRuleTypeRegistryMock } from './rules/observability_rule_type_registry_mock'; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx index 6940f6aaad692..0d47f3da89d36 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx @@ -13,7 +13,7 @@ import { AlertsPage } from '.'; import { HttpSetup } from '../../../../../../src/core/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { PluginContext, PluginContextValue } from '../../context/plugin_context'; -import { createObservabilityRuleRegistryMock } from '../../rules/observability_rule_registry_mock'; +import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock'; import { createCallObservabilityApi } from '../../services/call_observability_api'; import type { ObservabilityAPIReturnType } from '../../services/call_observability_api/types'; import { apmAlertResponseExample, dynamicIndexPattern } from './example_data'; @@ -62,7 +62,7 @@ export default { core: { http: { basePath: { prepend: (_: string) => '' } }, }, - observabilityRuleRegistry: createObservabilityRuleRegistryMock(), + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), } as unknown) as PluginContextValue } > diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx index 96d3c1fc9c390..90c75a70c0813 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx @@ -62,25 +62,26 @@ Example.args = { reason: 'Error count for opbeans-java was above the threshold', active: true, start: 1618235449493, - - 'rule.id': 'apm.error_rate', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'rule.name': 'Error count threshold | opbeans-java (smith test)', - 'kibana.rac.alert.duration.us': 61787000, - 'kibana.observability.evaluation.threshold': 0, - 'kibana.rac.alert.status': 'open', - tags: ['apm', 'service.name:opbeans-java'], - 'kibana.rac.alert.uuid': 'c50fbc70-0d77-462d-ac0a-f2bd0b8512e4', - 'rule.uuid': '474920d0-93e9-11eb-ac86-0b455460de81', - 'event.action': 'active', - '@timestamp': '2021-04-14T21:43:42.966Z', - 'kibana.rac.alert.id': 'apm.error_rate_opbeans-java_production', - 'processor.event': 'error', - 'kibana.rac.alert.start': '2021-04-14T21:42:41.179Z', - 'kibana.rac.producer': 'apm', - 'event.kind': 'state', - 'rule.category': 'Error count threshold', - 'kibana.observability.evaluation.value': 1, + fields: { + 'rule.id': 'apm.error_rate', + 'service.environment': ['production'], + 'service.name': ['opbeans-java'], + 'rule.name': 'Error count threshold | opbeans-java (smith test)', + 'kibana.rac.alert.duration.us': 61787000, + 'kibana.rac.alert.evaluation.threshold': 0, + 'kibana.rac.alert.status': 'open', + tags: ['apm', 'service.name:opbeans-java'], + 'kibana.rac.alert.uuid': 'c50fbc70-0d77-462d-ac0a-f2bd0b8512e4', + 'rule.uuid': '474920d0-93e9-11eb-ac86-0b455460de81', + 'event.action': 'active', + '@timestamp': '2021-04-14T21:43:42.966Z', + 'kibana.rac.alert.id': 'apm.error_rate_opbeans-java_production', + 'processor.event': ['error'], + 'kibana.rac.alert.start': '2021-04-14T21:42:41.179Z', + 'kibana.rac.producer': 'apm', + 'event.kind': 'state', + 'rule.category': 'Error count threshold', + 'kibana.rac.alert.evaluation.value': 1, + }, }, } as Args; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx index 09fe464aa8cb5..b4bf96bcc6905 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx @@ -22,6 +22,14 @@ import { import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; import React from 'react'; +import { + ALERT_DURATION, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_SEVERITY_LEVEL, + RULE_CATEGORY, + RULE_NAME, +} from '@kbn/rule-data-utils/target/technical_field_names'; import { TopAlert } from '../'; import { useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; import { asDuration } from '../../../../common/utils/formatters'; @@ -46,7 +54,7 @@ export function AlertsFlyout({ onClose, alert }: AlertsFlyoutProps) { title: i18n.translate('xpack.observability.alertsFlyout.severityLabel', { defaultMessage: 'Severity', }), - description: , + description: , }, { title: i18n.translate('xpack.observability.alertsFlyout.triggeredLabel', { @@ -60,25 +68,25 @@ export function AlertsFlyout({ onClose, alert }: AlertsFlyoutProps) { title: i18n.translate('xpack.observability.alertsFlyout.durationLabel', { defaultMessage: 'Duration', }), - description: asDuration(alert['kibana.rac.alert.duration.us'], { extended: true }), + description: asDuration(alert.fields[ALERT_DURATION], { extended: true }), }, { title: i18n.translate('xpack.observability.alertsFlyout.expectedValueLabel', { defaultMessage: 'Expected value', }), - description: alert['kibana.observability.evaluation.threshold'] ?? '-', + description: alert.fields[ALERT_EVALUATION_THRESHOLD] ?? '-', }, { title: i18n.translate('xpack.observability.alertsFlyout.actualValueLabel', { defaultMessage: 'Actual value', }), - description: alert['kibana.observability.evaluation.value'] ?? '-', + description: alert.fields[ALERT_EVALUATION_VALUE] ?? '-', }, { title: i18n.translate('xpack.observability.alertsFlyout.ruleTypeLabel', { defaultMessage: 'Rule type', }), - description: alert['rule.category'] ?? '-', + description: alert.fields[RULE_CATEGORY] ?? '-', }, ]; @@ -86,7 +94,7 @@ export function AlertsFlyout({ onClose, alert }: AlertsFlyoutProps) { -

{alert['rule.name']}

+

{alert.fields[RULE_NAME]}

{alert.reason} diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx index b0ff156fde377..f377186623a03 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx @@ -16,6 +16,10 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; +import { + ALERT_DURATION, + ALERT_SEVERITY_LEVEL, +} from '@kbn/rule-data-utils/target/technical_field_names'; import { asDuration } from '../../../common/utils/formatters'; import { TimestampTooltip } from '../../components/shared/timestamp_tooltip'; import { usePluginContext } from '../../hooks/use_plugin_context'; @@ -94,9 +98,7 @@ export function AlertsTable(props: AlertsTableProps) { }), render: (_, alert) => { const { active } = alert; - return active - ? null - : asDuration(alert['kibana.rac.alert.duration.us'], { extended: true }); + return active ? null : asDuration(alert.fields[ALERT_DURATION], { extended: true }); }, }, { @@ -105,7 +107,7 @@ export function AlertsTable(props: AlertsTableProps) { defaultMessage: 'Severity', }), render: (_, alert) => { - return ; + return ; }, }, { diff --git a/x-pack/plugins/observability/public/pages/alerts/example_data.ts b/x-pack/plugins/observability/public/pages/alerts/example_data.ts index dba6f1e9aaa2f..5318fce82c1d3 100644 --- a/x-pack/plugins/observability/public/pages/alerts/example_data.ts +++ b/x-pack/plugins/observability/public/pages/alerts/example_data.ts @@ -7,42 +7,42 @@ export const apmAlertResponseExample = [ { - 'rule.id': 'apm.error_rate', - 'service.name': 'opbeans-java', - 'rule.name': 'Error count threshold | opbeans-java (smith test)', - 'kibana.rac.alert.duration.us': 180057000, - 'kibana.rac.alert.status': 'open', - 'kibana.rac.alert.severity.level': 'warning', + 'rule.id': ['apm.error_rate'], + 'service.name': ['opbeans-java'], + 'rule.name': ['Error count threshold | opbeans-java (smith test)'], + 'kibana.rac.alert.duration.us': [180057000], + 'kibana.rac.alert.status': ['open'], + 'kibana.rac.alert.severity.level': ['warning'], tags: ['apm', 'service.name:opbeans-java'], - 'kibana.rac.alert.uuid': '0175ec0a-a3b1-4d41-b557-e21c2d024352', - 'rule.uuid': '474920d0-93e9-11eb-ac86-0b455460de81', - 'event.action': 'active', - '@timestamp': '2021-04-12T13:53:49.550Z', - 'kibana.rac.alert.id': 'apm.error_rate_opbeans-java_production', - 'kibana.rac.alert.start': '2021-04-12T13:50:49.493Z', - 'kibana.rac.producer': 'apm', - 'event.kind': 'state', - 'rule.category': 'Error count threshold', + 'kibana.rac.alert.uuid': ['0175ec0a-a3b1-4d41-b557-e21c2d024352'], + 'rule.uuid': ['474920d0-93e9-11eb-ac86-0b455460de81'], + 'event.action': ['active'], + '@timestamp': ['2021-04-12T13:53:49.550Z'], + 'kibana.rac.alert.id': ['apm.error_rate_opbeans-java_production'], + 'kibana.rac.alert.start': ['2021-04-12T13:50:49.493Z'], + 'kibana.rac.producer': ['apm'], + 'event.kind': ['state'], + 'rule.category': ['Error count threshold'], 'service.environment': ['production'], 'processor.event': ['error'], }, { - 'rule.id': 'apm.error_rate', - 'service.name': 'opbeans-java', - 'rule.name': 'Error count threshold | opbeans-java (smith test)', - 'kibana.rac.alert.duration.us': 2419005000, - 'kibana.rac.alert.end': '2021-04-12T13:49:49.446Z', - 'kibana.rac.alert.status': 'closed', + 'rule.id': ['apm.error_rate'], + 'service.name': ['opbeans-java'], + 'rule.name': ['Error count threshold | opbeans-java (smith test)'], + 'kibana.rac.alert.duration.us': [2419005000], + 'kibana.rac.alert.end': ['2021-04-12T13:49:49.446Z'], + 'kibana.rac.alert.status': ['closed'], tags: ['apm', 'service.name:opbeans-java'], - 'kibana.rac.alert.uuid': '32b940e1-3809-4c12-8eee-f027cbb385e2', - 'rule.uuid': '474920d0-93e9-11eb-ac86-0b455460de81', - 'event.action': 'close', - '@timestamp': '2021-04-12T13:49:49.446Z', - 'kibana.rac.alert.id': 'apm.error_rate_opbeans-java_production', - 'kibana.rac.alert.start': '2021-04-12T13:09:30.441Z', - 'kibana.rac.producer': 'apm', - 'event.kind': 'state', - 'rule.category': 'Error count threshold', + 'kibana.rac.alert.uuid': ['32b940e1-3809-4c12-8eee-f027cbb385e2'], + 'rule.uuid': ['474920d0-93e9-11eb-ac86-0b455460de81'], + 'event.action': ['close'], + '@timestamp': ['2021-04-12T13:49:49.446Z'], + 'kibana.rac.alert.id': ['apm.error_rate_opbeans-java_production'], + 'kibana.rac.alert.start': ['2021-04-12T13:09:30.441Z'], + 'kibana.rac.producer': ['apm'], + 'event.kind': ['state'], + 'rule.category': ['Error count threshold'], 'service.environment': ['production'], 'processor.event': ['error'], }, diff --git a/x-pack/plugins/observability/public/pages/alerts/index.tsx b/x-pack/plugins/observability/public/pages/alerts/index.tsx index a6d5c6926973e..1f468a70d0976 100644 --- a/x-pack/plugins/observability/public/pages/alerts/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/index.tsx @@ -17,6 +17,16 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useHistory } from 'react-router-dom'; import { format, parse } from 'url'; +import { + ALERT_START, + EVENT_ACTION, + RULE_ID, + RULE_NAME, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import { + ParsedTechnicalFields, + parseTechnicalFields, +} from '../../../../rule_registry/common/parse_technical_fields'; import { asDuration, asPercent } from '../../../common/utils/formatters'; import { ExperimentalBadge } from '../../components/shared/experimental_badge'; import { useFetcher } from '../../hooks/use_fetcher'; @@ -30,7 +40,8 @@ import { AlertsTable } from './alerts_table'; export type TopAlertResponse = ObservabilityAPIReturnType<'GET /api/observability/rules/alerts/top'>[number]; -export interface TopAlert extends TopAlertResponse { +export interface TopAlert { + fields: ParsedTechnicalFields; start: number; reason: string; link?: string; @@ -42,7 +53,7 @@ interface AlertsPageProps { } export function AlertsPage({ routeParams }: AlertsPageProps) { - const { core, observabilityRuleRegistry } = usePluginContext(); + const { core, observabilityRuleTypeRegistry } = usePluginContext(); const { prepend } = core.http.basePath; const history = useHistory(); const { @@ -74,18 +85,19 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { }, }).then((alerts) => { return alerts.map((alert) => { - const ruleType = observabilityRuleRegistry.getTypeByRuleId(alert['rule.id']); + const parsedFields = parseTechnicalFields(alert); + const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[RULE_ID]!); const formatted = { link: undefined, - reason: alert['rule.name'], - ...(ruleType?.format?.({ alert, formatters: { asDuration, asPercent } }) ?? {}), + reason: parsedFields[RULE_NAME]!, + ...(formatter?.({ fields: parsedFields, formatters: { asDuration, asPercent } }) ?? {}), }; const parsedLink = formatted.link ? parse(formatted.link, true) : undefined; return { - ...alert, ...formatted, + fields: parsedFields, link: parsedLink ? format({ ...parsedLink, @@ -96,13 +108,13 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { }, }) : undefined, - active: alert['event.action'] !== 'close', - start: new Date(alert['kibana.rac.alert.start']).getTime(), + active: parsedFields[EVENT_ACTION] !== 'close', + start: new Date(parsedFields[ALERT_START]!).getTime(), }; }); }); }, - [kuery, observabilityRuleRegistry, rangeFrom, rangeTo] + [kuery, observabilityRuleTypeRegistry, rangeFrom, rangeTo] ); return ( diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index 559aa8d5884a9..ebd1c73859169 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -23,7 +23,7 @@ import { emptyResponse as emptyLogsResponse, fetchLogsData } from './mock/logs.m import { emptyResponse as emptyMetricsResponse, fetchMetricsData } from './mock/metrics.mock'; import { newsFeedFetchData } from './mock/news_feed.mock'; import { emptyResponse as emptyUptimeResponse, fetchUptimeData } from './mock/uptime.mock'; -import { createObservabilityRuleRegistryMock } from '../../rules/observability_rule_registry_mock'; +import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock'; function unregisterAll() { unregisterDataHandler({ appName: 'apm' }); @@ -54,7 +54,7 @@ const withCore = makeDecorator({ }, }, } as unknown) as ObservabilityPublicPluginsStart, - observabilityRuleRegistry: createObservabilityRuleRegistryMock(), + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), }} > diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 517675fe1d525..6856bc97b4a36 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -7,6 +7,10 @@ import { i18n } from '@kbn/i18n'; import { BehaviorSubject } from 'rxjs'; +import { + TriggersAndActionsUIPublicPluginSetup, + TriggersAndActionsUIPublicPluginStart, +} from '../../triggers_actions_ui/public'; import { AppMountParameters, AppUpdater, @@ -25,26 +29,23 @@ import type { HomePublicPluginStart, } from '../../../../src/plugins/home/public'; import type { LensPublicStart } from '../../lens/public'; -import type { RuleRegistryPublicPluginSetupContract } from '../../rule_registry/public'; -import type { ObservabilityRuleFieldMap } from '../common/rules/observability_rule_field_map'; -import { observabilityRuleRegistrySettings } from '../common/rules/observability_rule_registry_settings'; import { registerDataHandler } from './data_handler'; -import { FormatterRuleRegistry } from './rules/formatter_rule_registry'; import { createCallObservabilityApi } from './services/call_observability_api'; import { toggleOverviewLinkInNav } from './toggle_overview_link_in_nav'; import { ConfigSchema } from '.'; +import { createObservabilityRuleTypeRegistry } from './rules/create_observability_rule_type_registry'; export type ObservabilityPublicSetup = ReturnType; -export type ObservabilityRuleRegistry = ObservabilityPublicSetup['ruleRegistry']; export interface ObservabilityPublicPluginsSetup { data: DataPublicPluginSetup; - ruleRegistry: RuleRegistryPublicPluginSetupContract; + triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; home?: HomePublicPluginSetup; } export interface ObservabilityPublicPluginsStart { home?: HomePublicPluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; lens: LensPublicStart; } @@ -75,11 +76,9 @@ export class Plugin createCallObservabilityApi(coreSetup.http); - const observabilityRuleRegistry = pluginsSetup.ruleRegistry.registry.create({ - ...observabilityRuleRegistrySettings, - fieldMap: {} as ObservabilityRuleFieldMap, - ctor: FormatterRuleRegistry, - }); + const observabilityRuleTypeRegistry = createObservabilityRuleTypeRegistry( + pluginsSetup.triggersActionsUi.alertTypeRegistry + ); const mount = async (params: AppMountParameters) => { // Load application bundle @@ -92,7 +91,7 @@ export class Plugin core: coreStart, plugins: pluginsStart, appMountParameters: params, - observabilityRuleRegistry, + observabilityRuleTypeRegistry, }); }; @@ -165,7 +164,7 @@ export class Plugin return { dashboard: { register: registerDataHandler }, - ruleRegistry: observabilityRuleRegistry, + observabilityRuleTypeRegistry, isAlertingExperienceEnabled: () => config.unsafe.alertingExperience.enabled, }; } diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 3e5c3ddc553ef..0bdb03995ad46 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -132,7 +132,7 @@ export const routes = { breadcrumb: [ { text: i18n.translate('xpack.observability.overview.exploratoryView', { - defaultMessage: 'Exploratory view', + defaultMessage: 'Analyze data', }), }, ], diff --git a/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts b/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts new file mode 100644 index 0000000000000..cba9df83c6fe3 --- /dev/null +++ b/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertTypeModel, AlertTypeRegistryContract } from '../../../triggers_actions_ui/public'; +import { ParsedTechnicalFields } from '../../../rule_registry/common/parse_technical_fields'; +import { AsDuration, AsPercent } from '../../common/utils/formatters'; + +type Formatter = (options: { + fields: ParsedTechnicalFields & Record; + formatters: { asDuration: AsDuration; asPercent: AsPercent }; +}) => { reason: string; link: string }; + +export function createObservabilityRuleTypeRegistry(alertTypeRegistry: AlertTypeRegistryContract) { + const formatters: Array<{ typeId: string; fn: Formatter }> = []; + return { + register: (type: AlertTypeModel & { format: Formatter }) => { + const { format, ...rest } = type; + formatters.push({ typeId: type.id, fn: format }); + alertTypeRegistry.register(rest); + }, + getFormatter: (typeId: string) => { + return formatters.find((formatter) => formatter.typeId === typeId)?.fn; + }, + }; +} + +export type ObservabilityRuleTypeRegistry = ReturnType; diff --git a/x-pack/plugins/observability/public/rules/formatter_rule_registry.ts b/x-pack/plugins/observability/public/rules/formatter_rule_registry.ts deleted file mode 100644 index 0d0d22cf750fb..0000000000000 --- a/x-pack/plugins/observability/public/rules/formatter_rule_registry.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { RuleType } from '../../../rule_registry/public'; -import type { BaseRuleFieldMap, OutputOfFieldMap } from '../../../rule_registry/common'; -import { RuleRegistry } from '../../../rule_registry/public'; -import type { asDuration, asPercent } from '../../common/utils/formatters'; - -type AlertTypeOf = OutputOfFieldMap; - -type FormattableRuleType = RuleType & { - format?: (options: { - alert: AlertTypeOf; - formatters: { - asDuration: typeof asDuration; - asPercent: typeof asPercent; - }; - }) => { - reason?: string; - link?: string; - }; -}; - -export class FormatterRuleRegistry extends RuleRegistry< - TFieldMap, - FormattableRuleType -> {} diff --git a/x-pack/plugins/observability/public/rules/observability_rule_registry_mock.ts b/x-pack/plugins/observability/public/rules/observability_rule_registry_mock.ts deleted file mode 100644 index 389b581b5fb60..0000000000000 --- a/x-pack/plugins/observability/public/rules/observability_rule_registry_mock.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ObservabilityRuleRegistry } from '../plugin'; - -const createRuleRegistryMock = () => ({ - registerType: () => {}, - getTypeByRuleId: () => ({ format: () => ({ link: '/test/example' }) }), - create: () => createRuleRegistryMock(), -}); - -export const createObservabilityRuleRegistryMock = () => - createRuleRegistryMock() as ObservabilityRuleRegistry & ReturnType; diff --git a/x-pack/plugins/observability/public/rules/observability_rule_type_registry_mock.ts b/x-pack/plugins/observability/public/rules/observability_rule_type_registry_mock.ts new file mode 100644 index 0000000000000..b2cf48f8e1c32 --- /dev/null +++ b/x-pack/plugins/observability/public/rules/observability_rule_type_registry_mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ObservabilityRuleTypeRegistry } from './create_observability_rule_type_registry'; + +const createRuleTypeRegistryMock = () => ({ + registerFormatter: () => {}, +}); + +export const createObservabilityRuleTypeRegistryMock = () => + createRuleTypeRegistryMock() as ObservabilityRuleTypeRegistry & + ReturnType; diff --git a/x-pack/plugins/observability/public/utils/test_helper.tsx b/x-pack/plugins/observability/public/utils/test_helper.tsx index 63e34b018aed0..ef7c62a143f25 100644 --- a/x-pack/plugins/observability/public/utils/test_helper.tsx +++ b/x-pack/plugins/observability/public/utils/test_helper.tsx @@ -15,7 +15,7 @@ import translations from '../../../translations/translations/ja-JP.json'; import { PluginContext } from '../context/plugin_context'; import { ObservabilityPublicPluginsStart } from '../plugin'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; -import { createObservabilityRuleRegistryMock } from '../rules/observability_rule_registry_mock'; +import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock'; const appMountParameters = ({ setHeaderActionMenu: () => {} } as unknown) as AppMountParameters; @@ -37,14 +37,14 @@ const plugins = ({ data: { query: { timefilter: { timefilter: { setTime: jest.fn() } } } }, } as unknown) as ObservabilityPublicPluginsStart; -const observabilityRuleRegistry = createObservabilityRuleRegistryMock(); +const observabilityRuleTypeRegistry = createObservabilityRuleTypeRegistryMock(); export const render = (component: React.ReactNode) => { return testLibRender( {component} diff --git a/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts b/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts index 0045c0f0c6757..ddfc112ab1452 100644 --- a/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts +++ b/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts @@ -4,24 +4,24 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Required } from 'utility-types'; -import { ObservabilityRuleRegistryClient } from '../../types'; +import { ALERT_UUID, TIMESTAMP } from '@kbn/rule-data-utils/target/technical_field_names'; +import { RuleDataClient } from '../../../../rule_registry/server'; import { kqlQuery, rangeQuery } from '../../utils/queries'; export async function getTopAlerts({ - ruleRegistryClient, + ruleDataClient, start, end, kuery, size, }: { - ruleRegistryClient: ObservabilityRuleRegistryClient; + ruleDataClient: RuleDataClient; start: number; end: number; kuery?: string; size: number; }) { - const response = await ruleRegistryClient.search({ + const response = await ruleDataClient.getReader().search({ body: { query: { bool: { @@ -30,26 +30,18 @@ export async function getTopAlerts({ }, fields: ['*'], collapse: { - field: 'kibana.rac.alert.uuid', + field: ALERT_UUID, }, size, sort: { - '@timestamp': 'desc', + [TIMESTAMP]: 'desc', }, _source: false, }, + allow_no_indices: true, }); - return response.events.map((event) => { - return event as Required< - typeof event, - | 'rule.id' - | 'rule.name' - | 'kibana.rac.alert.start' - | 'event.action' - | 'rule.category' - | 'rule.name' - | 'kibana.rac.alert.duration.us' - >; + return response.hits.hits.map((hit) => { + return hit.fields; }); } diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index b5208260297d0..046a9a62d5fa7 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -6,6 +6,7 @@ */ import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; +import { RuleDataClient } from '../../rule_registry/server'; import { ObservabilityConfig } from '.'; import { bootstrapAnnotations, @@ -16,11 +17,8 @@ import type { RuleRegistryPluginSetupContract } from '../../rule_registry/server import { uiSettings } from './ui_settings'; import { registerRoutes } from './routes/register_routes'; import { getGlobalObservabilityServerRouteRepository } from './routes/get_global_observability_server_route_repository'; -import { observabilityRuleRegistrySettings } from '../common/rules/observability_rule_registry_settings'; -import { observabilityRuleFieldMap } from '../common/rules/observability_rule_field_map'; export type ObservabilityPluginSetup = ReturnType; -export type ObservabilityRuleRegistry = ObservabilityPluginSetup['ruleRegistry']; export class ObservabilityPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { @@ -51,19 +49,25 @@ export class ObservabilityPlugin implements Plugin { }); } - const observabilityRuleRegistry = plugins.ruleRegistry.create({ - ...observabilityRuleRegistrySettings, - fieldMap: observabilityRuleFieldMap, + const start = () => core.getStartServices().then(([coreStart]) => coreStart); + + const ruleDataClient = new RuleDataClient({ + getClusterClient: async () => { + const coreStart = await start(); + return coreStart.elasticsearch.client.asInternalUser; + }, + ready: () => Promise.resolve(), + alias: plugins.ruleRegistry.getFullAssetName(), }); registerRoutes({ core: { setup: core, - start: () => core.getStartServices().then(([coreStart]) => coreStart), + start, }, - ruleRegistry: observabilityRuleRegistry, logger: this.initContext.logger.get(), repository: getGlobalObservabilityServerRouteRepository(), + ruleDataClient, }); return { @@ -71,7 +75,6 @@ export class ObservabilityPlugin implements Plugin { const api = await annotationsApiPromise; return api?.getScopedAnnotationsClient(...args); }, - ruleRegistry: observabilityRuleRegistry, }; } diff --git a/x-pack/plugins/observability/server/routes/register_routes.ts b/x-pack/plugins/observability/server/routes/register_routes.ts index 85ee456b812b8..75b6703cc64de 100644 --- a/x-pack/plugins/observability/server/routes/register_routes.ts +++ b/x-pack/plugins/observability/server/routes/register_routes.ts @@ -13,23 +13,23 @@ import { import { CoreSetup, CoreStart, Logger, RouteRegistrar } from 'kibana/server'; import Boom from '@hapi/boom'; import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors'; -import { ObservabilityRuleRegistry } from '../plugin'; +import { RuleDataClient } from '../../../rule_registry/server'; import { ObservabilityRequestHandlerContext } from '../types'; import { AbstractObservabilityServerRouteRepository } from './types'; export function registerRoutes({ - ruleRegistry, repository, core, logger, + ruleDataClient, }: { core: { setup: CoreSetup; start: () => Promise; }; - ruleRegistry: ObservabilityRuleRegistry; repository: AbstractObservabilityServerRouteRepository; logger: Logger; + ruleDataClient: RuleDataClient; }) { const routes = repository.getRoutes(); @@ -59,10 +59,10 @@ export function registerRoutes({ const data = (await handler({ context, request, - ruleRegistry, core, logger, params: decodedParams, + ruleDataClient, })) as any; return response.ok({ body: data }); diff --git a/x-pack/plugins/observability/server/routes/rules.ts b/x-pack/plugins/observability/server/routes/rules.ts index cd3f4976e0af3..1f500adff5dcf 100644 --- a/x-pack/plugins/observability/server/routes/rules.ts +++ b/x-pack/plugins/observability/server/routes/rules.ts @@ -6,7 +6,6 @@ */ import * as t from 'io-ts'; import { isoToEpochRt, toNumberRt } from '@kbn/io-ts-utils'; -import Boom from '@hapi/boom'; import { createObservabilityServerRoute } from './create_observability_server_route'; import { createObservabilityServerRouteRepository } from './create_observability_server_route_repository'; import { getTopAlerts } from '../lib/rules/get_top_alerts'; @@ -28,22 +27,13 @@ const alertsListRoute = createObservabilityServerRoute({ }), ]), }), - handler: async ({ ruleRegistry, context, params }) => { - const ruleRegistryClient = await ruleRegistry.createScopedRuleRegistryClient({ - context, - alertsClient: context.alerting.getAlertsClient(), - }); - - if (!ruleRegistryClient) { - throw Boom.failedDependency('xpack.ruleRegistry.unsafe.write.enabled is set to false'); - } - + handler: async ({ ruleDataClient, context, params }) => { const { query: { start, end, kuery, size = 100 }, } = params; return getTopAlerts({ - ruleRegistryClient, + ruleDataClient, start, end, kuery, @@ -57,17 +47,10 @@ const alertsDynamicIndexPatternRoute = createObservabilityServerRoute({ options: { tags: [], }, - handler: async ({ ruleRegistry, context }) => { - const ruleRegistryClient = await ruleRegistry.createScopedRuleRegistryClient({ - context, - alertsClient: context.alerting.getAlertsClient(), - }); - - if (!ruleRegistryClient) { - throw Boom.failedDependency(); - } + handler: async ({ ruleDataClient }) => { + const reader = ruleDataClient.getReader({ namespace: 'observability' }); - return ruleRegistryClient.getDynamicIndexPattern(); + return reader.getDynamicIndexPattern(); }, }); diff --git a/x-pack/plugins/observability/server/routes/types.ts b/x-pack/plugins/observability/server/routes/types.ts index 0588bf8df2292..1fa7229c6cf62 100644 --- a/x-pack/plugins/observability/server/routes/types.ts +++ b/x-pack/plugins/observability/server/routes/types.ts @@ -12,7 +12,7 @@ import type { ServerRouteRepository, } from '@kbn/server-route-repository'; import { CoreSetup, CoreStart, KibanaRequest, Logger } from 'kibana/server'; -import { ObservabilityRuleRegistry } from '../plugin'; +import { RuleDataClient } from '../../../rule_registry/server'; import { ObservabilityServerRouteRepository } from './get_global_observability_server_route_repository'; import { ObservabilityRequestHandlerContext } from '../types'; @@ -24,7 +24,7 @@ export interface ObservabilityRouteHandlerResources { start: () => Promise; setup: CoreSetup; }; - ruleRegistry: ObservabilityRuleRegistry; + ruleDataClient: RuleDataClient; request: KibanaRequest; context: ObservabilityRequestHandlerContext; logger: Logger; diff --git a/x-pack/plugins/observability/server/types.ts b/x-pack/plugins/observability/server/types.ts index 81b32b3f8db7b..da13e60804a60 100644 --- a/x-pack/plugins/observability/server/types.ts +++ b/x-pack/plugins/observability/server/types.ts @@ -7,9 +7,7 @@ import type { IRouter, RequestHandlerContext } from 'src/core/server'; import type { AlertingApiRequestHandlerContext } from '../../alerting/server'; -import type { ScopedRuleRegistryClient, FieldMapOf } from '../../rule_registry/server'; import type { LicensingApiRequestHandlerContext } from '../../licensing/server'; -import type { ObservabilityRuleRegistry } from './plugin'; export type { ObservabilityRouteCreateOptions, @@ -31,7 +29,3 @@ export interface ObservabilityRequestHandlerContext extends RequestHandlerContex * @internal */ export type ObservabilityPluginRouter = IRouter; - -export type ObservabilityRuleRegistryClient = ScopedRuleRegistryClient< - FieldMapOf ->; diff --git a/x-pack/plugins/rule_registry/README.md b/x-pack/plugins/rule_registry/README.md index 2c8f534a63d6b..cfbde612b45a6 100644 --- a/x-pack/plugins/rule_registry/README.md +++ b/x-pack/plugins/rule_registry/README.md @@ -2,62 +2,129 @@ The rule registry plugin aims to make it easy for rule type producers to have their rules produce the data that they need to build rich experiences on top of a unified experience, without the risk of mapping conflicts. -A rule registry creates a template, an ILM policy, and an alias. The template mappings can be configured. It also injects a client scoped to these indices. +The plugin installs default component templates and a default lifecycle policy that rule type producers can use to create index templates. -It also supports inheritance, which means that producers can create a registry specific to their solution or rule type, and specify additional mappings to be used. +It also exposes a rule data client that will create or update the index stream that rules will write data to. It will not do so on plugin setup or start, but only when data is written. -The rule registry plugin creates a root rule registry, with the mappings defined needed to create a unified experience. Rule type producers can use the plugin to access the root rule registry, and create their own registry that branches off of the root rule registry. The rule registry client sees data from its own registry, and all registries that branches off of it. It does not see data from its parents. +## Configuration -## Enabling writing - -Set +By default, these indices will be prefixed with `.alerts`. To change this, for instance to support legacy multitenancy, set the following configuration option: ```yaml -xpack.ruleRegistry.unsafe.write.enabled: true +xpack.ruleRegistry.index: '.kibana-alerts' ``` -in your Kibana configuration to allow the Rule Registry to write events to the alert indices. +To disable writing entirely: + +```yaml +xpack.ruleRegistry.write.enabled: false +``` -## Creating a rule registry +## Setting up the index template -To create a rule registry, producers should add the `ruleRegistry` plugin to their dependencies. They can then use the `ruleRegistry.create` method to create a child registry, with the additional mappings that should be used by specifying `fieldMap`: +On plugin setup, rule type producers can create the index template as follows: ```ts -const observabilityRegistry = plugins.ruleRegistry.create({ - name: 'observability', - fieldMap: { - ...pickWithPatterns(ecsFieldMap, 'host.name', 'service.name'), - }, -}); -``` +// get the FQN of the component template. All assets are prefixed with the configured `index` value, which is `.alerts` by default. -`fieldMap` is a key-value map of field names and mapping options: +const componentTemplateName = plugins.ruleRegistry.getFullAssetName( + 'apm-mappings' +); -```ts -{ - '@timestamp': { - type: 'date', - array: false, - required: true, - } +// if write is disabled, don't install these templates +if (!plugins.ruleRegistry.isWriteEnabled()) { + return; } -``` -ECS mappings are generated via a script in the rule registry plugin directory. These mappings are available in x-pack/plugins/rule_registry/server/generated/ecs_field_map.ts. - -To pick many fields, you can use `pickWithPatterns`, which supports wildcards with full type support. +// create or update the component template that should be used +await plugins.ruleRegistry.createOrUpdateComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + // mappingFromFieldMap is a utility function that will generate an + // ES mapping from a field map object. You can also define a literal + // mapping. + mappings: mappingFromFieldMap({ + [SERVICE_NAME]: { + type: 'keyword', + }, + [SERVICE_ENVIRONMENT]: { + type: 'keyword', + }, + [TRANSACTION_TYPE]: { + type: 'keyword', + }, + [PROCESSOR_EVENT]: { + type: 'keyword', + }, + }), + }, + }, +}); -If a registry is created, it will initialise as soon as the core services needed become available. It will create a (versioned) template, alias, and ILM policy, but only if these do not exist yet. +// Install the index template, that is composed of the component template +// defined above, and others. It is important that the technical component +// template is included. This will ensure functional compatibility across +// rule types, for a future scenario where a user will want to "point" the +// data from a rule to a different index. +await plugins.ruleRegistry.createOrUpdateIndexTemplate({ + name: plugins.ruleRegistry.getFullAssetName('apm-index-template'), + body: { + index_patterns: [ + plugins.ruleRegistry.getFullAssetName('observability-apm*'), + ], + composed_of: [ + // Technical component template, required + plugins.ruleRegistry.getFullAssetName( + TECHNICAL_COMPONENT_TEMPLATE_NAME + ), + componentTemplateName, + ], + }, +}); -## Rule registry client +// Finally, create the rule data client that can be injected into rule type +// executors and API endpoints +const ruleDataClient = new RuleDataClient({ + alias: plugins.ruleRegistry.getFullAssetName('observability-apm'), + getClusterClient: async () => { + const coreStart = await getCoreStart(); + return coreStart.elasticsearch.client.asInternalUser; + }, + ready, +}); -The rule registry client can either be injected in the executor, or created in the scope of a request. It exposes a `search` method and a `bulkIndex` method. When `search` is called, it first gets all the rules the current user has access to, and adds these ids to the search request that it executes. This means that the user can only see data from rules they have access to. +// to start writing data, call `getWriter().bulk()`. It supports a `namespace` +// property as well, that for instance can be used to write data to a space-specific +// index. +await ruleDataClient.getWriter().bulk({ + body: eventsToIndex.flatMap((event) => [{ index: {} }, event]), +}); -Both `search` and `bulkIndex` are fully typed, in the sense that they reflect the mappings defined for the registry. +// to read data, simply call ruleDataClient.getReader().search: +const response = await ruleDataClient.getReader().search({ + body: { + query: { + }, + size: 100, + fields: ['*'], + collapse: { + field: ALERT_UUID, + }, + sort: { + '@timestamp': 'desc', + }, + }, + allow_no_indices: true, +}); +``` ## Schema -The following fields are available in the root rule registry: +The following fields are defined in the technical field component template and should always be used: - `@timestamp`: the ISO timestamp of the alert event. For the lifecycle rule type helper, it is always the value of `startedAt` that is injected by the Kibana alerting framework. - `event.kind`: signal (for the changeable alert document), state (for the state changes of the alert, e.g. when it opens, recovers, or changes in severity), or metric (individual evaluations that might be related to an alert). @@ -67,7 +134,7 @@ The following fields are available in the root rule registry: - `rule.uuid`: the saved objects id of the rule. - `rule.name`: the name of the rule (as specified by the user). - `rule.category`: the name of the rule type (as defined by the rule type producer) -- `kibana.rac.producer`: the producer of the rule type. Usually a Kibana plugin. e.g., `APM`. +- `kibana.rac.alert.producer`: the producer of the rule type. Usually a Kibana plugin. e.g., `APM`. - `kibana.rac.alert.id`: the id of the alert, that is unique within the context of the rule execution it was created in. E.g., for a rule that monitors latency for all services in all environments, this might be `opbeans-java:production`. - `kibana.rac.alert.uuid`: the unique identifier for the alert during its lifespan. If an alert recovers (or closes), this identifier is re-generated when it is opened again. - `kibana.rac.alert.status`: the status of the alert. Can be `open` or `closed`. @@ -76,5 +143,5 @@ The following fields are available in the root rule registry: - `kibana.rac.alert.duration.us`: the duration of the alert, in microseconds. This is always the difference between either the current time, or the time when the alert recovered. - `kibana.rac.alert.severity.level`: the severity of the alert, as a keyword (e.g. critical). - `kibana.rac.alert.severity.value`: the severity of the alert, as a numerical value, which allows sorting. - -This list is not final - just a start. Field names might change or moved to a scoped registry. If we implement log and sequence based rule types the list of fields will grow. If a rule type needs additional fields, the recommendation would be to have the field in its own registry first (or in its producer’s registry), and if usage is more broadly adopted, it can be moved to the root registry. +- `kibana.rac.alert.evaluation.value`: The measured (numerical value). +- `kibana.rac.alert.threshold.value`: The threshold that was defined (or, in case of multiple thresholds, the one that was exceeded). diff --git a/x-pack/plugins/rule_registry/common/assets.ts b/x-pack/plugins/rule_registry/common/assets.ts new file mode 100644 index 0000000000000..1a5b14c605ea5 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/assets.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const TECHNICAL_COMPONENT_TEMPLATE_NAME = `technical-mappings`; +export const ECS_COMPONENT_TEMPLATE_NAME = `ecs-mappings`; +export const DEFAULT_ILM_POLICY_ID = 'ilm-policy'; diff --git a/x-pack/plugins/rule_registry/common/assets/component_templates/ecs_component_template.ts b/x-pack/plugins/rule_registry/common/assets/component_templates/ecs_component_template.ts new file mode 100644 index 0000000000000..7acbe0bc1227b --- /dev/null +++ b/x-pack/plugins/rule_registry/common/assets/component_templates/ecs_component_template.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { merge } from 'lodash'; +import { mappingFromFieldMap } from '../../mapping_from_field_map'; +import { ClusterPutComponentTemplateBody } from '../../types'; +import { ecsFieldMap } from '../field_maps/ecs_field_map'; +import { technicalRuleFieldMap } from '../field_maps/technical_rule_field_map'; + +export const ecsComponentTemplate: ClusterPutComponentTemplateBody = { + template: { + settings: { + number_of_shards: 1, + }, + mappings: merge( + {}, + mappingFromFieldMap(ecsFieldMap), + mappingFromFieldMap(technicalRuleFieldMap) + ), + }, +}; diff --git a/x-pack/plugins/rule_registry/common/assets/component_templates/technical_component_template.ts b/x-pack/plugins/rule_registry/common/assets/component_templates/technical_component_template.ts new file mode 100644 index 0000000000000..cc096faba387e --- /dev/null +++ b/x-pack/plugins/rule_registry/common/assets/component_templates/technical_component_template.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mappingFromFieldMap } from '../../mapping_from_field_map'; +import { ClusterPutComponentTemplateBody } from '../../types'; +import { technicalRuleFieldMap } from '../field_maps/technical_rule_field_map'; + +export const technicalComponentTemplate: ClusterPutComponentTemplateBody = { + template: { + settings: { + number_of_shards: 1, + }, + mappings: mappingFromFieldMap(technicalRuleFieldMap), + }, +}; diff --git a/x-pack/plugins/rule_registry/common/field_map/ecs_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts similarity index 100% rename from x-pack/plugins/rule_registry/common/field_map/ecs_field_map.ts rename to x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts new file mode 100644 index 0000000000000..a946e9523548c --- /dev/null +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { pickWithPatterns } from '../../../common/pick_with_patterns'; +import { + ALERT_DURATION, + ALERT_END, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_ID, + ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY_VALUE, + ALERT_START, + ALERT_STATUS, + ALERT_UUID, + EVENT_ACTION, + EVENT_KIND, + PRODUCER, + RULE_CATEGORY, + RULE_ID, + RULE_NAME, + RULE_UUID, + TAGS, + TIMESTAMP, +} from '../../../common/technical_rule_data_field_names'; +import { ecsFieldMap } from './ecs_field_map'; + +export const technicalRuleFieldMap = { + ...pickWithPatterns( + ecsFieldMap, + TIMESTAMP, + EVENT_KIND, + EVENT_ACTION, + RULE_UUID, + RULE_ID, + RULE_NAME, + RULE_CATEGORY, + TAGS + ), + [PRODUCER]: { type: 'keyword' }, + [ALERT_UUID]: { type: 'keyword' }, + [ALERT_ID]: { type: 'keyword' }, + [ALERT_START]: { type: 'date' }, + [ALERT_END]: { type: 'date' }, + [ALERT_DURATION]: { type: 'long' }, + [ALERT_SEVERITY_LEVEL]: { type: 'keyword' }, + [ALERT_SEVERITY_VALUE]: { type: 'long' }, + [ALERT_STATUS]: { type: 'keyword' }, + [ALERT_EVALUATION_THRESHOLD]: { type: 'scaled_float', scaling_factor: 100 }, + [ALERT_EVALUATION_VALUE]: { type: 'scaled_float', scaling_factor: 100 }, +} as const; + +export type TechnicalRuleFieldMaps = typeof technicalRuleFieldMap; diff --git a/x-pack/plugins/rule_registry/common/assets/index_templates/base_index_template.ts b/x-pack/plugins/rule_registry/common/assets/index_templates/base_index_template.ts new file mode 100644 index 0000000000000..ee2e45640c149 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/assets/index_templates/base_index_template.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const baseIndexTemplate = { + template: { + settings: { + number_of_shards: 1, + number_of_replicas: 0, + }, + }, +}; diff --git a/x-pack/plugins/rule_registry/server/rule_registry/defaults/ilm_policy.ts b/x-pack/plugins/rule_registry/common/assets/lifecycle_policies/default_lifecycle_policy.ts similarity index 86% rename from x-pack/plugins/rule_registry/server/rule_registry/defaults/ilm_policy.ts rename to x-pack/plugins/rule_registry/common/assets/lifecycle_policies/default_lifecycle_policy.ts index c80f7e772f308..f207087f7aa19 100644 --- a/x-pack/plugins/rule_registry/server/rule_registry/defaults/ilm_policy.ts +++ b/x-pack/plugins/rule_registry/common/assets/lifecycle_policies/default_lifecycle_policy.ts @@ -5,9 +5,7 @@ * 2.0. */ -import { ILMPolicy } from '../types'; - -export const defaultIlmPolicy: ILMPolicy = { +export const defaultLifecyclePolicy = { policy: { phases: { hot: { diff --git a/x-pack/plugins/rule_registry/common/field_map/base_rule_field_map.ts b/x-pack/plugins/rule_registry/common/field_map/base_rule_field_map.ts deleted file mode 100644 index 22a74212d2ce0..0000000000000 --- a/x-pack/plugins/rule_registry/common/field_map/base_rule_field_map.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { ecsFieldMap } from './ecs_field_map'; -import { pickWithPatterns } from '../pick_with_patterns'; - -export const baseRuleFieldMap = { - ...pickWithPatterns( - ecsFieldMap, - '@timestamp', - 'event.kind', - 'event.action', - 'rule.uuid', - 'rule.id', - 'rule.name', - 'rule.category', - 'tags' - ), - 'kibana.rac.producer': { type: 'keyword' }, - 'kibana.rac.alert.uuid': { type: 'keyword' }, - 'kibana.rac.alert.id': { type: 'keyword' }, - 'kibana.rac.alert.start': { type: 'date' }, - 'kibana.rac.alert.end': { type: 'date' }, - 'kibana.rac.alert.duration.us': { type: 'long' }, - 'kibana.rac.alert.severity.level': { type: 'keyword' }, - 'kibana.rac.alert.severity.value': { type: 'long' }, - 'kibana.rac.alert.status': { type: 'keyword' }, -} as const; - -export type BaseRuleFieldMap = typeof baseRuleFieldMap; diff --git a/x-pack/plugins/rule_registry/common/field_map/index.ts b/x-pack/plugins/rule_registry/common/field_map/index.ts index 8db5c2738439b..fac8575b8af48 100644 --- a/x-pack/plugins/rule_registry/common/field_map/index.ts +++ b/x-pack/plugins/rule_registry/common/field_map/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -export * from './base_rule_field_map'; -export * from './ecs_field_map'; export * from './merge_field_maps'; export * from './runtime_type_from_fieldmap'; export * from './types'; diff --git a/x-pack/plugins/rule_registry/common/index.ts b/x-pack/plugins/rule_registry/common/index.ts index b614feebc974a..5d36cd8cad7be 100644 --- a/x-pack/plugins/rule_registry/common/index.ts +++ b/x-pack/plugins/rule_registry/common/index.ts @@ -4,5 +4,4 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -export * from './field_map'; -export * from './pick_with_patterns'; +export { parseTechnicalFields } from './parse_technical_fields'; diff --git a/x-pack/plugins/rule_registry/server/rule_registry/field_map/mapping_from_field_map.ts b/x-pack/plugins/rule_registry/common/mapping_from_field_map.ts similarity index 79% rename from x-pack/plugins/rule_registry/server/rule_registry/field_map/mapping_from_field_map.ts rename to x-pack/plugins/rule_registry/common/mapping_from_field_map.ts index f1d7126906431..17eb5ae8967af 100644 --- a/x-pack/plugins/rule_registry/server/rule_registry/field_map/mapping_from_field_map.ts +++ b/x-pack/plugins/rule_registry/common/mapping_from_field_map.ts @@ -5,11 +5,11 @@ * 2.0. */ +import { TypeMapping } from '@elastic/elasticsearch/api/types'; import { set } from '@elastic/safer-lodash-set'; -import { FieldMap } from '../../../common'; -import { Mappings } from '../types'; +import { FieldMap } from './field_map/types'; -export function mappingFromFieldMap(fieldMap: FieldMap): Mappings { +export function mappingFromFieldMap(fieldMap: FieldMap): TypeMapping { const mappings = { dynamic: 'strict' as const, properties: {}, diff --git a/x-pack/plugins/rule_registry/common/parse_technical_fields.ts b/x-pack/plugins/rule_registry/common/parse_technical_fields.ts new file mode 100644 index 0000000000000..9d92c657468a3 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/parse_technical_fields.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 { isLeft } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; +import { technicalRuleFieldMap } from './assets/field_maps/technical_rule_field_map'; +import { runtimeTypeFromFieldMap } from './field_map'; + +const technicalFieldRuntimeType = runtimeTypeFromFieldMap(technicalRuleFieldMap); + +export const parseTechnicalFields = (input: unknown) => { + const validate = technicalFieldRuntimeType.decode(input); + + if (isLeft(validate)) { + throw new Error(PathReporter.report(validate).join('\n')); + } + + return technicalFieldRuntimeType.encode(validate.right); +}; + +export type ParsedTechnicalFields = ReturnType; diff --git a/x-pack/plugins/canvas/public/components/autocomplete/index.js b/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts similarity index 79% rename from x-pack/plugins/canvas/public/components/autocomplete/index.js rename to x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts index a5572c26d04f5..5c954a31e79ac 100644 --- a/x-pack/plugins/canvas/public/components/autocomplete/index.js +++ b/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { Autocomplete } from './autocomplete'; +export * from '@kbn/rule-data-utils/target/technical_field_names'; diff --git a/x-pack/plugins/rule_registry/common/types.ts b/x-pack/plugins/rule_registry/common/types.ts new file mode 100644 index 0000000000000..299d2c300ab49 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/types.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 { estypes } from '@elastic/elasticsearch'; + +export type PutIndexTemplateRequest = estypes.PutIndexTemplateRequest & { + body?: { composed_of?: string[] }; +}; + +export interface ClusterPutComponentTemplateBody { + template: { + settings: { + number_of_shards: number; + }; + mappings: estypes.TypeMapping; + }; +} diff --git a/x-pack/plugins/rule_registry/kibana.json b/x-pack/plugins/rule_registry/kibana.json index ec2b366f739e6..7e3f8bf6afb72 100644 --- a/x-pack/plugins/rule_registry/kibana.json +++ b/x-pack/plugins/rule_registry/kibana.json @@ -10,6 +10,5 @@ "alerting", "triggersActionsUi" ], - "server": true, - "ui": true + "server": true } diff --git a/x-pack/plugins/rule_registry/public/index.ts b/x-pack/plugins/rule_registry/public/index.ts deleted file mode 100644 index 59697261ff20b..0000000000000 --- a/x-pack/plugins/rule_registry/public/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { PluginInitializerContext } from 'kibana/public'; -import { Plugin } from './plugin'; - -export type { RuleRegistryPublicPluginSetupContract } from './plugin'; -export { RuleRegistry } from './rule_registry'; -export type { IRuleRegistry, RuleType } from './rule_registry/types'; - -export const plugin = (context: PluginInitializerContext) => { - return new Plugin(context); -}; diff --git a/x-pack/plugins/rule_registry/public/plugin.ts b/x-pack/plugins/rule_registry/public/plugin.ts deleted file mode 100644 index 7f0bceefb6797..0000000000000 --- a/x-pack/plugins/rule_registry/public/plugin.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { - CoreSetup, - CoreStart, - Plugin as PluginClass, - PluginInitializerContext, -} from '../../../../src/core/public'; -import type { - PluginSetupContract as AlertingPluginPublicSetupContract, - PluginStartContract as AlertingPluginPublicStartContract, -} from '../../alerting/public'; -import type { - TriggersAndActionsUIPublicPluginSetup, - TriggersAndActionsUIPublicPluginStart, -} from '../../triggers_actions_ui/public'; -import type { BaseRuleFieldMap } from '../common'; -import { RuleRegistry } from './rule_registry'; - -interface RuleRegistrySetupPlugins { - alerting: AlertingPluginPublicSetupContract; - triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; -} - -interface RuleRegistryStartPlugins { - alerting: AlertingPluginPublicStartContract; - triggersActionsUi: TriggersAndActionsUIPublicPluginStart; -} - -export type RuleRegistryPublicPluginSetupContract = ReturnType; - -export class Plugin - implements PluginClass { - constructor(context: PluginInitializerContext) {} - - public setup(core: CoreSetup, plugins: RuleRegistrySetupPlugins) { - const rootRegistry = new RuleRegistry({ - fieldMap: {} as BaseRuleFieldMap, - alertTypeRegistry: plugins.triggersActionsUi.alertTypeRegistry, - }); - return { - registry: rootRegistry, - }; - } - - start(core: CoreStart, plugins: RuleRegistryStartPlugins) { - return { - registerType: plugins.triggersActionsUi.alertTypeRegistry, - }; - } -} diff --git a/x-pack/plugins/rule_registry/public/rule_registry/index.ts b/x-pack/plugins/rule_registry/public/rule_registry/index.ts deleted file mode 100644 index ea47fe2e26aad..0000000000000 --- a/x-pack/plugins/rule_registry/public/rule_registry/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { BaseRuleFieldMap } from '../../common'; -import type { RuleType, CreateRuleRegistry, RuleRegistryConstructorOptions } from './types'; - -export class RuleRegistry { - protected types: TRuleType[] = []; - - constructor(private readonly options: RuleRegistryConstructorOptions) {} - - getTypes(): TRuleType[] { - return this.types; - } - - getTypeByRuleId(id: string): TRuleType | undefined { - return this.types.find((type) => type.id === id); - } - - registerType(type: TRuleType) { - this.types.push(type); - if (this.options.parent) { - this.options.parent.registerType(type); - } else { - this.options.alertTypeRegistry.register(type); - } - } - - create: CreateRuleRegistry = ({ fieldMap, ctor }) => { - const createOptions = { - fieldMap: { - ...this.options.fieldMap, - ...fieldMap, - }, - alertTypeRegistry: this.options.alertTypeRegistry, - parent: this, - }; - - const registry = ctor ? new ctor(createOptions) : new RuleRegistry(createOptions); - - return registry as any; - }; -} diff --git a/x-pack/plugins/rule_registry/public/rule_registry/types.ts b/x-pack/plugins/rule_registry/public/rule_registry/types.ts deleted file mode 100644 index 7c186385ebd35..0000000000000 --- a/x-pack/plugins/rule_registry/public/rule_registry/types.ts +++ /dev/null @@ -1,63 +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 { AlertTypeRegistryContract } from '../../../triggers_actions_ui/public'; -import type { BaseRuleFieldMap, FieldMap } from '../../common'; - -export interface RuleRegistryConstructorOptions { - fieldMap: TFieldMap; - alertTypeRegistry: AlertTypeRegistryContract; - parent?: IRuleRegistry; -} - -export type RuleType = Parameters[0]; - -export type RegisterRuleType< - TFieldMap extends BaseRuleFieldMap, - TAdditionalRegisterOptions = {} -> = (type: RuleType & TAdditionalRegisterOptions) => void; - -export type RuleRegistryExtensions = Record< - T, - (...args: any[]) => any ->; - -export type CreateRuleRegistry< - TFieldMap extends BaseRuleFieldMap, - TRuleType extends RuleType, - TInstanceType = undefined -> = < - TNextFieldMap extends FieldMap, - TRuleRegistryInstance extends IRuleRegistry< - TFieldMap & TNextFieldMap, - any - > = TInstanceType extends IRuleRegistry - ? TInstanceType - : IRuleRegistry ->(options: { - fieldMap: TNextFieldMap; - ctor?: new ( - options: RuleRegistryConstructorOptions - ) => TRuleRegistryInstance; -}) => TRuleRegistryInstance; - -export interface IRuleRegistry< - TFieldMap extends BaseRuleFieldMap, - TRuleType extends RuleType, - TInstanceType = undefined -> { - create: CreateRuleRegistry; - registerType(type: TRuleType): void; - getTypeByRuleId(ruleId: string): TRuleType; - getTypes(): TRuleType[]; -} - -export type FieldMapOfRuleRegistry = TRuleRegistry extends IRuleRegistry< - infer TFieldMap, - any -> - ? TFieldMap - : never; diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index 9fd1408fcdb21..b51ba3e10f91a 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -9,21 +9,23 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { RuleRegistryPlugin } from './plugin'; -export { RuleRegistryPluginSetupContract } from './plugin'; -export { createLifecycleRuleTypeFactory } from './rule_registry/rule_type_helpers/create_lifecycle_rule_type_factory'; -export { FieldMapOf } from './types'; -export { ScopedRuleRegistryClient } from './rule_registry/create_scoped_rule_registry_client/types'; +export type { RuleRegistryPluginSetupContract, RuleRegistryPluginStartContract } from './plugin'; +export { RuleDataClient } from './rule_data_client'; +export { IRuleDataClient } from './rule_data_client/types'; +export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data'; +export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory'; export const config = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), - unsafe: schema.object({ - write: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), + write: schema.object({ + enabled: schema.boolean({ defaultValue: true }), }), + index: schema.string({ defaultValue: '.alerts' }), }), }; -export type RuleRegistryConfig = TypeOf; +export type RuleRegistryPluginConfig = TypeOf; export const plugin = (initContext: PluginInitializerContext) => new RuleRegistryPlugin(initContext); diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 09df47c40a394..3c645f98f5c71 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -6,44 +6,44 @@ */ import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; -import { PluginSetupContract as AlertingPluginSetupContract } from '../../alerting/server'; -import { RuleRegistry } from './rule_registry'; -import { defaultIlmPolicy } from './rule_registry/defaults/ilm_policy'; -import { BaseRuleFieldMap, baseRuleFieldMap } from '../common'; -import { RuleRegistryConfig } from '.'; +import { RuleDataPluginService } from './rule_data_plugin_service'; +import { RuleRegistryPluginConfig } from '.'; -export type RuleRegistryPluginSetupContract = RuleRegistry; +export type RuleRegistryPluginSetupContract = RuleDataPluginService; +export type RuleRegistryPluginStartContract = void; export class RuleRegistryPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; } - public setup( - core: CoreSetup, - plugins: { alerting: AlertingPluginSetupContract } - ): RuleRegistryPluginSetupContract { - const globalConfig = this.initContext.config.legacy.get(); - const config = this.initContext.config.get(); + public setup(core: CoreSetup): RuleRegistryPluginSetupContract { + const config = this.initContext.config.get(); const logger = this.initContext.logger.get(); - const rootRegistry = new RuleRegistry({ - coreSetup: core, - ilmPolicy: defaultIlmPolicy, - fieldMap: baseRuleFieldMap, - kibanaIndex: globalConfig.kibana.index, - name: 'alerts', - kibanaVersion: this.initContext.env.packageInfo.version, - logger: logger.get('root'), - alertingPluginSetupContract: plugins.alerting, - writeEnabled: config.unsafe.write.enabled, + const service = new RuleDataPluginService({ + logger, + isWriteEnabled: config.write.enabled, + index: config.index, + getClusterClient: async () => { + const [coreStart] = await core.getStartServices(); + + return coreStart.elasticsearch.client.asInternalUser; + }, + }); + + service.init().catch((originalError) => { + const error = new Error('Failed installing assets'); + // @ts-ignore + error.stack = originalError.stack; + logger.error(error); }); - return rootRegistry; + return service; } - public start() {} + public start(): RuleRegistryPluginStartContract {} public stop() {} } diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/index.ts b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts new file mode 100644 index 0000000000000..135c870f20727 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_client/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { TypeMapping } from '@elastic/elasticsearch/api/types'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { IndexPatternsFetcher } from '../../../../../src/plugins/data/server'; +import { + IRuleDataClient, + RuleDataClientConstructorOptions, + RuleDataReader, + RuleDataWriter, +} from './types'; + +function getNamespacedAlias(options: { alias: string; namespace?: string }) { + return [options.alias, options.namespace].filter(Boolean).join('-'); +} + +export class RuleDataClient implements IRuleDataClient { + constructor(private readonly options: RuleDataClientConstructorOptions) {} + + private async getClusterClient() { + await this.options.ready(); + return await this.options.getClusterClient(); + } + + getReader(options: { namespace?: string } = {}): RuleDataReader { + const index = `${[this.options.alias, options.namespace].filter(Boolean).join('-')}*`; + + return { + search: async (request) => { + const clusterClient = await this.getClusterClient(); + + const { body } = (await clusterClient.search({ + ...request, + index, + })) as { body: any }; + + return body; + }, + getDynamicIndexPattern: async () => { + const clusterClient = await this.getClusterClient(); + const indexPatternsFetcher = new IndexPatternsFetcher(clusterClient); + + const fields = await indexPatternsFetcher.getFieldsForWildcard({ + pattern: index, + }); + + return { + fields, + timeFieldName: '@timestamp', + title: index, + }; + }, + }; + } + + getWriter(options: { namespace?: string } = {}): RuleDataWriter { + const { namespace } = options; + const alias = getNamespacedAlias({ alias: this.options.alias, namespace }); + return { + bulk: async (request) => { + const clusterClient = await this.getClusterClient(); + + const requestWithDefaultParameters = { + ...request, + require_alias: true, + index: alias, + }; + + return clusterClient.bulk(requestWithDefaultParameters).then((response) => { + if (response.body.errors) { + if ( + response.body.items.length === 1 && + response.body.items[0]?.index?.error?.type === 'index_not_found_exception' + ) { + return this.createOrUpdateWriteTarget({ namespace }).then(() => { + return clusterClient.bulk(requestWithDefaultParameters); + }); + } + const error = new ResponseError(response); + throw error; + } + return response; + }); + }, + }; + } + + async createOrUpdateWriteTarget({ namespace }: { namespace?: string }) { + const alias = getNamespacedAlias({ alias: this.options.alias, namespace }); + + const clusterClient = await this.getClusterClient(); + + const { body: aliasExists } = await clusterClient.indices.existsAlias({ + name: alias, + }); + + const concreteIndexName = `${alias}-000001`; + + if (!aliasExists) { + try { + await clusterClient.indices.create({ + index: concreteIndexName, + body: { + aliases: { + [alias]: { + is_write_index: true, + }, + }, + }, + }); + } catch (err) { + // something might have created the index already, that sounds OK + if (err?.meta?.body?.type !== 'resource_already_exists_exception') { + throw err; + } + } + } + + const { body: simulateResponse } = await clusterClient.transport.request({ + method: 'POST', + path: `/_index_template/_simulate_index/${concreteIndexName}`, + }); + + const mappings: TypeMapping = simulateResponse.template.mappings; + + await clusterClient.indices.putMapping({ index: `${alias}*`, body: mappings }); + } +} diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts new file mode 100644 index 0000000000000..348fca6a58188 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ApiResponse } from '@elastic/elasticsearch'; +import { BulkRequest, BulkResponse } from '@elastic/elasticsearch/api/types'; +import { ElasticsearchClient } from 'kibana/server'; +import { FieldDescriptor } from 'src/plugins/data/server'; +import { ESSearchRequest, ESSearchResponse } from 'typings/elasticsearch'; +import { TechnicalRuleDataFieldName } from '../../common/technical_rule_data_field_names'; + +export interface RuleDataReader { + search( + request: TSearchRequest + ): Promise< + ESSearchResponse>, TSearchRequest> + >; + getDynamicIndexPattern( + target?: string + ): Promise<{ + title: string; + timeFieldName: string; + fields: FieldDescriptor[]; + }>; +} + +export interface RuleDataWriter { + bulk(request: BulkRequest): Promise>; +} + +export interface IRuleDataClient { + getReader(options?: { namespace?: string }): RuleDataReader; + getWriter(options?: { namespace?: string }): RuleDataWriter; + createOrUpdateWriteTarget(options: { namespace?: string }): Promise; +} + +export interface RuleDataClientConstructorOptions { + getClusterClient: () => Promise; + ready: () => Promise; + alias: string; +} diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts new file mode 100644 index 0000000000000..159e9b8152597 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ClusterPutComponentTemplate } from '@elastic/elasticsearch/api/requestParams'; +import { estypes } from '@elastic/elasticsearch'; +import { ElasticsearchClient, Logger } from 'kibana/server'; +import { technicalComponentTemplate } from '../../common/assets/component_templates/technical_component_template'; +import { + DEFAULT_ILM_POLICY_ID, + ECS_COMPONENT_TEMPLATE_NAME, + TECHNICAL_COMPONENT_TEMPLATE_NAME, +} from '../../common/assets'; +import { ecsComponentTemplate } from '../../common/assets/component_templates/ecs_component_template'; +import { defaultLifecyclePolicy } from '../../common/assets/lifecycle_policies/default_lifecycle_policy'; +import { ClusterPutComponentTemplateBody, PutIndexTemplateRequest } from '../../common/types'; + +const BOOTSTRAP_TIMEOUT = 60000; + +interface RuleDataPluginServiceConstructorOptions { + getClusterClient: () => Promise; + logger: Logger; + isWriteEnabled: boolean; + index: string; +} + +function createSignal() { + let resolver: () => void; + + let ready: boolean = false; + + const promise = new Promise((resolve) => { + resolver = resolve; + }); + + function wait(): Promise { + return promise.then(() => { + ready = true; + }); + } + + function complete() { + resolver(); + } + + return { wait, complete, isReady: () => ready }; +} + +export class RuleDataPluginService { + signal = createSignal(); + + constructor(private readonly options: RuleDataPluginServiceConstructorOptions) {} + + private assertWriteEnabled() { + if (!this.isWriteEnabled) { + throw new Error('Write operations are disabled'); + } + } + + private async getClusterClient() { + return await this.options.getClusterClient(); + } + + async init() { + if (!this.isWriteEnabled) { + this.options.logger.info('Write is disabled, not installing assets'); + this.signal.complete(); + return; + } + + this.options.logger.info(`Installing assets in namespace ${this.getFullAssetName()}`); + + await this._createOrUpdateLifecyclePolicy({ + policy: this.getFullAssetName(DEFAULT_ILM_POLICY_ID), + body: defaultLifecyclePolicy, + }); + + await this._createOrUpdateComponentTemplate({ + name: this.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME), + body: technicalComponentTemplate, + }); + + await this._createOrUpdateComponentTemplate({ + name: this.getFullAssetName(ECS_COMPONENT_TEMPLATE_NAME), + body: ecsComponentTemplate, + }); + + this.options.logger.info(`Installed all assets`); + + this.signal.complete(); + } + + private async _createOrUpdateComponentTemplate( + template: ClusterPutComponentTemplate + ) { + this.assertWriteEnabled(); + + const clusterClient = await this.getClusterClient(); + this.options.logger.debug(`Installing component template ${template.name}`); + return clusterClient.cluster.putComponentTemplate(template); + } + + private async _createOrUpdateIndexTemplate(template: PutIndexTemplateRequest) { + this.assertWriteEnabled(); + + const clusterClient = await this.getClusterClient(); + this.options.logger.debug(`Installing index template ${template.name}`); + return clusterClient.indices.putIndexTemplate(template); + } + + private async _createOrUpdateLifecyclePolicy(policy: estypes.PutLifecycleRequest) { + this.assertWriteEnabled(); + const clusterClient = await this.getClusterClient(); + + this.options.logger.debug(`Installing lifecycle policy ${policy.policy}`); + return clusterClient.ilm.putLifecycle(policy); + } + + async createOrUpdateComponentTemplate( + template: ClusterPutComponentTemplate + ) { + await this.wait(); + return this._createOrUpdateComponentTemplate(template); + } + + async createOrUpdateIndexTemplate(template: PutIndexTemplateRequest) { + await this.wait(); + return this._createOrUpdateIndexTemplate(template); + } + + async createOrUpdateLifecyclePolicy(policy: estypes.PutLifecycleRequest) { + await this.wait(); + return this._createOrUpdateLifecyclePolicy(policy); + } + + isReady() { + return this.signal.isReady(); + } + + wait() { + return Promise.race([ + this.signal.wait(), + new Promise((resolve, reject) => { + setTimeout(reject, BOOTSTRAP_TIMEOUT); + }), + ]); + } + + isWriteEnabled(): boolean { + return this.options.isWriteEnabled; + } + + getFullAssetName(assetName?: string) { + return [this.options.index, assetName].filter(Boolean).join('-'); + } +} diff --git a/x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/index.ts b/x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/index.ts deleted file mode 100644 index 0d7735380b640..0000000000000 --- a/x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/index.ts +++ /dev/null @@ -1,179 +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 { Either, isLeft, isRight } from 'fp-ts/lib/Either'; -import { Errors } from 'io-ts'; -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { Logger } from 'kibana/server'; -import { IScopedClusterClient as ScopedClusterClient } from 'src/core/server'; -import { castArray, compact } from 'lodash'; -import { ESSearchRequest } from 'typings/elasticsearch'; -import { IndexPatternsFetcher } from '../../../../../../src/plugins/data/server'; -import { ClusterClientAdapter } from '../../../../event_log/server'; -import { TypeOfFieldMap } from '../../../common'; -import { ScopedRuleRegistryClient, EventsOf } from './types'; -import { BaseRuleFieldMap } from '../../../common'; -import { RuleRegistry } from '..'; - -const createPathReporterError = (either: Either) => { - const error = new Error(`Failed to validate alert event`); - error.stack += '\n' + PathReporter.report(either).join('\n'); - return error; -}; - -export function createScopedRuleRegistryClient({ - ruleUuids, - scopedClusterClient, - clusterClientAdapter, - indexAliasName, - indexTarget, - logger, - registry, - ruleData, -}: { - ruleUuids: string[]; - scopedClusterClient: ScopedClusterClient; - clusterClientAdapter: ClusterClientAdapter<{ - body: TypeOfFieldMap; - index: string; - }>; - indexAliasName: string; - indexTarget: string; - logger: Logger; - registry: RuleRegistry; - ruleData?: { - rule: { - id: string; - uuid: string; - category: string; - name: string; - }; - producer: string; - tags: string[]; - }; -}): ScopedRuleRegistryClient { - const fieldmapType = registry.getFieldMapType(); - - const defaults = ruleData - ? { - 'rule.uuid': ruleData.rule.uuid, - 'rule.id': ruleData.rule.id, - 'rule.name': ruleData.rule.name, - 'rule.category': ruleData.rule.category, - 'kibana.rac.producer': ruleData.producer, - tags: ruleData.tags, - } - : {}; - - const client: ScopedRuleRegistryClient = { - search: async (searchRequest) => { - const fields = [ - 'rule.id', - ...(searchRequest.body?.fields ? castArray(searchRequest.body.fields) : []), - ]; - - const response = await scopedClusterClient.asInternalUser.search({ - ...searchRequest, - index: indexTarget, - body: { - ...searchRequest.body, - query: { - bool: { - filter: [ - { terms: { 'rule.uuid': ruleUuids } }, - ...compact([searchRequest.body?.query]), - ], - }, - }, - fields, - }, - }); - - return { - body: response.body as any, - events: compact( - response.body.hits.hits.map((hit) => { - const ruleTypeId: string = hit.fields!['rule.id'][0]; - - const registryOfType = registry.getRegistryByRuleTypeId(ruleTypeId); - - if (ruleTypeId && !registryOfType) { - logger.warn( - `Could not find type ${ruleTypeId} in registry, decoding with default type` - ); - } - - const type = registryOfType?.getFieldMapType() ?? fieldmapType; - - const validation = type.decode(hit.fields); - if (isLeft(validation)) { - const error = createPathReporterError(validation); - logger.error(error); - return undefined; - } - return type.encode(validation.right); - }) - ) as EventsOf, - }; - }, - getDynamicIndexPattern: async () => { - const indexPatternsFetcher = new IndexPatternsFetcher(scopedClusterClient.asInternalUser); - - const fields = await indexPatternsFetcher.getFieldsForWildcard({ - pattern: indexTarget, - }); - - return { - fields, - timeFieldName: '@timestamp', - title: indexTarget, - }; - }, - index: (doc) => { - const validation = fieldmapType.decode({ - ...doc, - ...defaults, - }); - - if (isLeft(validation)) { - throw createPathReporterError(validation); - } - - clusterClientAdapter.indexDocument({ - body: validation.right, - index: indexAliasName, - }); - }, - bulkIndex: (docs) => { - const validations = docs.map((doc) => { - return fieldmapType.decode({ - ...doc, - ...defaults, - }); - }); - - const errors = compact( - validations.map((validation) => - isLeft(validation) ? createPathReporterError(validation) : null - ) - ); - - errors.forEach((error) => { - logger.error(error); - }); - - const operations = compact( - validations.map((validation) => (isRight(validation) ? validation.right : null)) - ).map((doc) => ({ body: doc, index: indexAliasName })); - - return clusterClientAdapter.indexDocuments(operations); - }, - }; - - // @ts-expect-error: We can't use ScopedRuleRegistryClient - // when creating the client, due to #41693 which will be fixed in 4.2 - return client; -} diff --git a/x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/types.ts b/x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/types.ts deleted file mode 100644 index f7b2394fe3a32..0000000000000 --- a/x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/types.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FieldDescriptor } from 'src/plugins/data/server'; -import { ESSearchRequest, ESSearchResponse } from 'typings/elasticsearch'; -import { - PatternsUnionOf, - PickWithPatterns, - OutputOfFieldMap, - BaseRuleFieldMap, -} from '../../../common'; - -export type PrepopulatedRuleEventFields = keyof Pick< - BaseRuleFieldMap, - 'rule.uuid' | 'rule.id' | 'rule.name' | 'rule.category' | 'kibana.rac.producer' ->; - -type FieldsOf = - | Array<{ field: PatternsUnionOf } | PatternsUnionOf> - | PatternsUnionOf; - -type Fields = Array<{ field: TPattern } | TPattern> | TPattern; - -type FieldsESSearchRequest = ESSearchRequest & { - body?: { fields: FieldsOf }; -}; - -export type EventsOf< - TFieldsESSearchRequest extends ESSearchRequest, - TFieldMap extends BaseRuleFieldMap -> = TFieldsESSearchRequest extends { body: { fields: infer TFields } } - ? TFields extends Fields - ? Array>> - : never - : never; - -export interface ScopedRuleRegistryClient { - search>( - request: TSearchRequest - ): Promise<{ - body: ESSearchResponse; - events: EventsOf; - }>; - getDynamicIndexPattern(): Promise<{ - title: string; - timeFieldName: string; - fields: FieldDescriptor[]; - }>; - index(doc: Omit, PrepopulatedRuleEventFields>): void; - bulkIndex( - doc: Array, PrepopulatedRuleEventFields>> - ): Promise; -} diff --git a/x-pack/plugins/rule_registry/server/rule_registry/index.ts b/x-pack/plugins/rule_registry/server/rule_registry/index.ts deleted file mode 100644 index bbc381f60a809..0000000000000 --- a/x-pack/plugins/rule_registry/server/rule_registry/index.ts +++ /dev/null @@ -1,328 +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 { CoreSetup, Logger, RequestHandlerContext } from 'kibana/server'; -import { inspect } from 'util'; -import { AlertsClient } from '../../../alerting/server'; -import { SpacesServiceStart } from '../../../spaces/server'; -import { - ActionVariable, - AlertInstanceState, - AlertTypeParams, - AlertTypeState, -} from '../../../alerting/common'; -import { createReadySignal, ClusterClientAdapter } from '../../../event_log/server'; -import { ILMPolicy } from './types'; -import { RuleParams, RuleType } from '../types'; -import { - mergeFieldMaps, - TypeOfFieldMap, - FieldMap, - FieldMapType, - BaseRuleFieldMap, - runtimeTypeFromFieldMap, -} from '../../common'; -import { mappingFromFieldMap } from './field_map/mapping_from_field_map'; -import { PluginSetupContract as AlertingPluginSetupContract } from '../../../alerting/server'; -import { createScopedRuleRegistryClient } from './create_scoped_rule_registry_client'; -import { ScopedRuleRegistryClient } from './create_scoped_rule_registry_client/types'; - -interface RuleRegistryOptions { - kibanaIndex: string; - kibanaVersion: string; - name: string; - logger: Logger; - coreSetup: CoreSetup; - spacesStart?: SpacesServiceStart; - fieldMap: TFieldMap; - ilmPolicy: ILMPolicy; - alertingPluginSetupContract: AlertingPluginSetupContract; - writeEnabled: boolean; -} - -export class RuleRegistry { - private readonly esAdapter: ClusterClientAdapter<{ - body: TypeOfFieldMap; - index: string; - }>; - private readonly children: Array> = []; - private readonly types: Array> = []; - - private readonly fieldmapType: FieldMapType; - - constructor(private readonly options: RuleRegistryOptions) { - const { logger, coreSetup } = options; - - this.fieldmapType = runtimeTypeFromFieldMap(options.fieldMap); - - const { wait, signal } = createReadySignal(); - - this.esAdapter = new ClusterClientAdapter<{ - body: TypeOfFieldMap; - index: string; - }>({ - wait, - elasticsearchClientPromise: coreSetup - .getStartServices() - .then(([{ elasticsearch }]) => elasticsearch.client.asInternalUser), - logger: logger.get('esAdapter'), - }); - - if (this.options.writeEnabled) { - this.initialize() - .then(() => { - this.options.logger.debug('Bootstrapped alerts index'); - signal(true); - }) - .catch((err) => { - logger.error(inspect(err, { depth: null })); - signal(false); - }); - } else { - logger.debug('Write disabled, indices are not being bootstrapped'); - } - } - - private getEsNames() { - const base = [this.options.kibanaIndex, this.options.name]; - const indexTarget = `${base.join('-')}*`; - const indexAliasName = [...base, this.options.kibanaVersion.toLowerCase()].join('-'); - const policyName = [...base, 'policy'].join('-'); - - return { - indexAliasName, - indexTarget, - policyName, - }; - } - - private async initialize() { - const { indexAliasName, policyName } = this.getEsNames(); - - const ilmPolicyExists = await this.esAdapter.doesIlmPolicyExist(policyName); - - if (!ilmPolicyExists) { - await this.esAdapter.createIlmPolicy( - policyName, - (this.options.ilmPolicy as unknown) as Record - ); - } - - const templateExists = await this.esAdapter.doesIndexTemplateExist(indexAliasName); - - const mappings = mappingFromFieldMap(this.options.fieldMap); - - const esClient = (await this.options.coreSetup.getStartServices())[0].elasticsearch.client - .asInternalUser; - - if (!templateExists) { - await this.esAdapter.createIndexTemplate(indexAliasName, { - index_patterns: [`${indexAliasName}-*`], - settings: { - number_of_shards: 1, - auto_expand_replicas: '0-1', - 'index.lifecycle.name': policyName, - 'index.lifecycle.rollover_alias': indexAliasName, - 'sort.field': '@timestamp', - 'sort.order': 'desc', - }, - mappings, - }); - } else { - await esClient.indices.putTemplate({ - name: indexAliasName, - body: { - index_patterns: [`${indexAliasName}-*`], - mappings, - }, - create: false, - }); - } - - const aliasExists = await this.esAdapter.doesAliasExist(indexAliasName); - - if (!aliasExists) { - await this.esAdapter.createIndex(`${indexAliasName}-000001`, { - aliases: { - [indexAliasName]: { - is_write_index: true, - }, - }, - }); - } else { - const { body: aliases } = (await esClient.indices.getAlias({ - index: indexAliasName, - })) as { body: Record }> }; - - const writeIndex = Object.entries(aliases).find( - ([indexName, alias]) => alias.aliases[indexAliasName]?.is_write_index === true - )![0]; - - const { body: fieldsInWriteIndex } = await esClient.fieldCaps({ - index: writeIndex, - fields: '*', - }); - - const fieldsNotOrDifferentInIndex = Object.entries(this.options.fieldMap).filter( - ([fieldName, descriptor]) => { - return ( - !fieldsInWriteIndex.fields[fieldName] || - !fieldsInWriteIndex.fields[fieldName][descriptor.type] - ); - } - ); - - if (fieldsNotOrDifferentInIndex.length > 0) { - this.options.logger.debug( - `Some fields were not found in write index mapping: ${Object.keys( - Object.fromEntries(fieldsNotOrDifferentInIndex) - ).join(',')}` - ); - this.options.logger.info(`Updating index mapping due to new fields`); - - await esClient.indices.putMapping({ - index: indexAliasName, - body: mappings, - }); - } - } - } - - getFieldMapType() { - return this.fieldmapType; - } - - getRuleTypeById(ruleTypeId: string) { - return this.types.find((type) => type.id === ruleTypeId); - } - - getRegistryByRuleTypeId(ruleTypeId: string): RuleRegistry | undefined { - if (this.getRuleTypeById(ruleTypeId)) { - return this; - } - - return this.children.find((child) => child.getRegistryByRuleTypeId(ruleTypeId)); - } - - async createScopedRuleRegistryClient({ - context, - alertsClient, - }: { - context: RequestHandlerContext; - alertsClient: AlertsClient; - }): Promise | undefined> { - if (!this.options.writeEnabled) { - return undefined; - } - const { indexAliasName, indexTarget } = this.getEsNames(); - - const frameworkAlerts = ( - await alertsClient.find({ - options: { - perPage: 1000, - }, - }) - ).data; - - return createScopedRuleRegistryClient({ - ruleUuids: frameworkAlerts.map((frameworkAlert) => frameworkAlert.id), - scopedClusterClient: context.core.elasticsearch.client, - clusterClientAdapter: this.esAdapter, - registry: this, - indexAliasName, - indexTarget, - logger: this.options.logger, - }); - } - - registerType( - type: RuleType - ) { - const logger = this.options.logger.get(type.id); - - const { indexAliasName, indexTarget } = this.getEsNames(); - - this.types.push(type); - - this.options.alertingPluginSetupContract.registerType< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - { [key in TActionVariable['name']]: any }, - string - >({ - ...type, - executor: async (executorOptions) => { - const { services, alertId, name, tags } = executorOptions; - - const rule = { - id: type.id, - uuid: alertId, - category: type.name, - name, - }; - - const producer = type.producer; - - return type.executor({ - ...executorOptions, - rule, - producer, - services: { - ...services, - logger, - ...(this.options.writeEnabled - ? { - scopedRuleRegistryClient: createScopedRuleRegistryClient({ - scopedClusterClient: services.scopedClusterClient, - ruleUuids: [rule.uuid], - clusterClientAdapter: this.esAdapter, - registry: this, - indexAliasName, - indexTarget, - ruleData: { - producer, - rule, - tags, - }, - logger: this.options.logger, - }), - } - : {}), - }, - }); - }, - }); - } - - create({ - name, - fieldMap, - ilmPolicy, - }: { - name: string; - fieldMap: TNextFieldMap; - ilmPolicy?: ILMPolicy; - }): RuleRegistry { - const mergedFieldMap = fieldMap - ? mergeFieldMaps(this.options.fieldMap, fieldMap) - : this.options.fieldMap; - - const child = new RuleRegistry({ - ...this.options, - logger: this.options.logger.get(name), - name: [this.options.name, name].filter(Boolean).join('-'), - fieldMap: mergedFieldMap, - ...(ilmPolicy ? { ilmPolicy } : {}), - }); - - this.children.push(child); - - // @ts-expect-error could be instantiated with a different subtype of constraint - return child; - } -} diff --git a/x-pack/plugins/rule_registry/server/rule_registry/rule_type_helpers/create_lifecycle_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/rule_registry/rule_type_helpers/create_lifecycle_rule_type_factory.ts deleted file mode 100644 index 65eaf0964cfca..0000000000000 --- a/x-pack/plugins/rule_registry/server/rule_registry/rule_type_helpers/create_lifecycle_rule_type_factory.ts +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import * as t from 'io-ts'; -import { isLeft } from 'fp-ts/lib/Either'; -import v4 from 'uuid/v4'; -import { Mutable } from 'utility-types'; -import { AlertInstance } from '../../../../alerting/server'; -import { ActionVariable, AlertInstanceState } from '../../../../alerting/common'; -import { RuleParams, RuleType } from '../../types'; -import { BaseRuleFieldMap, OutputOfFieldMap } from '../../../common'; -import { PrepopulatedRuleEventFields } from '../create_scoped_rule_registry_client/types'; -import { RuleRegistry } from '..'; - -type UserDefinedAlertFields = Omit< - OutputOfFieldMap, - PrepopulatedRuleEventFields | 'kibana.rac.alert.id' | 'kibana.rac.alert.uuid' | '@timestamp' ->; - -type LifecycleAlertService< - TFieldMap extends BaseRuleFieldMap, - TActionVariable extends ActionVariable -> = (alert: { - id: string; - fields: UserDefinedAlertFields; -}) => AlertInstance; - -type CreateLifecycleRuleType = < - TRuleParams extends RuleParams, - TActionVariable extends ActionVariable ->( - type: RuleType< - TFieldMap, - TRuleParams, - TActionVariable, - { alertWithLifecycle: LifecycleAlertService } - > -) => RuleType; - -const trackedAlertStateRt = t.type({ - alertId: t.string, - alertUuid: t.string, - started: t.string, -}); - -const wrappedStateRt = t.type({ - wrapped: t.record(t.string, t.unknown), - trackedAlerts: t.record(t.string, trackedAlertStateRt), -}); - -export function createLifecycleRuleTypeFactory< - TRuleRegistry extends RuleRegistry ->(): TRuleRegistry extends RuleRegistry - ? CreateLifecycleRuleType - : never; - -export function createLifecycleRuleTypeFactory(): CreateLifecycleRuleType { - return (type) => { - return { - ...type, - executor: async (options) => { - const { - services: { scopedRuleRegistryClient, alertInstanceFactory, logger }, - state: previousState, - rule, - } = options; - - const decodedState = wrappedStateRt.decode(previousState); - - const state = isLeft(decodedState) - ? { - wrapped: previousState, - trackedAlerts: {}, - } - : decodedState.right; - - const currentAlerts: Record< - string, - UserDefinedAlertFields & { 'kibana.rac.alert.id': string } - > = {}; - - const timestamp = options.startedAt.toISOString(); - - const nextWrappedState = await type.executor({ - ...options, - state: state.wrapped, - services: { - ...options.services, - alertWithLifecycle: ({ id, fields }) => { - currentAlerts[id] = { - ...fields, - 'kibana.rac.alert.id': id, - }; - return alertInstanceFactory(id); - }, - }, - }); - - const currentAlertIds = Object.keys(currentAlerts); - const trackedAlertIds = Object.keys(state.trackedAlerts); - const newAlertIds = currentAlertIds.filter((alertId) => !trackedAlertIds.includes(alertId)); - - const allAlertIds = [...new Set(currentAlertIds.concat(trackedAlertIds))]; - - const trackedAlertStatesOfRecovered = Object.values(state.trackedAlerts).filter( - (trackedAlertState) => !currentAlerts[trackedAlertState.alertId] - ); - - logger.debug( - `Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStatesOfRecovered.length} recovered)` - ); - - const alertsDataMap: Record> = { - ...currentAlerts, - }; - - if (scopedRuleRegistryClient && trackedAlertStatesOfRecovered.length) { - const { events } = await scopedRuleRegistryClient.search({ - body: { - query: { - bool: { - filter: [ - { - term: { - 'rule.uuid': rule.uuid, - }, - }, - { - terms: { - 'kibana.rac.alert.uuid': trackedAlertStatesOfRecovered.map( - (trackedAlertState) => trackedAlertState.alertUuid - ), - }, - }, - ], - }, - }, - size: trackedAlertStatesOfRecovered.length, - collapse: { - field: 'kibana.rac.alert.uuid', - }, - _source: false, - fields: ['*'], - sort: { - '@timestamp': 'desc' as const, - }, - }, - }); - - events.forEach((event) => { - const alertId = event['kibana.rac.alert.id']!; - alertsDataMap[alertId] = event; - }); - } - - const eventsToIndex: Array> = allAlertIds.map( - (alertId) => { - const alertData = alertsDataMap[alertId]; - - if (!alertData) { - logger.warn(`Could not find alert data for ${alertId}`); - } - - const event: Mutable> = { - ...alertData, - '@timestamp': timestamp, - 'event.kind': 'state', - 'kibana.rac.alert.id': alertId, - }; - - const isNew = !state.trackedAlerts[alertId]; - const isRecovered = !currentAlerts[alertId]; - const isActiveButNotNew = !isNew && !isRecovered; - const isActive = !isRecovered; - - const { alertUuid, started } = state.trackedAlerts[alertId] ?? { - alertUuid: v4(), - started: timestamp, - }; - - event['kibana.rac.alert.start'] = started; - event['kibana.rac.alert.uuid'] = alertUuid; - - if (isNew) { - event['event.action'] = 'open'; - } - - if (isRecovered) { - event['kibana.rac.alert.end'] = timestamp; - event['event.action'] = 'close'; - event['kibana.rac.alert.status'] = 'closed'; - } - - if (isActiveButNotNew) { - event['event.action'] = 'active'; - } - - if (isActive) { - event['kibana.rac.alert.status'] = 'open'; - } - - event['kibana.rac.alert.duration.us'] = - (options.startedAt.getTime() - new Date(event['kibana.rac.alert.start']!).getTime()) * - 1000; - - return event; - } - ); - - if (eventsToIndex.length && scopedRuleRegistryClient) { - await scopedRuleRegistryClient.bulkIndex(eventsToIndex); - } - - const nextTrackedAlerts = Object.fromEntries( - eventsToIndex - .filter((event) => event['kibana.rac.alert.status'] !== 'closed') - .map((event) => { - const alertId = event['kibana.rac.alert.id']!; - const alertUuid = event['kibana.rac.alert.uuid']!; - const started = new Date(event['kibana.rac.alert.start']!).toISOString(); - return [alertId, { alertId, alertUuid, started }]; - }) - ); - - return { - wrapped: nextWrappedState, - trackedAlerts: nextTrackedAlerts, - }; - }, - }; - }; -} diff --git a/x-pack/plugins/rule_registry/server/rule_registry/types.ts b/x-pack/plugins/rule_registry/server/rule_registry/types.ts deleted file mode 100644 index ec7293d1c1d4c..0000000000000 --- a/x-pack/plugins/rule_registry/server/rule_registry/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export interface Mappings { - dynamic: 'strict' | boolean; - properties: Record; -} - -enum ILMPolicyPhase { - hot = 'hot', - delete = 'delete', -} - -enum ILMPolicyAction { - rollover = 'rollover', - delete = 'delete', -} - -interface ILMActionOptions { - [ILMPolicyAction.rollover]: { - max_size: string; - max_age: string; - }; - [ILMPolicyAction.delete]: {}; -} - -export interface ILMPolicy { - policy: { - phases: Record< - ILMPolicyPhase, - { - actions: { - [key in keyof ILMActionOptions]?: ILMActionOptions[key]; - }; - } - >; - }; -} diff --git a/x-pack/plugins/rule_registry/server/types.ts b/x-pack/plugins/rule_registry/server/types.ts index dd54046365d98..959c05fd1334e 100644 --- a/x-pack/plugins/rule_registry/server/types.ts +++ b/x-pack/plugins/rule_registry/server/types.ts @@ -4,97 +4,37 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Type, TypeOf } from '@kbn/config-schema'; -import { Logger } from 'kibana/server'; + import { - ActionVariable, AlertInstanceContext, AlertInstanceState, AlertTypeParams, AlertTypeState, } from '../../alerting/common'; -import { ActionGroup, AlertExecutorOptions } from '../../alerting/server'; -import { RuleRegistry } from './rule_registry'; -import { ScopedRuleRegistryClient } from './rule_registry/create_scoped_rule_registry_client/types'; -import { BaseRuleFieldMap } from '../common'; - -export type RuleParams = Type; +import { AlertType } from '../../alerting/server'; -type TypeOfRuleParams = TypeOf; +type SimpleAlertType< + TParams extends AlertTypeParams = {}, + TAlertInstanceContext extends AlertInstanceContext = {} +> = AlertType; -type RuleExecutorServices< - TFieldMap extends BaseRuleFieldMap, - TActionVariable extends ActionVariable -> = AlertExecutorOptions< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - { [key in TActionVariable['name']]: any }, - string ->['services'] & { - logger: Logger; - scopedRuleRegistryClient?: ScopedRuleRegistryClient; -}; - -type PassthroughAlertExecutorOptions = Pick< - AlertExecutorOptions< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, - string - >, - 'previousStartedAt' | 'startedAt' | 'state' ->; - -type RuleExecutorFunction< - TFieldMap extends BaseRuleFieldMap, - TRuleParams extends RuleParams, - TActionVariable extends ActionVariable, - TAdditionalRuleExecutorServices extends Record +export type AlertTypeExecutor< + TParams extends AlertTypeParams = {}, + TAlertInstanceContext extends AlertInstanceContext = {}, + TServices extends Record = {} > = ( - options: PassthroughAlertExecutorOptions & { - services: RuleExecutorServices & TAdditionalRuleExecutorServices; - params: TypeOfRuleParams; - rule: { - id: string; - uuid: string; - name: string; - category: string; - }; - producer: string; + options: Parameters['executor']>[0] & { + services: TServices; } -) => Promise>; - -interface RuleTypeBase { - id: string; - name: string; - actionGroups: Array>; - defaultActionGroupId: string; - producer: string; - minimumLicenseRequired: 'basic' | 'gold' | 'trial'; -} - -export type RuleType< - TFieldMap extends BaseRuleFieldMap, - TRuleParams extends RuleParams, - TActionVariable extends ActionVariable, - TAdditionalRuleExecutorServices extends Record = {} -> = RuleTypeBase & { - validate: { - params: TRuleParams; - }; - actionVariables: { - context: TActionVariable[]; - }; - executor: RuleExecutorFunction< - TFieldMap, - TRuleParams, - TActionVariable, - TAdditionalRuleExecutorServices - >; +) => Promise; + +export type AlertTypeWithExecutor< + TParams extends AlertTypeParams = {}, + TAlertInstanceContext extends AlertInstanceContext = {}, + TServices extends Record = {} +> = Omit< + AlertType, + 'executor' +> & { + executor: AlertTypeExecutor; }; - -export type FieldMapOf< - TRuleRegistry extends RuleRegistry -> = TRuleRegistry extends RuleRegistry ? TFieldMap : never; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts new file mode 100644 index 0000000000000..b523dd6770b9f --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts @@ -0,0 +1,246 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { isLeft } from 'fp-ts/lib/Either'; +import * as t from 'io-ts'; +import { Mutable } from 'utility-types'; +import v4 from 'uuid/v4'; +import { AlertInstance } from '../../../alerting/server'; +import { RuleDataClient } from '..'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertTypeParams, +} from '../../../alerting/common'; +import { + ALERT_DURATION, + ALERT_END, + ALERT_ID, + ALERT_START, + ALERT_STATUS, + ALERT_UUID, + EVENT_ACTION, + EVENT_KIND, + RULE_UUID, + TIMESTAMP, +} from '../../common/technical_rule_data_field_names'; +import { AlertTypeWithExecutor } from '../types'; +import { ParsedTechnicalFields, parseTechnicalFields } from '../../common/parse_technical_fields'; +import { getRuleExecutorData } from './get_rule_executor_data'; + +type LifecycleAlertService> = (alert: { + id: string; + fields: Record; +}) => AlertInstance; + +const trackedAlertStateRt = t.type({ + alertId: t.string, + alertUuid: t.string, + started: t.string, +}); + +const wrappedStateRt = t.type({ + wrapped: t.record(t.string, t.unknown), + trackedAlerts: t.record(t.string, trackedAlertStateRt), +}); + +type CreateLifecycleRuleTypeFactory = (options: { + ruleDataClient: RuleDataClient; + logger: Logger; +}) => < + TParams extends AlertTypeParams, + TAlertInstanceContext extends AlertInstanceContext, + TServices extends { alertWithLifecycle: LifecycleAlertService } +>( + type: AlertTypeWithExecutor +) => AlertTypeWithExecutor; + +export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ + logger, + ruleDataClient, +}) => (type) => { + return { + ...type, + executor: async (options) => { + const { + services: { alertInstanceFactory }, + state: previousState, + } = options; + + const ruleExecutorData = getRuleExecutorData(type, options); + + const decodedState = wrappedStateRt.decode(previousState); + + const state = isLeft(decodedState) + ? { + wrapped: previousState, + trackedAlerts: {}, + } + : decodedState.right; + + const currentAlerts: Record = {}; + + const timestamp = options.startedAt.toISOString(); + + const nextWrappedState = await type.executor({ + ...options, + state: state.wrapped, + services: { + ...options.services, + alertWithLifecycle: ({ id, fields }) => { + currentAlerts[id] = { + ...fields, + [ALERT_ID]: id, + }; + return alertInstanceFactory(id); + }, + }, + }); + + const currentAlertIds = Object.keys(currentAlerts); + const trackedAlertIds = Object.keys(state.trackedAlerts); + const newAlertIds = currentAlertIds.filter((alertId) => !trackedAlertIds.includes(alertId)); + + const allAlertIds = [...new Set(currentAlertIds.concat(trackedAlertIds))]; + + const trackedAlertStatesOfRecovered = Object.values(state.trackedAlerts).filter( + (trackedAlertState) => !currentAlerts[trackedAlertState.alertId] + ); + + logger.debug( + `Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStatesOfRecovered.length} recovered)` + ); + + const alertsDataMap: Record< + string, + { + [ALERT_ID]: string; + } + > = { + ...currentAlerts, + }; + + if (trackedAlertStatesOfRecovered.length) { + const { hits } = await ruleDataClient.getReader().search({ + body: { + query: { + bool: { + filter: [ + { + term: { + [RULE_UUID]: ruleExecutorData[RULE_UUID], + }, + }, + { + terms: { + [ALERT_UUID]: trackedAlertStatesOfRecovered.map( + (trackedAlertState) => trackedAlertState.alertUuid + ), + }, + }, + ], + }, + }, + size: trackedAlertStatesOfRecovered.length, + collapse: { + field: ALERT_UUID, + }, + _source: false, + fields: [{ field: '*', include_unmapped: true }], + sort: { + [TIMESTAMP]: 'desc' as const, + }, + }, + allow_no_indices: true, + }); + + hits.hits.forEach((hit) => { + const fields = parseTechnicalFields(hit.fields); + const alertId = fields[ALERT_ID]!; + alertsDataMap[alertId] = { + ...fields, + [ALERT_ID]: alertId, + }; + }); + } + + const eventsToIndex = allAlertIds.map((alertId) => { + const alertData = alertsDataMap[alertId]; + + if (!alertData) { + logger.warn(`Could not find alert data for ${alertId}`); + } + + const event: Mutable = { + ...alertData, + ...ruleExecutorData, + [TIMESTAMP]: timestamp, + [EVENT_KIND]: 'state', + [ALERT_ID]: alertId, + }; + + const isNew = !state.trackedAlerts[alertId]; + const isRecovered = !currentAlerts[alertId]; + const isActiveButNotNew = !isNew && !isRecovered; + const isActive = !isRecovered; + + const { alertUuid, started } = state.trackedAlerts[alertId] ?? { + alertUuid: v4(), + started: timestamp, + }; + + event[ALERT_START] = started; + event[ALERT_UUID] = alertUuid; + + if (isNew) { + event[EVENT_ACTION] = 'open'; + } + + if (isRecovered) { + event[ALERT_END] = timestamp; + event[EVENT_ACTION] = 'close'; + event[ALERT_STATUS] = 'closed'; + } + + if (isActiveButNotNew) { + event[EVENT_ACTION] = 'active'; + } + + if (isActive) { + event[ALERT_STATUS] = 'open'; + } + + event[ALERT_DURATION] = + (options.startedAt.getTime() - new Date(event[ALERT_START]!).getTime()) * 1000; + + return event; + }); + + if (eventsToIndex.length) { + await ruleDataClient.getWriter().bulk({ + body: eventsToIndex.flatMap((event) => [{ index: {} }, event]), + }); + } + + const nextTrackedAlerts = Object.fromEntries( + eventsToIndex + .filter((event) => event[ALERT_STATUS] !== 'closed') + .map((event) => { + const alertId = event[ALERT_ID]!; + const alertUuid = event[ALERT_UUID]!; + const started = new Date(event[ALERT_START]!).toISOString(); + return [alertId, { alertId, alertUuid, started }]; + }) + ); + + return { + wrapped: nextWrappedState, + trackedAlerts: nextTrackedAlerts, + }; + }, + }; +}; diff --git a/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts b/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts new file mode 100644 index 0000000000000..1ea640add7b48 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.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 { + PRODUCER, + RULE_CATEGORY, + RULE_ID, + RULE_NAME, + RULE_UUID, + TAGS, +} from '../../common/technical_rule_data_field_names'; +import { AlertTypeExecutor, AlertTypeWithExecutor } from '../types'; + +export interface RuleExecutorData { + [RULE_CATEGORY]: string; + [RULE_ID]: string; + [RULE_UUID]: string; + [RULE_NAME]: string; + [PRODUCER]: string; + [TAGS]: string[]; +} + +export function getRuleExecutorData( + type: AlertTypeWithExecutor, + options: Parameters[0] +) { + return { + [RULE_ID]: type.id, + [RULE_UUID]: options.alertId, + [RULE_CATEGORY]: type.name, + [RULE_NAME]: options.name, + [TAGS]: options.tags, + [PRODUCER]: type.producer, + }; +} diff --git a/x-pack/plugins/rule_registry/server/utils/with_rule_data_client_factory.ts b/x-pack/plugins/rule_registry/server/utils/with_rule_data_client_factory.ts new file mode 100644 index 0000000000000..02ff6b10f74cf --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/with_rule_data_client_factory.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 { AlertInstanceContext, AlertTypeParams } from '../../../alerting/common'; +import { RuleDataClient } from '../rule_data_client'; +import { AlertTypeWithExecutor } from '../types'; + +export const withRuleDataClientFactory = (ruleDataClient: RuleDataClient) => < + TParams extends AlertTypeParams, + TAlertInstanceContext extends AlertInstanceContext, + TServices extends Record = {} +>( + type: AlertTypeWithExecutor< + TParams, + TAlertInstanceContext, + TServices & { ruleDataClient: RuleDataClient } + > +): AlertTypeWithExecutor< + TParams, + TAlertInstanceContext, + TServices & { ruleDataClient: RuleDataClient } +> => { + return { + ...type, + executor: (options) => { + return type.executor({ + ...options, + services: { + ...options.services, + ruleDataClient, + }, + }); + }, + }; +}; diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index 0ff04e4f731d0..6d808f2305c91 100644 --- a/x-pack/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/common/constants.ts @@ -57,3 +57,31 @@ export const NAME_REGEX = /^(?! )[a-zA-Z0-9 !"#$%&'()*+,\-./\\:;<=>?@\[\]^_`{|}~ * Maximum length of usernames and role names. */ export const MAX_NAME_LENGTH = 1024; + +/** + * Client session timeout is decreased by this number so that Kibana server can still access session + * content during logout request to properly clean user session up (invalidate access tokens, + * redirect to logout portal etc.). + */ +export const SESSION_GRACE_PERIOD_MS = 5 * 1000; + +/** + * Duration we'll normally display the warning toast + */ +export const SESSION_EXPIRATION_WARNING_MS = 5 * 60 * 1000; + +/** + * Current session info is checked this number of milliseconds before the warning toast shows. This + * will prevent the toast from being shown if the session has already been extended. + */ +export const SESSION_CHECK_MS = 1000; + +/** + * Session will be extended at most once this number of milliseconds while user activity is detected. + */ +export const SESSION_EXTENSION_THROTTLE_MS = 60 * 1000; + +/** + * Route to get session info and extend session expiration + */ +export const SESSION_ROUTE = '/internal/security/session'; diff --git a/x-pack/plugins/security/common/types.ts b/x-pack/plugins/security/common/types.ts index 94e4fef6517a6..690aced63b187 100644 --- a/x-pack/plugins/security/common/types.ts +++ b/x-pack/plugins/security/common/types.ts @@ -8,8 +8,7 @@ import type { AuthenticationProvider } from './model'; export interface SessionInfo { - now: number; - idleTimeoutExpiration: number | null; - lifespanExpiration: number | null; + expiresInMs: number | null; + canBeExtended: boolean; provider: AuthenticationProvider; } diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index 5df73f7f8ec4e..b8963ea5a76e3 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -143,7 +143,8 @@ function getProps({ rolesAPIClient.getRole.mockResolvedValue(role); const indexPatterns = dataPluginMock.createStartContract().indexPatterns; - indexPatterns.getTitles = jest.fn().mockResolvedValue(['foo*', 'bar*']); + // `undefined` titles can technically happen via import/export or other manual manipulation + indexPatterns.getTitles = jest.fn().mockResolvedValue(['foo*', 'bar*', undefined]); const indicesAPIClient = indicesAPIClientMock.create(); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index f810cd2079d16..0f49aaf48c394 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -125,7 +125,7 @@ function useIndexPatternsTitles( fatalErrors.add(err); throw err; }) - .then(setIndexPatternsTitles); + .then((titles) => setIndexPatternsTitles(titles.filter(Boolean))); }, [fatalErrors, indexPatterns, notifications]); return indexPatternsTitles; diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index c805d9f1caf79..7d7ab36f1dadb 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -27,13 +27,7 @@ import type { ConfigType } from './config'; import { ManagementService } from './management'; import { SecurityNavControlService } from './nav_control'; import { SecurityCheckupService } from './security_checkup'; -import type { ISessionTimeout } from './session'; -import { - SessionExpired, - SessionTimeout, - SessionTimeoutHttpInterceptor, - UnauthorizedResponseHttpInterceptor, -} from './session'; +import { SessionExpired, SessionTimeout, UnauthorizedResponseHttpInterceptor } from './session'; export interface PluginSetupDependencies { licensing: LicensingPluginSetup; @@ -58,7 +52,7 @@ export class SecurityPlugin PluginSetupDependencies, PluginStartDependencies > { - private sessionTimeout!: ISessionTimeout; + private sessionTimeout!: SessionTimeout; private readonly authenticationService = new AuthenticationService(); private readonly navControlService = new SecurityNavControlService(); private readonly securityLicenseService = new SecurityLicenseService(); @@ -84,7 +78,6 @@ export class SecurityPlugin const sessionExpired = new SessionExpired(logoutUrl, tenant); http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths)); this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); - http.intercept(new SessionTimeoutHttpInterceptor(this.sessionTimeout, anonymousPaths)); const { license } = this.securityLicenseService.setup({ license$: licensing.license$ }); diff --git a/x-pack/plugins/security/public/session/index.ts b/x-pack/plugins/security/public/session/index.ts index 30e73eacd9264..9679e05fd1bd9 100644 --- a/x-pack/plugins/security/public/session/index.ts +++ b/x-pack/plugins/security/public/session/index.ts @@ -6,6 +6,5 @@ */ export { SessionExpired } from './session_expired'; -export { SessionTimeout, ISessionTimeout } from './session_timeout'; -export { SessionTimeoutHttpInterceptor } from './session_timeout_http_interceptor'; +export { SessionTimeout } from './session_timeout'; export { UnauthorizedResponseHttpInterceptor } from './unauthorized_response_http_interceptor'; diff --git a/x-pack/plugins/security/public/session/session_expiration_toast.test.tsx b/x-pack/plugins/security/public/session/session_expiration_toast.test.tsx new file mode 100644 index 0000000000000..a78bed34b2088 --- /dev/null +++ b/x-pack/plugins/security/public/session/session_expiration_toast.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { of } from 'rxjs'; + +import { I18nProvider } from '@kbn/i18n/react'; + +import { createSessionExpirationToast, SessionExpirationToast } from './session_expiration_toast'; +import type { SessionState } from './session_timeout'; + +describe('createSessionExpirationToast', () => { + it('creates a toast', () => { + const sessionState$ = of({ + lastExtensionTime: Date.now(), + expiresInMs: 60 * 1000, + canBeExtended: true, + }); + const onExtend = jest.fn(); + const onClose = jest.fn(); + const toast = createSessionExpirationToast(sessionState$, onExtend, onClose); + + expect(toast).toEqual( + expect.objectContaining({ + color: 'warning', + iconType: 'clock', + onClose: expect.any(Function), + text: expect.any(Function), + title: expect.any(String), + toastLifeTimeMs: 2147483647, + }) + ); + }); +}); + +describe('SessionExpirationToast', () => { + it('renders session expiration time', () => { + const sessionState$ = of({ + lastExtensionTime: Date.now(), + expiresInMs: 60 * 1000, + canBeExtended: true, + }); + + const { getByText } = render( + + + + ); + getByText(/You will be logged out in [0-9]+ seconds/); + }); + + it('renders extend button if session can be extended', () => { + const sessionState$ = of({ + lastExtensionTime: Date.now(), + expiresInMs: 60 * 1000, + canBeExtended: true, + }); + const onExtend = jest.fn().mockReturnValue(new Promise(() => {})); + + const { getByRole } = render( + + + + ); + fireEvent.click(getByRole('button', { name: 'Stay logged in' })); + + expect(onExtend).toHaveBeenCalled(); + }); + + it('does not render extend button if session cannot be extended', () => { + const sessionState$ = of({ + lastExtensionTime: Date.now(), + expiresInMs: 60 * 1000, + canBeExtended: false, + }); + const onExtend = jest.fn(); + + const { queryByRole } = render( + + + + ); + expect(queryByRole('button', { name: 'Stay logged in' })).toBeNull(); + }); +}); diff --git a/x-pack/plugins/security/public/session/session_expiration_toast.tsx b/x-pack/plugins/security/public/session/session_expiration_toast.tsx new file mode 100644 index 0000000000000..ad7aa67b64b3f --- /dev/null +++ b/x-pack/plugins/security/public/session/session_expiration_toast.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React from 'react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import useObservable from 'react-use/lib/useObservable'; +import type { Observable } from 'rxjs'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; +import type { ToastInput } from 'src/core/public'; + +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { SESSION_GRACE_PERIOD_MS } from '../../common/constants'; +import type { SessionState } from './session_timeout'; + +export interface SessionExpirationToastProps { + sessionState$: Observable; + onExtend: () => Promise; +} + +export const SessionExpirationToast: FunctionComponent = ({ + sessionState$, + onExtend, +}) => { + const state = useObservable(sessionState$); + const [{ loading }, extend] = useAsyncFn(onExtend); + + if (!state || !state.expiresInMs) { + return null; + } + + const expirationWarning = ( + + ), + }} + /> + ); + + if (state.canBeExtended) { + return ( + <> + {expirationWarning} + + + + + + + + + + ); + } + + return expirationWarning; +}; + +export const createSessionExpirationToast = ( + sessionState$: Observable, + onExtend: () => Promise, + onClose: () => void +): ToastInput => { + return { + color: 'warning', + iconType: 'clock', + title: i18n.translate('xpack.security.sessionExpirationToast.title', { + defaultMessage: 'Session timeout', + }), + text: toMountPoint( + + ), + onClose, + toastLifeTimeMs: 0x7fffffff, // Toast is hidden based on observable so using maximum possible timeout + }; +}; diff --git a/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx b/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx deleted file mode 100644 index 48a3bb482948e..0000000000000 --- a/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { mountWithIntl } from '@kbn/test/jest'; - -import { SessionIdleTimeoutWarning } from './session_idle_timeout_warning'; - -describe('SessionIdleTimeoutWarning', () => { - it('fires its callback when the OK button is clicked', () => { - const handler = jest.fn(); - const wrapper = mountWithIntl( - - ); - - expect(handler).toBeCalledTimes(0); - wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); - expect(handler).toBeCalledTimes(1); - }); -}); diff --git a/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx b/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx deleted file mode 100644 index 47dce0aa9993e..0000000000000 --- a/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx +++ /dev/null @@ -1,67 +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 { EuiButton, EuiProgress } from '@elastic/eui'; -import React from 'react'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; -import type { ToastInput } from 'src/core/public'; - -import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; - -interface Props { - onRefreshSession: () => void; - timeout: number; -} - -export const SessionIdleTimeoutWarning = (props: Props) => { - return ( - <> - -

- - ), - }} - /> -

-
- - - -
- - ); -}; - -export const createToast = (toastLifeTimeMs: number, onRefreshSession: () => void): ToastInput => { - const timeout = toastLifeTimeMs + Date.now(); - return { - color: 'warning', - text: toMountPoint( - - ), - title: i18n.translate('xpack.security.components.sessionIdleTimeoutWarning.title', { - defaultMessage: 'Warning', - }), - iconType: 'clock', - toastLifeTimeMs, - }; -}; diff --git a/x-pack/plugins/security/public/session/session_lifespan_warning.tsx b/x-pack/plugins/security/public/session/session_lifespan_warning.tsx deleted file mode 100644 index e611c9aa2ac5f..0000000000000 --- a/x-pack/plugins/security/public/session/session_lifespan_warning.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 { EuiProgress } from '@elastic/eui'; -import React from 'react'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; -import type { ToastInput } from 'src/core/public'; - -import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; - -interface Props { - timeout: number; -} - -export const SessionLifespanWarning = (props: Props) => { - return ( - <> - -

- - ), - }} - /> -

- - ); -}; - -export const createToast = (toastLifeTimeMs: number): ToastInput => { - const timeout = toastLifeTimeMs + Date.now(); - return { - color: 'danger', - text: toMountPoint(), - title: i18n.translate('xpack.security.components.sessionLifespanWarning.title', { - defaultMessage: 'Warning', - }), - iconType: 'alert', - toastLifeTimeMs, - }; -}; diff --git a/x-pack/plugins/security/public/session/session_timeout.mock.ts b/x-pack/plugins/security/public/session/session_timeout.mock.ts index 47e16810f2c2b..15071b08ded8f 100644 --- a/x-pack/plugins/security/public/session/session_timeout.mock.ts +++ b/x-pack/plugins/security/public/session/session_timeout.mock.ts @@ -5,12 +5,13 @@ * 2.0. */ -import type { ISessionTimeout } from './session_timeout'; +import type { PublicMethodsOf } from '@kbn/utility-types'; + +import type { SessionTimeout } from './session_timeout'; export function createSessionTimeoutMock() { return { start: jest.fn(), stop: jest.fn(), - extend: jest.fn(), - } as jest.Mocked; + } as jest.Mocked>; } diff --git a/x-pack/plugins/security/public/session/session_timeout.test.ts b/x-pack/plugins/security/public/session/session_timeout.test.ts new file mode 100644 index 0000000000000..b6cd758354dfa --- /dev/null +++ b/x-pack/plugins/security/public/session/session_timeout.test.ts @@ -0,0 +1,403 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { BroadcastChannel } from 'broadcast-channel'; + +import type { ToastInputFields } from 'src/core/public'; +import { coreMock } from 'src/core/public/mocks'; + +import { + SESSION_CHECK_MS, + SESSION_EXPIRATION_WARNING_MS, + SESSION_EXTENSION_THROTTLE_MS, + SESSION_GRACE_PERIOD_MS, + SESSION_ROUTE, +} from '../../common/constants'; +import type { SessionInfo } from '../../common/types'; +import { createSessionExpiredMock } from './session_expired.mock'; +import type { SessionState } from './session_timeout'; +import { SessionTimeout, startTimer } from './session_timeout'; + +jest.mock('broadcast-channel'); + +jest.useFakeTimers(); + +jest.spyOn(window, 'addEventListener'); +jest.spyOn(window, 'removeEventListener'); + +jest.spyOn(document, 'addEventListener'); +jest.spyOn(document, 'removeEventListener'); + +const nowMock = jest.spyOn(Date, 'now'); +const visibilityStateMock = jest.spyOn(document, 'visibilityState', 'get'); + +function createSessionTimeout(expiresInMs: number | null = 60 * 60 * 1000, canBeExtended = true) { + const { notifications, http } = coreMock.createSetup(); + const toast = Symbol(); + notifications.toasts.add.mockReturnValue(toast as any); + const sessionExpired = createSessionExpiredMock(); + const tenant = 'test'; + const sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); + + http.fetch.mockResolvedValue({ + expiresInMs, + canBeExtended, + provider: { type: 'basic', name: 'basic1' }, + } as SessionInfo); + + return { sessionTimeout, sessionExpired, notifications, http }; +} + +describe('SessionTimeout', () => { + afterEach(async () => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + test(`does not initialize when starting an anonymous path`, async () => { + const { sessionTimeout, http } = createSessionTimeout(); + http.anonymousPaths.isAnonymous.mockReturnValue(true); + await sessionTimeout.start(); + expect(http.fetch).not.toHaveBeenCalled(); + }); + + it('fetches session info when starting', async () => { + const { sessionTimeout, http } = createSessionTimeout(); + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + expect(http.fetch).toHaveBeenCalledWith(SESSION_ROUTE, { + asSystemRequest: true, + method: 'GET', + }); + }); + + it('adds event handlers when starting', async () => { + const { sessionTimeout } = createSessionTimeout(); + + expect(window.addEventListener).not.toHaveBeenCalled(); + expect(document.addEventListener).not.toHaveBeenCalled(); + + await sessionTimeout.start(); + + expect(window.addEventListener).toHaveBeenCalledWith('mousemove', expect.any(Function)); + expect(window.addEventListener).toHaveBeenCalledWith('mousedown', expect.any(Function)); + expect(window.addEventListener).toHaveBeenCalledWith('wheel', expect.any(Function)); + expect(window.addEventListener).toHaveBeenCalledWith('touchstart', expect.any(Function)); + expect(window.addEventListener).toHaveBeenCalledWith('keydown', expect.any(Function)); + expect(document.addEventListener).toHaveBeenCalledWith( + 'visibilitychange', + expect.any(Function) + ); + }); + + it('removes event handlers when stopping', async () => { + const { sessionTimeout } = createSessionTimeout(); + + await sessionTimeout.start(); + + expect(window.removeEventListener).not.toHaveBeenCalled(); + expect(document.removeEventListener).not.toHaveBeenCalled(); + + sessionTimeout.stop(); + + expect(window.removeEventListener).toHaveBeenCalledWith('mousemove', expect.any(Function)); + expect(window.removeEventListener).toHaveBeenCalledWith('mousedown', expect.any(Function)); + expect(window.removeEventListener).toHaveBeenCalledWith('wheel', expect.any(Function)); + expect(window.removeEventListener).toHaveBeenCalledWith('touchstart', expect.any(Function)); + expect(window.removeEventListener).toHaveBeenCalledWith('keydown', expect.any(Function)); + expect(document.removeEventListener).toHaveBeenCalledWith( + 'visibilitychange', + expect.any(Function) + ); + }); + + it('clears timers when stopping', async () => { + const { sessionTimeout, sessionExpired } = createSessionTimeout(100); + await sessionTimeout.start(); + sessionTimeout.stop(); + + jest.advanceTimersByTime(200); + + expect(sessionExpired.logout).not.toHaveBeenCalled(); + }); + + it('extends session when detecting user activity', async () => { + const { sessionTimeout, http } = createSessionTimeout(); + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // Increment system time enough so that session extension gets triggered + nowMock.mockReturnValue(Date.now() + SESSION_EXTENSION_THROTTLE_MS + 10); + window.dispatchEvent(new Event('mousemove')); + + expect(http.fetch).toHaveBeenCalledTimes(2); + expect(http.fetch).toHaveBeenLastCalledWith( + SESSION_ROUTE, + expect.objectContaining({ asSystemRequest: false }) + ); + }); + + it('refetches session info before warning is displayed', async () => { + const { sessionTimeout, http } = createSessionTimeout(60 * 60 * 1000); + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime( + 60 * 60 * 1000 - SESSION_GRACE_PERIOD_MS - SESSION_EXPIRATION_WARNING_MS - SESSION_CHECK_MS + ); + + expect(http.fetch).toHaveBeenCalledTimes(2); + }); + + it('does not extend session if recently extended', async () => { + const { sessionTimeout, http } = createSessionTimeout(); + await sessionTimeout.start(); + + expect(http.fetch).toHaveBeenCalledTimes(1); + + window.dispatchEvent(new Event('mousemove')); + + expect(http.fetch).toHaveBeenCalledTimes(1); + }); + + it('marks HTTP requests as system requests when tab is not visible', async () => { + const { sessionTimeout, http } = createSessionTimeout(); + await sessionTimeout.start(); + + visibilityStateMock.mockReturnValue('hidden'); + document.dispatchEvent(new Event('visibilitychange')); + + const [{ request: handleHttpRequest }] = http.intercept.mock.calls[0]; + const fetchOptions = handleHttpRequest!({ path: '/test' }, {} as any); + expect(fetchOptions).toEqual({ path: '/test', asSystemRequest: true }); + }); + + it('does not mark HTTP requests to external URLs as system requests', async () => { + const { sessionTimeout, http } = createSessionTimeout(); + await sessionTimeout.start(); + + visibilityStateMock.mockReturnValue('hidden'); + document.dispatchEvent(new Event('visibilitychange')); + + const [{ request: handleHttpRequest }] = http.intercept.mock.calls[0]; + const fetchOptions = handleHttpRequest!({ path: 'http://elastic.co/' }, {} as any); + expect(fetchOptions).toBe(undefined); + }); + + it('marks HTTP requests as user requests when tab is visible', async () => { + const { sessionTimeout, http } = createSessionTimeout(); + await sessionTimeout.start(); + + visibilityStateMock.mockReturnValue('visible'); + document.dispatchEvent(new Event('visibilitychange')); + + const [{ request: handleHttpRequest }] = http.intercept.mock.calls[0]; + const fetchOptions = handleHttpRequest!({ path: '/test' }, {} as any); + expect(fetchOptions).toEqual({ path: '/test', asSystemRequest: false }); + }); + + it('resets timers when receiving message from other tabs', async () => { + const { sessionTimeout, sessionExpired } = createSessionTimeout(60 * 1000); + await sessionTimeout.start(); + + jest.advanceTimersByTime(30 * 1000); + + const [broadcastChannelMock] = jest.requireMock('broadcast-channel').BroadcastChannel.mock + .instances as [BroadcastChannel]; + + broadcastChannelMock.onmessage!({ + lastExtensionTime: Date.now(), + expiresInMs: 60 * 1000, + canBeExtended: true, + }); + + jest.advanceTimersByTime(30 * 1000); + + expect(sessionExpired.logout).not.toHaveBeenCalled(); + }); + + it('shows warning before session expires', async () => { + const { sessionTimeout, notifications } = createSessionTimeout(60 * 60 * 1000); + await sessionTimeout.start(); + + jest.advanceTimersByTime( + 60 * 60 * 1000 - SESSION_GRACE_PERIOD_MS - SESSION_EXPIRATION_WARNING_MS + ); + + expect(notifications.toasts.add).toHaveBeenCalledWith( + expect.objectContaining({ color: 'warning', iconType: 'clock' }) + ); + }); + + it('extends session when closing expiration warning', async () => { + const { sessionTimeout, notifications, http } = createSessionTimeout(60 * 60 * 1000); + await sessionTimeout.start(); + + expect(http.fetch).toHaveBeenCalledTimes(1); + expect(http.fetch).toHaveBeenLastCalledWith( + SESSION_ROUTE, + expect.objectContaining({ asSystemRequest: true }) + ); + + jest.runOnlyPendingTimers(); + + expect(http.fetch).toHaveBeenCalledTimes(2); + expect(http.fetch).toHaveBeenLastCalledWith( + SESSION_ROUTE, + expect.objectContaining({ asSystemRequest: true }) + ); + + const [toast] = notifications.toasts.add.mock.calls[0] as [ToastInputFields]; + + await toast.onClose!(); + + expect(http.fetch).toHaveBeenCalledTimes(3); + expect(http.fetch).toHaveBeenLastCalledWith( + SESSION_ROUTE, + expect.objectContaining({ asSystemRequest: false }) + ); + }); + + it('show warning 5 minutes before expiration if not previously dismissed', async () => { + const { sessionTimeout, notifications } = createSessionTimeout(null); + await sessionTimeout.start(); + + const expiresInMs = 10 * 60 * 1000; + const showWarningInMs = expiresInMs - SESSION_GRACE_PERIOD_MS - SESSION_EXPIRATION_WARNING_MS; + + // eslint-disable-next-line dot-notation + sessionTimeout['resetTimers']({ + lastExtensionTime: Date.now() + 1 * 60 * 1000, + expiresInMs, + canBeExtended: false, + }); + + jest.advanceTimersByTime(showWarningInMs); + + expect(notifications.toasts.add).toHaveBeenCalled(); + }); + + it('do not show warning again if previously dismissed', async () => { + const { sessionTimeout, notifications } = createSessionTimeout(null); + await sessionTimeout.start(); + + const expiresInMs = 10 * 60 * 1000; + const showWarningInMs = expiresInMs - SESSION_GRACE_PERIOD_MS - SESSION_EXPIRATION_WARNING_MS; + + // eslint-disable-next-line dot-notation + sessionTimeout['snoozedWarningState'] = { + lastExtensionTime: Date.now(), + expiresInMs, + canBeExtended: false, + }; + + // eslint-disable-next-line dot-notation + sessionTimeout['resetTimers']({ + lastExtensionTime: Date.now() + 1 * 60 * 1000, + expiresInMs, + canBeExtended: false, + }); + + // We would normally show the warning at this point in time. However, since the warning has been + // dismissed for 10 minutes we will only show it after 10 minutes have elapsed + jest.advanceTimersByTime(showWarningInMs); + expect(notifications.toasts.add).not.toHaveBeenCalled(); + + // Advance the timer further so that a total have 10 minutes would have passed. This is the + // expiration time of the warning that was dismissed. + jest.advanceTimersByTime(9 * 60 * 1000 - showWarningInMs); + expect(notifications.toasts.add).toHaveBeenCalled(); + }); + + it('hides warning if session gets extended', async () => { + const { sessionTimeout, notifications } = createSessionTimeout(60 * 60 * 1000); + await sessionTimeout.start(); + + jest.advanceTimersByTime( + 60 * 60 * 1000 - SESSION_GRACE_PERIOD_MS - SESSION_EXPIRATION_WARNING_MS + ); + + expect(notifications.toasts.add).toHaveBeenCalled(); + + // eslint-disable-next-line dot-notation + await sessionTimeout['fetchSessionInfo'](true); + + expect(notifications.toasts.remove).toHaveBeenCalled(); + }); + + it('logs user out slightly before session expires', async () => { + const { sessionTimeout, sessionExpired } = createSessionTimeout(60 * 60 * 1000); + await sessionTimeout.start(); + + jest.advanceTimersByTime(60 * 60 * 1000 - SESSION_GRACE_PERIOD_MS); + + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + it('logs user out immediately if session expiration is imminent', async () => { + const { sessionTimeout, sessionExpired } = createSessionTimeout(SESSION_GRACE_PERIOD_MS / 2); + await sessionTimeout.start(); + + jest.advanceTimersByTime(0); + + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + it('does not log user out if session does not expire', async () => { + const { sessionTimeout, sessionExpired } = createSessionTimeout(null); + await sessionTimeout.start(); + + jest.runOnlyPendingTimers(); + + expect(sessionExpired.logout).not.toHaveBeenCalled(); + }); + + it('does not log user out if session does not exist', async () => { + const { sessionTimeout, sessionExpired, http } = createSessionTimeout(); + + http.fetch.mockResolvedValue(''); // Session endpoint return 204 No content when session does not exist + await sessionTimeout.start(); + + jest.runOnlyPendingTimers(); + + expect(sessionExpired.logout).not.toHaveBeenCalled(); + }); +}); + +describe('startTimer', () => { + it('executes callback after time elapses', () => { + const callback = jest.fn(); + startTimer(callback, 100); + jest.advanceTimersByTime(100); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('executes callback after a very long time elapses', () => { + const callback = jest.fn(); + startTimer(callback, 0x7fffffff + 100); + jest.advanceTimersByTime(0x7fffffff + 100); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('does not executes callback if stopped before time elapses', () => { + const callback = jest.fn(); + const stop = startTimer(callback, 100); + jest.advanceTimersByTime(50); + stop(); + jest.advanceTimersByTime(50); + expect(callback).toHaveBeenCalledTimes(0); + }); + + it('does not executes callback if stopped before a very long time elapses', () => { + const callback = jest.fn(); + const stop = startTimer(callback, 0x7fffffff + 100); + jest.advanceTimersByTime(0x7fffffff + 50); + stop(); + jest.advanceTimersByTime(50); + expect(callback).toHaveBeenCalledTimes(0); + }); +}); diff --git a/x-pack/plugins/security/public/session/session_timeout.test.tsx b/x-pack/plugins/security/public/session/session_timeout.test.tsx deleted file mode 100644 index 1faa105691259..0000000000000 --- a/x-pack/plugins/security/public/session/session_timeout.test.tsx +++ /dev/null @@ -1,495 +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 BroadcastChannel from 'broadcast-channel'; - -import { mountWithIntl, nextTick } from '@kbn/test/jest'; -import { coreMock } from 'src/core/public/mocks'; - -import { createSessionExpiredMock } from './session_expired.mock'; -import { SessionTimeout } from './session_timeout'; - -jest.useFakeTimers(); - -const expectNoWarningToast = ( - notifications: ReturnType['notifications'] -) => { - expect(notifications.toasts.add).not.toHaveBeenCalled(); -}; - -const expectIdleTimeoutWarningToast = ( - notifications: ReturnType['notifications'], - toastLifeTimeMs: number = 60000 -) => { - expect(notifications.toasts.add).toHaveBeenCalledTimes(1); - expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot( - { - text: expect.any(Function), - }, - ` - Object { - "color": "warning", - "iconType": "clock", - "text": Any, - "title": "Warning", - "toastLifeTimeMs": ${toastLifeTimeMs}, - } - ` - ); -}; - -const expectLifespanWarningToast = ( - notifications: ReturnType['notifications'], - toastLifeTimeMs: number = 60000 -) => { - expect(notifications.toasts.add).toHaveBeenCalledTimes(1); - expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot( - { - text: expect.any(Function), - }, - ` - Object { - "color": "danger", - "iconType": "alert", - "text": Any, - "title": "Warning", - "toastLifeTimeMs": ${toastLifeTimeMs}, - } - ` - ); -}; - -const expectWarningToastHidden = ( - notifications: ReturnType['notifications'], - toast: symbol -) => { - expect(notifications.toasts.remove).toHaveBeenCalledTimes(1); - expect(notifications.toasts.remove).toHaveBeenCalledWith(toast); -}; - -describe('Session Timeout', () => { - const now = new Date().getTime(); - const defaultSessionInfo = { - now, - idleTimeoutExpiration: now + 2 * 60 * 1000, - lifespanExpiration: null, - provider: { type: 'basic', name: 'basic1' }, - }; - let notifications: ReturnType['notifications']; - let http: ReturnType['http']; - let sessionExpired: ReturnType; - let sessionTimeout: SessionTimeout; - const toast = Symbol(); - - beforeAll(() => { - BroadcastChannel.enforceOptions({ - type: 'simulate', - }); - Object.defineProperty(window, 'sessionStorage', { - value: { - setItem: jest.fn(() => null), - }, - writable: true, - }); - }); - - beforeEach(() => { - const setup = coreMock.createSetup(); - notifications = setup.notifications; - http = setup.http; - notifications.toasts.add.mockReturnValue(toast as any); - sessionExpired = createSessionExpiredMock(); - const tenant = ''; - sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); - - // default mocked response for checking session info - http.fetch.mockResolvedValue(defaultSessionInfo); - }); - - afterEach(async () => { - jest.clearAllMocks(); - jest.unmock('broadcast-channel'); - sessionTimeout.stop(); - }); - - afterAll(() => { - BroadcastChannel.enforceOptions(null); - delete (window as any).sessionStorage; - }); - - describe('Lifecycle', () => { - test(`starts and initializes on a non-anonymous path`, async () => { - sessionTimeout.start(); - await nextTick(); - // eslint-disable-next-line dot-notation - expect(sessionTimeout['channel']).not.toBeUndefined(); - expect(http.fetch).toHaveBeenCalledTimes(1); - }); - - test(`starts and initializes if the broadcast channel fails to load`, async () => { - jest.mock('broadcast-channel', () => { - throw new Error('Unable to load broadcast channel!'); - }); - const consoleSpy = jest.spyOn(console, 'warn'); - - sessionTimeout.start(); - await nextTick(); - // eslint-disable-next-line dot-notation - expect(sessionTimeout['channel']).toBeUndefined(); - expect(http.fetch).toHaveBeenCalledTimes(1); - expect(consoleSpy).toHaveBeenCalledTimes(1); - expect(consoleSpy.mock.calls[0][0]).toMatchInlineSnapshot( - `"Failed to load broadcast channel. Session management will not be kept in sync when multiple tabs are loaded."` - ); - }); - - test(`starts and does not initialize on an anonymous path`, async () => { - http.anonymousPaths.isAnonymous.mockReturnValue(true); - sessionTimeout.start(); - await nextTick(); - // eslint-disable-next-line dot-notation - expect(sessionTimeout['channel']).toBeUndefined(); - expect(http.fetch).not.toHaveBeenCalled(); - }); - - test(`stops`, async () => { - sessionTimeout.start(); - await nextTick(); - // eslint-disable-next-line dot-notation - const close = jest.fn(sessionTimeout['channel']!.close); - // eslint-disable-next-line dot-notation - sessionTimeout['channel']!.close = close; - // eslint-disable-next-line dot-notation - const cleanup = jest.fn(sessionTimeout['cleanup']); - // eslint-disable-next-line dot-notation - sessionTimeout['cleanup'] = cleanup; - - sessionTimeout.stop(); - expect(close).toHaveBeenCalled(); - expect(cleanup).toHaveBeenCalled(); - }); - - test(`stop works properly for large timeouts`, async () => { - http.fetch.mockResolvedValue({ - ...defaultSessionInfo, - idleTimeoutExpiration: now + 5_000_000_000, - }); - sessionTimeout.start(); - await nextTick(); - - // Advance timers far enough to call intermediate `setTimeout` multiple times, but before any - // of the timers is supposed to be triggered. - jest.advanceTimersByTime(5_000_000_000 - (60 + 5 + 2) * 1000); - - sessionTimeout.stop(); - - // Advance timer even further and make sure that timers were properly cleaned up. - jest.runAllTimers(); - - expect(http.fetch).toHaveBeenCalledTimes(1); - expect(sessionExpired.logout).not.toHaveBeenCalled(); - expectNoWarningToast(notifications); - }); - }); - - describe('API calls', () => { - const methodName = 'handleSessionInfoAndResetTimers'; - let method: jest.Mock; - - beforeEach(() => { - method = jest.fn(sessionTimeout[methodName]); - sessionTimeout[methodName] = method; - }); - - test(`handles success`, async () => { - sessionTimeout.start(); - await nextTick(); - - expect(http.fetch).toHaveBeenCalledTimes(1); - // eslint-disable-next-line dot-notation - expect(sessionTimeout['sessionInfo']).toBe(defaultSessionInfo); - expect(method).toHaveBeenCalledTimes(1); - }); - - test(`handles error`, async () => { - const mockErrorResponse = new Error('some-error'); - http.fetch.mockRejectedValue(mockErrorResponse); - sessionTimeout.start(); - await nextTick(); - - expect(http.fetch).toHaveBeenCalledTimes(1); - // eslint-disable-next-line dot-notation - expect(sessionTimeout['sessionInfo']).toBeUndefined(); - expect(method).not.toHaveBeenCalled(); - }); - }); - - describe('warning toast', () => { - test(`shows idle timeout warning toast`, async () => { - sessionTimeout.start(); - await nextTick(); - - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectIdleTimeoutWarningToast(notifications); - }); - - test(`shows idle timeout warning toast even for large timeouts`, async () => { - http.fetch.mockResolvedValue({ - ...defaultSessionInfo, - idleTimeoutExpiration: now + 5_000_000_000, - }); - sessionTimeout.start(); - await nextTick(); - - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(5_000_000_000 - 66 * 1000); - expectNoWarningToast(notifications); - - jest.advanceTimersByTime(1000); - expectIdleTimeoutWarningToast(notifications); - }); - - test(`shows lifespan warning toast`, async () => { - const sessionInfo = { - now, - idleTimeoutExpiration: null, - lifespanExpiration: now + 2 * 60 * 1000, - provider: { type: 'basic', name: 'basic1' }, - }; - http.fetch.mockResolvedValue(sessionInfo); - sessionTimeout.start(); - await nextTick(); - - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectLifespanWarningToast(notifications); - }); - - test(`shows lifespan warning toast even for large timeouts`, async () => { - const sessionInfo = { - ...defaultSessionInfo, - idleTimeoutExpiration: null, - lifespanExpiration: now + 5_000_000_000, - }; - http.fetch.mockResolvedValue(sessionInfo); - sessionTimeout.start(); - await nextTick(); - - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(5_000_000_000 - 66 * 1000); - expectNoWarningToast(notifications); - - jest.advanceTimersByTime(1000); - expectLifespanWarningToast(notifications); - }); - - test(`extend only results in an HTTP call if a warning is shown`, async () => { - sessionTimeout.start(); - await nextTick(); - expect(http.fetch).toHaveBeenCalledTimes(1); - - await sessionTimeout.extend('/foo'); - expect(http.fetch).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(54 * 1000); - expect(http.fetch).toHaveBeenCalledTimes(2); - expectNoWarningToast(notifications); - - await sessionTimeout.extend('/foo'); - expect(http.fetch).toHaveBeenCalledTimes(2); - jest.advanceTimersByTime(10 * 1000); - expectIdleTimeoutWarningToast(notifications); - - await sessionTimeout.extend('/foo'); - expect(http.fetch).toHaveBeenCalledTimes(3); - }); - - test(`extend does not result in an HTTP call if a lifespan warning is shown`, async () => { - const sessionInfo = { - now, - idleTimeoutExpiration: null, - lifespanExpiration: now + 2 * 60 * 1000, - provider: { type: 'basic', name: 'basic1' }, - }; - http.fetch.mockResolvedValue(sessionInfo); - sessionTimeout.start(); - await nextTick(); - - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectLifespanWarningToast(notifications); - - expect(http.fetch).toHaveBeenCalledTimes(1); - await sessionTimeout.extend('/foo'); - expect(http.fetch).toHaveBeenCalledTimes(1); - }); - - test(`extend hides displayed warning toast`, async () => { - sessionTimeout.start(); - await nextTick(); - expect(http.fetch).toHaveBeenCalledTimes(1); - - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - const elapsed = 55 * 1000; - jest.advanceTimersByTime(elapsed); - expectIdleTimeoutWarningToast(notifications); - - http.fetch.mockResolvedValue({ - now: now + elapsed, - idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, - lifespanExpiration: null, - provider: { type: 'basic', name: 'basic1' }, - }); - await sessionTimeout.extend('/foo'); - expect(http.fetch).toHaveBeenCalledTimes(3); - expectWarningToastHidden(notifications, toast); - }); - - test(`extend does nothing for session-related routes`, async () => { - sessionTimeout.start(); - await nextTick(); - expect(http.fetch).toHaveBeenCalledTimes(1); - - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - const elapsed = 55 * 1000; - jest.advanceTimersByTime(elapsed); - expect(http.fetch).toHaveBeenCalledTimes(2); - expectIdleTimeoutWarningToast(notifications); - - await sessionTimeout.extend('/internal/security/session'); - expect(http.fetch).toHaveBeenCalledTimes(2); - }); - - test(`checks for updated session info before the warning displays`, async () => { - sessionTimeout.start(); - await nextTick(); - expect(http.fetch).toHaveBeenCalledTimes(1); - - // we check for updated session info 1 second before the warning is shown - const elapsed = 54 * 1000; - jest.advanceTimersByTime(elapsed); - expect(http.fetch).toHaveBeenCalledTimes(2); - }); - - test('clicking "extend" causes a new HTTP request (which implicitly extends the session)', async () => { - sessionTimeout.start(); - await nextTick(); - expect(http.fetch).toHaveBeenCalledTimes(1); - - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expect(http.fetch).toHaveBeenCalledTimes(2); - expectIdleTimeoutWarningToast(notifications); - - const toastInput = notifications.toasts.add.mock.calls[0][0]; - expect(toastInput).toHaveProperty('text'); - const mountPoint = (toastInput as any).text; - const wrapper = mountWithIntl(mountPoint.__reactMount__); - wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); - expect(http.fetch).toHaveBeenCalledTimes(3); - }); - - test('when the session timeout is shorter than 65 seconds, display the warning immediately and for a shorter duration', async () => { - http.fetch.mockResolvedValue({ - now, - idleTimeoutExpiration: now + 64 * 1000, - lifespanExpiration: null, - provider: { type: 'basic', name: 'basic1' }, - }); - sessionTimeout.start(); - await nextTick(); - expect(http.fetch).toHaveBeenCalled(); - - jest.advanceTimersByTime(0); - expectIdleTimeoutWarningToast(notifications, 59 * 1000); - }); - }); - - describe('session expiration', () => { - test(`expires the session 5 seconds before it really expires`, async () => { - sessionTimeout.start(); - await nextTick(); - - jest.advanceTimersByTime(114 * 1000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(1 * 1000); - expect(sessionExpired.logout).toHaveBeenCalled(); - }); - - test(`expires the session 5 seconds before it really expires even for large timeouts`, async () => { - http.fetch.mockResolvedValue({ - ...defaultSessionInfo, - idleTimeoutExpiration: now + 5_000_000_000, - }); - - sessionTimeout.start(); - await nextTick(); - - jest.advanceTimersByTime(5_000_000_000 - 6000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(1000); - expect(sessionExpired.logout).toHaveBeenCalled(); - }); - - test(`extend delays the expiration`, async () => { - sessionTimeout.start(); - await nextTick(); - expect(http.fetch).toHaveBeenCalledTimes(1); - - const elapsed = 114 * 1000; - jest.advanceTimersByTime(elapsed); - expect(http.fetch).toHaveBeenCalledTimes(2); - expectIdleTimeoutWarningToast(notifications); - - const sessionInfo = { - now: now + elapsed, - idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, - lifespanExpiration: null, - provider: { type: 'basic', name: 'basic1' }, - }; - http.fetch.mockResolvedValue(sessionInfo); - await sessionTimeout.extend('/foo'); - expect(http.fetch).toHaveBeenCalledTimes(3); - // eslint-disable-next-line dot-notation - expect(sessionTimeout['sessionInfo']).toEqual(sessionInfo); - - // at this point, the session is good for another 120 seconds - jest.advanceTimersByTime(114 * 1000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); - - // because "extend" results in an async request and HTTP call, there is a slight delay when timers are updated - // so we need an extra 100ms of padding for this test to ensure that logout has been called - jest.advanceTimersByTime(1 * 1000 + 100); - expect(sessionExpired.logout).toHaveBeenCalled(); - }); - - test(`if the session timeout is shorter than 5 seconds, expire session immediately`, async () => { - http.fetch.mockResolvedValue({ - now, - idleTimeoutExpiration: now + 4 * 1000, - lifespanExpiration: null, - provider: { type: 'basic', name: 'basic1' }, - }); - sessionTimeout.start(); - await nextTick(); - - jest.advanceTimersByTime(0); - expect(sessionExpired.logout).toHaveBeenCalled(); - }); - - test(`'null' sessionTimeout never logs you out`, async () => { - http.fetch.mockResolvedValue({ now, idleTimeoutExpiration: null, lifespanExpiration: null }); - sessionTimeout.start(); - await nextTick(); - - jest.advanceTimersByTime(Number.MAX_VALUE); - expect(sessionExpired.logout).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/x-pack/plugins/security/public/session/session_timeout.ts b/x-pack/plugins/security/public/session/session_timeout.ts new file mode 100644 index 0000000000000..7a23f1bb25ad2 --- /dev/null +++ b/x-pack/plugins/security/public/session/session_timeout.ts @@ -0,0 +1,330 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { BroadcastChannel as BroadcastChannelType } from 'broadcast-channel'; +import type { Subscription } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; +import { skip, tap, throttleTime } from 'rxjs/operators'; + +import type { + HttpFetchOptionsWithPath, + HttpSetup, + NotificationsSetup, + Toast, +} from 'src/core/public'; + +import { + SESSION_CHECK_MS, + SESSION_EXPIRATION_WARNING_MS, + SESSION_EXTENSION_THROTTLE_MS, + SESSION_GRACE_PERIOD_MS, + SESSION_ROUTE, +} from '../../common/constants'; +import type { SessionInfo } from '../../common/types'; +import { createSessionExpirationToast } from './session_expiration_toast'; +import type { ISessionExpired } from './session_expired'; + +export interface SessionState extends Pick { + lastExtensionTime: number; +} + +export class SessionTimeout { + private channel?: BroadcastChannelType; + + private isVisible = document.visibilityState !== 'hidden'; + private isFetchingSessionInfo = false; + private snoozedWarningState?: SessionState; + + private sessionState$ = new BehaviorSubject({ + lastExtensionTime: 0, + expiresInMs: null, + canBeExtended: false, + }); + private subscription?: Subscription; + + private warningToast?: Toast; + + private stopActivityMonitor?: Function; + private stopVisibilityMonitor?: Function; + private removeHttpInterceptor?: Function; + + private stopRefreshTimer?: Function; + private stopWarningTimer?: Function; + private stopLogoutTimer?: Function; + + constructor( + private notifications: NotificationsSetup, + private sessionExpired: ISessionExpired, + private http: HttpSetup, + private tenant: string + ) {} + + public async start() { + if (this.http.anonymousPaths.isAnonymous(window.location.pathname)) { + return; + } + + this.subscription = this.sessionState$ + .pipe(skip(1), throttleTime(1000), tap(this.toggleEventHandlers)) + .subscribe(this.resetTimers); + + // Subscribe to a broadcast channel for session timeout messages. + // This allows us to synchronize the UX across tabs and avoid repetitive API calls. + try { + const { BroadcastChannel } = await import('broadcast-channel'); + this.channel = new BroadcastChannel(`${this.tenant}/session_timeout`, { + webWorkerSupport: false, + }); + this.channel.onmessage = this.handleChannelMessage; + } catch (error) { + // eslint-disable-next-line no-console + console.warn( + `Failed to load broadcast channel. Session management will not be kept in sync when multiple tabs are loaded.`, + error + ); + } + + return this.fetchSessionInfo(); + } + + public stop() { + const nextState = { + lastExtensionTime: 0, + expiresInMs: null, + canBeExtended: false, + }; + this.toggleEventHandlers(nextState); + this.resetTimers(nextState); + this.subscription?.unsubscribe(); + this.channel?.close(); + } + + /** + * Event handler that receives session information from other browser tabs. + */ + private handleChannelMessage = (message: SessionState) => { + this.sessionState$.next(message); + }; + + /** + * HTTP request interceptor which ensures that API calls extend the session only if tab is + * visible. + */ + private handleHttpRequest = (fetchOptions: HttpFetchOptionsWithPath) => { + // Ignore requests to external URLs + if (fetchOptions.path.indexOf('://') !== -1 || fetchOptions.path.startsWith('//')) { + return; + } + + if (!fetchOptions.asSystemRequest) { + return { ...fetchOptions, asSystemRequest: !this.isVisible }; + } + }; + + /** + * Event handler that tracks user activity and extends the session if needed. + */ + private handleUserActivity = () => { + if (this.shouldExtend()) { + this.fetchSessionInfo(true); + } + }; + + /** + * Event handler that tracks page visibility. + */ + private handleVisibilityChange = (isVisible: boolean) => { + this.isVisible = isVisible; + if (isVisible) { + this.handleUserActivity(); + } + }; + + private resetTimers = ({ lastExtensionTime, expiresInMs }: SessionState) => { + this.stopRefreshTimer = this.stopRefreshTimer?.(); + this.stopWarningTimer = this.stopWarningTimer?.(); + this.stopLogoutTimer = this.stopLogoutTimer?.(); + + if (expiresInMs !== null) { + const logoutInMs = Math.max(expiresInMs - SESSION_GRACE_PERIOD_MS, 0); + + // Show warning before session expires. However, do not show warning again if previously + // dismissed. The snooze time is the expiration time that was remaining in the warning. + const showWarningInMs = Math.max( + logoutInMs - SESSION_EXPIRATION_WARNING_MS, + this.snoozedWarningState + ? this.snoozedWarningState.lastExtensionTime + + this.snoozedWarningState.expiresInMs! - + SESSION_GRACE_PERIOD_MS - + lastExtensionTime + : 0, + 0 + ); + + const fetchSessionInMs = showWarningInMs - SESSION_CHECK_MS; + + // Schedule logout when session is about to expire + this.stopLogoutTimer = startTimer(() => this.sessionExpired.logout(), logoutInMs); + + // Hide warning if session has been extended + if (showWarningInMs > 0) { + this.hideWarning(); + } + + // Schedule warning before session expires + if (showWarningInMs < logoutInMs) { + this.stopWarningTimer = startTimer(this.showWarning, showWarningInMs); + } + + // Refresh session info before showing warning + if (fetchSessionInMs > 0 && fetchSessionInMs < logoutInMs) { + this.stopRefreshTimer = startTimer(this.fetchSessionInfo, fetchSessionInMs); + } + } + }; + + private toggleEventHandlers = ({ expiresInMs, canBeExtended }: SessionState) => { + if (expiresInMs !== null) { + // Monitor activity if session can be extended + if (canBeExtended && !this.stopActivityMonitor) { + this.stopActivityMonitor = monitorActivity(this.handleUserActivity); + } + + // Intercept HTTP requests if session can expire + if (!this.removeHttpInterceptor) { + this.removeHttpInterceptor = this.http.intercept({ request: this.handleHttpRequest }); + } + + if (!this.stopVisibilityMonitor) { + this.stopVisibilityMonitor = monitorVisibility(this.handleVisibilityChange); + } + } else { + this.removeHttpInterceptor = this.removeHttpInterceptor?.(); + this.stopActivityMonitor = this.stopActivityMonitor?.(); + this.stopVisibilityMonitor = this.stopVisibilityMonitor?.(); + } + }; + + private shouldExtend() { + const { lastExtensionTime } = this.sessionState$.getValue(); + return ( + !this.isFetchingSessionInfo && + !this.warningToast && + Date.now() > lastExtensionTime + SESSION_EXTENSION_THROTTLE_MS + ); + } + + private fetchSessionInfo = async (extend = false) => { + this.isFetchingSessionInfo = true; + try { + const sessionInfo = await this.http.fetch(SESSION_ROUTE, { + method: extend ? 'POST' : 'GET', + asSystemRequest: !extend, + }); + if (sessionInfo) { + const { expiresInMs, canBeExtended } = sessionInfo; + const nextState: SessionState = { + lastExtensionTime: Date.now(), + expiresInMs, + canBeExtended, + }; + this.sessionState$.next(nextState); + if (this.channel) { + this.channel.postMessage(nextState); + } + return nextState; + } + } catch (error) { + // ignore + } finally { + this.isFetchingSessionInfo = false; + } + }; + + private showWarning = () => { + if (!this.warningToast) { + const onExtend = async () => { + const { canBeExtended } = this.sessionState$.getValue(); + if (canBeExtended) { + await this.fetchSessionInfo(true); + } + }; + const onClose = () => { + this.hideWarning(true); + return onExtend(); + }; + const toast = createSessionExpirationToast(this.sessionState$, onExtend, onClose); + this.warningToast = this.notifications.toasts.add(toast); + } + }; + + private hideWarning = (snooze = false) => { + if (this.warningToast) { + this.notifications.toasts.remove(this.warningToast); + this.warningToast = undefined; + if (snooze) { + this.snoozedWarningState = this.sessionState$.getValue(); + } + } + }; +} + +/** + * Starts a timer that uses a native `setTimeout` under the hood. When `timeout` is larger + * than the maximum supported one then method calls itself recursively as many times as needed. + * @param callback A function to be executed after the timer expires. + * @param timeout The time, in milliseconds the timer should wait before the specified function is + * executed. + * @returns Function to stop the timer. + */ +export function startTimer(callback: () => void, timeout: number, updater?: (id: number) => void) { + // Max timeout is the largest possible 32-bit signed integer or 2,147,483,647 or 0x7fffffff. + const maxTimeout = 0x7fffffff; + let timeoutID: number; + updater = updater ?? ((id: number) => (timeoutID = id)); + + updater( + timeout > maxTimeout + ? window.setTimeout(() => startTimer(callback, timeout - maxTimeout, updater), maxTimeout) + : window.setTimeout(callback, timeout) + ); + + return () => clearTimeout(timeoutID); +} + +/** + * Adds event handlers to the window object that track user activity. + * @param callback Function to be executed when user activity is detected. + * @returns Function to remove all event handlers from window. + */ +function monitorActivity(callback: () => void) { + const eventTypes = ['mousemove', 'mousedown', 'wheel', 'touchstart', 'keydown']; + for (const eventType of eventTypes) { + window.addEventListener(eventType, callback); + } + + return () => { + for (const eventType of eventTypes) { + window.removeEventListener(eventType, callback); + } + }; +} + +/** + * Adds event handlers to the document object that track page visibility. + * @param callback Function to be executed when page visibility changes. + * @returns Function to remove all event handlers from document. + */ +function monitorVisibility(callback: (isVisible: boolean) => void) { + const handleVisibilityChange = () => callback(document.visibilityState !== 'hidden'); + + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; +} diff --git a/x-pack/plugins/security/public/session/session_timeout.tsx b/x-pack/plugins/security/public/session/session_timeout.tsx deleted file mode 100644 index 2288fce8d30af..0000000000000 --- a/x-pack/plugins/security/public/session/session_timeout.tsx +++ /dev/null @@ -1,255 +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 { BroadcastChannel as BroadcastChannelType } from 'broadcast-channel'; - -import type { HttpSetup, NotificationsSetup, Toast, ToastInput } from 'src/core/public'; - -import type { SessionInfo } from '../../common/types'; -import type { ISessionExpired } from './session_expired'; -import { createToast as createIdleTimeoutToast } from './session_idle_timeout_warning'; -import { createToast as createLifespanToast } from './session_lifespan_warning'; - -/** - * Client session timeout is decreased by this number so that Kibana server - * can still access session content during logout request to properly clean - * user session up (invalidate access tokens, redirect to logout portal etc.). - */ -const GRACE_PERIOD_MS = 5 * 1000; - -/** - * Duration we'll normally display the warning toast - */ -const WARNING_MS = 60 * 1000; - -/** - * Current session info is checked this number of milliseconds before the - * warning toast shows. This will prevent the toast from being shown if the - * session has already been extended. - */ -const SESSION_CHECK_MS = 1000; - -/** - * Route to get session info and extend session expiration - */ -const SESSION_ROUTE = '/internal/security/session'; - -export interface ISessionTimeout { - start(): void; - stop(): void; - extend(url: string): void; -} - -export class SessionTimeout implements ISessionTimeout { - private channel?: BroadcastChannelType; - private sessionInfo?: SessionInfo; - private fetchTimer?: number; - private warningTimer?: number; - private expirationTimer?: number; - private warningToast?: Toast; - - constructor( - private notifications: NotificationsSetup, - private sessionExpired: ISessionExpired, - private http: HttpSetup, - private tenant: string - ) {} - - start() { - if (this.http.anonymousPaths.isAnonymous(window.location.pathname)) { - return; - } - - import('broadcast-channel') - .then(({ BroadcastChannel }) => { - // subscribe to a broadcast channel for session timeout messages - // this allows us to synchronize the UX across tabs and avoid repetitive API calls - const name = `${this.tenant}/session_timeout`; - this.channel = new BroadcastChannel(name, { webWorkerSupport: false }); - this.channel.onmessage = this.handleSessionInfoAndResetTimers; - }) - .catch((e) => { - // eslint-disable-next-line no-console - console.warn( - `Failed to load broadcast channel. Session management will not be kept in sync when multiple tabs are loaded.`, - e - ); - }) - .finally(() => { - // Triggers an initial call to the endpoint to get session info; - // when that returns, it will set the timeout - return this.fetchSessionInfoAndResetTimers(); - }); - } - - stop() { - if (this.channel) { - this.channel.close(); - } - this.cleanup(); - } - - /** - * When the user makes an authenticated, non-system API call, this function is used to check - * and see if the session has been extended. - * @param url The URL that was called - */ - extend(url: string) { - // avoid an additional API calls when the user clicks the button on the session idle timeout - if (url.endsWith(SESSION_ROUTE)) { - return; - } - - const { isLifespanTimeout } = this.getTimeout(); - if (this.warningToast && !isLifespanTimeout) { - // the idle timeout warning is currently showing and the user has clicked elsewhere on the page; - // make a new call to get the latest session info - return this.fetchSessionInfoAndResetTimers(); - } - } - - /** - * Fetch latest session information from the server, and optionally attempt to extend - * the session expiration. - */ - private fetchSessionInfoAndResetTimers = async (extend = false) => { - const method = extend ? 'POST' : 'GET'; - try { - const result = await this.http.fetch(SESSION_ROUTE, { method, asSystemRequest: !extend }); - - this.handleSessionInfoAndResetTimers(result); - - // share this updated session info with any other tabs to sync the UX - if (this.channel) { - this.channel.postMessage(result); - } - } catch (err) { - // do nothing; 401 errors will be caught by the http interceptor - } - }; - - /** - * Processes latest session information, and resets timers based on it. These timers are - * used to trigger an HTTP call for updated session information, to show a timeout - * warning, and to log the user out when their session is expired. - */ - private handleSessionInfoAndResetTimers = (sessionInfo: SessionInfo) => { - this.sessionInfo = sessionInfo; - // save the provider name in session storage, we will need it when we log out - const key = `${this.tenant}/session_provider`; - sessionStorage.setItem(key, sessionInfo.provider.name); - - const { timeout, isLifespanTimeout } = this.getTimeout(); - if (timeout == null) { - return; - } - - this.cleanup(); - - // set timers - const timeoutVal = timeout - WARNING_MS - GRACE_PERIOD_MS - SESSION_CHECK_MS; - if (timeoutVal > 0 && !isLifespanTimeout) { - // we should check for the latest session info before the warning displays - this.startTimer( - (timeoutID) => (this.fetchTimer = timeoutID), - this.fetchSessionInfoAndResetTimers, - timeoutVal - ); - } - - this.startTimer( - (timeoutID) => (this.warningTimer = timeoutID), - this.showWarning, - Math.max(timeout - WARNING_MS - GRACE_PERIOD_MS, 0) - ); - - this.startTimer( - (timeoutID) => (this.expirationTimer = timeoutID), - () => this.sessionExpired.logout(), - Math.max(timeout - GRACE_PERIOD_MS, 0) - ); - }; - - private cleanup = () => { - if (this.fetchTimer) { - window.clearTimeout(this.fetchTimer); - } - if (this.warningTimer) { - window.clearTimeout(this.warningTimer); - } - if (this.expirationTimer) { - window.clearTimeout(this.expirationTimer); - } - if (this.warningToast) { - this.notifications.toasts.remove(this.warningToast); - this.warningToast = undefined; - } - }; - - /** - * Get the amount of time until the session times out, and whether or not the - * session has reached it maximum lifespan. - */ - private getTimeout = (): { timeout: number | null; isLifespanTimeout: boolean } => { - let timeout = null; - let isLifespanTimeout = false; - if (this.sessionInfo) { - const { now, idleTimeoutExpiration, lifespanExpiration } = this.sessionInfo; - if (idleTimeoutExpiration) { - timeout = idleTimeoutExpiration - now; - } - if ( - lifespanExpiration && - (idleTimeoutExpiration === null || lifespanExpiration <= idleTimeoutExpiration) - ) { - timeout = lifespanExpiration - now; - isLifespanTimeout = true; - } - } - return { timeout, isLifespanTimeout }; - }; - - /** - * Show a warning toast depending on the session state. - */ - private showWarning = () => { - const { timeout, isLifespanTimeout } = this.getTimeout(); - const toastLifeTimeMs = Math.min(timeout! - GRACE_PERIOD_MS, WARNING_MS); - let toast: ToastInput; - if (!isLifespanTimeout) { - const refresh = () => this.fetchSessionInfoAndResetTimers(true); - toast = createIdleTimeoutToast(toastLifeTimeMs, refresh); - } else { - toast = createLifespanToast(toastLifeTimeMs); - } - this.warningToast = this.notifications.toasts.add(toast); - }; - - /** - * Starts a timer that uses a native `setTimeout` under the hood. When `timeout` is larger - * than the maximum supported one then method calls itself recursively as many times as needed. - * @param updater Method that is supposed to update a reference to a native timer ID that can be - * used with native `clearTimeout`. It's essential for the larger timeouts when `setTimeout` is - * called multiple times and timer ID changes. - * when timer ID changes - * @param callback A function to be executed after the timer expires. - * @param timeout The time, in milliseconds the timer should wait before the specified function is - * executed. - */ - private startTimer(updater: (timeoutID: number) => void, callback: () => void, timeout: number) { - // Max timeout is the largest possible 32-bit signed integer or 2,147,483,647 or 0x7fffffff. - const maxTimeout = 0x7fffffff; - updater( - timeout > maxTimeout - ? window.setTimeout( - () => this.startTimer(updater, callback, timeout - maxTimeout), - maxTimeout - ) - : window.setTimeout(callback, timeout) - ); - } -} diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts deleted file mode 100644 index 4f216d331c3f2..0000000000000 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts +++ /dev/null @@ -1,123 +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. - */ - -// @ts-ignore -import fetchMock from 'fetch-mock/es5/client'; - -import { setup } from 'src/core/test_helpers/http_test_setup'; - -import { createSessionTimeoutMock } from './session_timeout.mock'; -import { SessionTimeoutHttpInterceptor } from './session_timeout_http_interceptor'; - -const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); - -const setupHttp = (basePath: string) => { - const { http } = setup((injectedMetadata) => { - injectedMetadata.getBasePath.mockReturnValue(basePath); - }); - return http; -}; - -afterEach(() => { - fetchMock.restore(); -}); - -describe('response', () => { - test('extends session timeouts', async () => { - const http = setupHttp('/foo'); - const sessionTimeoutMock = createSessionTimeoutMock(); - const interceptor = new SessionTimeoutHttpInterceptor(sessionTimeoutMock, http.anonymousPaths); - http.intercept(interceptor); - fetchMock.mock('*', 200); - - await http.fetch('/foo-api'); - - expect(sessionTimeoutMock.extend).toHaveBeenCalled(); - }); - - test(`doesn't extend session timeouts for anonymous paths`, async () => { - mockCurrentUrl('/foo/bar'); - const http = setupHttp('/foo'); - const sessionTimeoutMock = createSessionTimeoutMock(); - const { anonymousPaths } = http; - anonymousPaths.register('/bar'); - const interceptor = new SessionTimeoutHttpInterceptor(sessionTimeoutMock, anonymousPaths); - http.intercept(interceptor); - fetchMock.mock('*', 200); - - await http.fetch('/foo-api'); - - expect(sessionTimeoutMock.extend).not.toHaveBeenCalled(); - }); - - test(`doesn't extend session timeouts for system api requests`, async () => { - const http = setupHttp('/foo'); - const sessionTimeoutMock = createSessionTimeoutMock(); - const interceptor = new SessionTimeoutHttpInterceptor(sessionTimeoutMock, http.anonymousPaths); - http.intercept(interceptor); - fetchMock.mock('*', 200); - - await http.fetch('/foo-api', { asSystemRequest: true }); - - expect(sessionTimeoutMock.extend).not.toHaveBeenCalled(); - }); -}); - -describe('responseError', () => { - test('extends session timeouts', async () => { - const http = setupHttp('/foo'); - const sessionTimeoutMock = createSessionTimeoutMock(); - const interceptor = new SessionTimeoutHttpInterceptor(sessionTimeoutMock, http.anonymousPaths); - http.intercept(interceptor); - fetchMock.mock('*', 401); - - await expect(http.fetch('/foo-api')).rejects.toMatchInlineSnapshot(`[Error: Unauthorized]`); - - expect(sessionTimeoutMock.extend).toHaveBeenCalled(); - }); - - test(`doesn't extend session timeouts for anonymous paths`, async () => { - mockCurrentUrl('/foo/bar'); - const http = setupHttp('/foo'); - const sessionTimeoutMock = createSessionTimeoutMock(); - const { anonymousPaths } = http; - anonymousPaths.register('/bar'); - const interceptor = new SessionTimeoutHttpInterceptor(sessionTimeoutMock, anonymousPaths); - http.intercept(interceptor); - fetchMock.mock('*', 401); - - await expect(http.fetch('/foo-api')).rejects.toMatchInlineSnapshot(`[Error: Unauthorized]`); - - expect(sessionTimeoutMock.extend).not.toHaveBeenCalled(); - }); - - test(`doesn't extend session timeouts for system api requests`, async () => { - const http = setupHttp('/foo'); - const sessionTimeoutMock = createSessionTimeoutMock(); - const interceptor = new SessionTimeoutHttpInterceptor(sessionTimeoutMock, http.anonymousPaths); - http.intercept(interceptor); - fetchMock.mock('*', 401); - - await expect(http.fetch('/foo-api', { asSystemRequest: true })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized]` - ); - - expect(sessionTimeoutMock.extend).not.toHaveBeenCalled(); - }); - - test(`doesn't extend session timeouts when there is no response`, async () => { - const http = setupHttp('/foo'); - const sessionTimeoutMock = createSessionTimeoutMock(); - const interceptor = new SessionTimeoutHttpInterceptor(sessionTimeoutMock, http.anonymousPaths); - http.intercept(interceptor); - fetchMock.mock('*', new Promise((resolve, reject) => reject(new Error('Network is down')))); - - await expect(http.fetch('/foo-api')).rejects.toMatchInlineSnapshot(`[Error: Network is down]`); - - expect(sessionTimeoutMock.extend).not.toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts deleted file mode 100644 index 6fc7fb88578ff..0000000000000 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts +++ /dev/null @@ -1,52 +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 { - HttpInterceptor, - HttpInterceptorResponseError, - HttpResponse, - IAnonymousPaths, -} from 'src/core/public'; - -import type { ISessionTimeout } from './session_timeout'; - -export class SessionTimeoutHttpInterceptor implements HttpInterceptor { - constructor(private sessionTimeout: ISessionTimeout, private anonymousPaths: IAnonymousPaths) {} - - response(httpResponse: HttpResponse) { - if (this.anonymousPaths.isAnonymous(window.location.pathname)) { - return; - } - - if (httpResponse.fetchOptions.asSystemRequest) { - return; - } - - this.sessionTimeout.extend(httpResponse.request.url); - } - - responseError(httpErrorResponse: HttpInterceptorResponseError) { - if (this.anonymousPaths.isAnonymous(window.location.pathname)) { - return; - } - - if (httpErrorResponse.fetchOptions.asSystemRequest) { - return; - } - - // if we happen to not have a response, for example if there is no - // network connectivity, we won't extend the session because there - // won't be a response with a set-cookie header, which is required - // to extend the session - const { response } = httpErrorResponse; - if (!response) { - return; - } - - this.sessionTimeout.extend(httpErrorResponse.request.url); - } -} diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 087cf8f4f8ee8..e50ab66a92547 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -27,6 +27,7 @@ export type { GrantAPIKeyResult, } from './authentication'; export type { CheckPrivilegesPayload } from './authorization'; +export type AuthorizationServiceSetup = SecurityPluginStart['authz']; export { LegacyAuditLogger, AuditLogger, AuditEvent } from './audit'; export type { SecurityPluginSetup, SecurityPluginStart }; export type { AuthenticatedUser } from '../common/model'; diff --git a/x-pack/plugins/security/server/routes/session_management/info.test.ts b/x-pack/plugins/security/server/routes/session_management/info.test.ts index 29c0865b8c561..4c420f3b14346 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.test.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.test.ts @@ -10,6 +10,7 @@ import type { RequestHandler, RouteConfig } from 'src/core/server'; import { kibanaResponseFactory } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; +import { SESSION_EXPIRATION_WARNING_MS } from '../../../common/constants'; import type { Session } from '../../session_management'; import { sessionMock } from '../../session_management/session.mock'; import type { SecurityRequestHandlerContext, SecurityRouter } from '../../types'; @@ -29,7 +30,7 @@ describe('Info session routes', () => { defineSessionInfoRoutes(routeParamsMock); }); - describe('extend session', () => { + describe('session info', () => { let routeHandler: RequestHandler; let routeConfig: RouteConfig; beforeEach(() => { @@ -63,30 +64,94 @@ describe('Info session routes', () => { }); it('returns session info.', async () => { - session.get.mockResolvedValue( - sessionMock.createValue({ idleTimeoutExpiration: 100, lifespanExpiration: 200 }) - ); - + const now = 1000; const dateSpy = jest.spyOn(Date, 'now'); - dateSpy.mockReturnValue(1234); - - const expectedBody = { - now: 1234, - provider: { type: 'basic', name: 'basic1' }, - idleTimeoutExpiration: 100, - lifespanExpiration: 200, - }; - await expect( - routeHandler( - ({} as unknown) as SecurityRequestHandlerContext, - httpServerMock.createKibanaRequest(), - kibanaResponseFactory - ) - ).resolves.toEqual({ - status: 200, - payload: expectedBody, - options: { body: expectedBody }, - }); + dateSpy.mockReturnValue(now); + + const assertions = [ + [ + { + idleTimeoutExpiration: 100 + now, + lifespanExpiration: 200 + SESSION_EXPIRATION_WARNING_MS + now, + }, + { + canBeExtended: true, + expiresInMs: 100, + }, + ], + [ + { + idleTimeoutExpiration: 100 + now, + lifespanExpiration: 200 + now, + }, + { + canBeExtended: false, + expiresInMs: 100, + }, + ], + [ + { + idleTimeoutExpiration: 200 + now, + lifespanExpiration: 100 + now, + }, + { + canBeExtended: false, + expiresInMs: 100, + }, + ], + [ + { + idleTimeoutExpiration: null, + lifespanExpiration: 100 + now, + }, + { + canBeExtended: false, + expiresInMs: 100, + }, + ], + [ + { + idleTimeoutExpiration: 100 + now, + lifespanExpiration: null, + }, + { + canBeExtended: true, + expiresInMs: 100, + }, + ], + [ + { + idleTimeoutExpiration: null, + lifespanExpiration: null, + }, + { + canBeExtended: false, + expiresInMs: null, + }, + ], + ]; + + for (const [sessionInfo, expected] of assertions) { + session.get.mockResolvedValue(sessionMock.createValue(sessionInfo)); + + const expectedBody = { + canBeExtended: expected.canBeExtended, + expiresInMs: expected.expiresInMs, + provider: { type: 'basic', name: 'basic1' }, + }; + + await expect( + routeHandler( + ({} as unknown) as SecurityRequestHandlerContext, + httpServerMock.createKibanaRequest(), + kibanaResponseFactory + ) + ).resolves.toEqual({ + status: 200, + payload: expectedBody, + options: { body: expectedBody }, + }); + } }); it('returns empty response if session is not available.', async () => { diff --git a/x-pack/plugins/security/server/routes/session_management/info.ts b/x-pack/plugins/security/server/routes/session_management/info.ts index cfd76bb8abbc0..b141f42c4b072 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.ts @@ -6,6 +6,7 @@ */ import type { RouteDefinitionParams } from '../'; +import { SESSION_EXPIRATION_WARNING_MS } from '../../../common/constants'; import type { SessionInfo } from '../../../common/types'; /** @@ -17,13 +18,19 @@ export function defineSessionInfoRoutes({ router, getSession }: RouteDefinitionP async (_context, request, response) => { const sessionValue = await getSession().get(request); if (sessionValue) { + const expirationTime = + sessionValue.idleTimeoutExpiration && sessionValue.lifespanExpiration + ? Math.min(sessionValue.idleTimeoutExpiration, sessionValue.lifespanExpiration) + : sessionValue.idleTimeoutExpiration || sessionValue.lifespanExpiration; + return response.ok({ body: { - // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return - // the current server time -- that way the client can calculate the relative time to expiration. - now: Date.now(), - idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, - lifespanExpiration: sessionValue.lifespanExpiration, + expiresInMs: expirationTime ? expirationTime - Date.now() : null, + canBeExtended: + sessionValue.idleTimeoutExpiration !== null && + expirationTime !== null && + (sessionValue.lifespanExpiration === null || + expirationTime + SESSION_EXPIRATION_WARNING_MS < sessionValue.lifespanExpiration), provider: sessionValue.provider, } as SessionInfo, }); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts new file mode 100644 index 0000000000000..6e508a099003a --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.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 { estypes } from '@elastic/elasticsearch'; +import { DeepPartial } from 'utility-types'; +import { merge } from 'lodash'; +import { BaseDataGenerator } from './base_data_generator'; +import { Agent, AGENTS_INDEX, FleetServerAgent } from '../../../../fleet/common'; + +export class FleetAgentGenerator extends BaseDataGenerator { + /** + * @param [overrides] any partial value to the full Agent record + * + * @example + * + * fleetAgentGenerator.generate({ + * local_metadata: { + * elastic: { + * agent: { + * log_level: `debug` + * } + * } + * } + * }); + */ + generate(overrides: DeepPartial = {}): Agent { + const hit = this.generateEsHit(); + + // The mapping below is identical to `searchHitToAgent()` located in + // `x-pack/plugins/fleet/server/services/agents/helpers.ts:19` + return merge( + { + // Casting here is needed because several of the attributes in `FleetServerAgent` are + // defined as optional, but required in `Agent` type. + ...(hit._source as Agent), + id: hit._id, + policy_revision: hit._source?.policy_revision_idx, + access_api_key: undefined, + status: undefined, + packages: hit._source?.packages ?? [], + }, + overrides + ); + } + + /** + * @param [overrides] any partial value to the full document + */ + generateEsHit( + overrides: DeepPartial> = {} + ): estypes.Hit { + const hostname = this.randomHostname(); + const now = new Date().toISOString(); + const osFamily = this.randomOSFamily(); + + return merge, DeepPartial>>( + { + _index: AGENTS_INDEX, + _id: this.randomUUID(), + _score: 1.0, + _source: { + access_api_key_id: 'jY3dWnkBj1tiuAw9pAmq', + action_seq_no: -1, + active: true, + enrolled_at: now, + local_metadata: { + elastic: { + agent: { + 'build.original': `8.0.0-SNAPSHOT (build: ${this.randomString( + 5 + )} at 2021-05-07 18:42:49 +0000 UTC)`, + id: this.randomUUID(), + log_level: 'info', + snapshot: true, + upgradeable: true, + version: '8.0.0', + }, + }, + host: { + architecture: 'x86_64', + hostname, + id: this.randomUUID(), + ip: [this.randomIP()], + mac: [this.randomMac()], + name: hostname, + }, + os: { + family: osFamily, + full: `${osFamily} 2019 Datacenter`, + kernel: '10.0.17763.1879 (Build.160101.0800)', + name: `${osFamily} Server 2019 Datacenter`, + platform: osFamily, + version: this.randomVersion(), + }, + }, + user_provided_metadata: {}, + policy_id: this.randomUUID(), + type: 'PERMANENT', + default_api_key: 'so3dWnkBj1tiuAw9yAm3:t7jNlnPnR6azEI_YpXuBXQ', + // policy_output_permissions_hash: + // '81b3d070dddec145fafcbdfb6f22888495a12edc31881f6b0511fa10de66daa7', + default_api_key_id: 'so3dWnkBj1tiuAw9yAm3', + updated_at: now, + last_checkin: now, + policy_revision_idx: 2, + policy_coordinator_idx: 1, + }, + }, + overrides + ); + } +} 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 fd26a2d95c9b4..0dc7891560c2d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -5,31 +5,31 @@ * 2.0. */ -import { Client } from '@elastic/elasticsearch'; +import { Client, estypes } from '@elastic/elasticsearch'; import seedrandom from 'seedrandom'; // eslint-disable-next-line import/no-extraneous-dependencies import { KbnClient } from '@kbn/test'; import { AxiosResponse } from 'axios'; -import { EndpointDocGenerator, TreeOptions, Event } from './generate_data'; +import { EndpointDocGenerator, Event, TreeOptions } from './generate_data'; import { firstNonNullValue } from './models/ecs_safety_helpers'; import { + AGENT_POLICY_API_ROUTES, CreateAgentPolicyRequest, CreateAgentPolicyResponse, CreatePackagePolicyRequest, CreatePackagePolicyResponse, - GetPackagesResponse, - AGENT_API_ROUTES, - AGENT_POLICY_API_ROUTES, EPM_API_ROUTES, + FLEET_SERVER_SERVERS_INDEX, + FleetServerAgent, + GetPackagesResponse, PACKAGE_POLICY_API_ROUTES, - ENROLLMENT_API_KEY_ROUTES, - GetEnrollmentAPIKeysResponse, - GetOneEnrollmentAPIKeyResponse, - Agent, } from '../../../fleet/common'; import { policyFactory as policyConfigFactory } from './models/policy_config'; import { HostMetadata } from './types'; import { KbnClientWithApiKeySupport } from '../../scripts/endpoint/kbn_client_with_api_key_support'; +import { FleetAgentGenerator } from './data_generators/fleet_agent_generator'; + +const fleetAgentGenerator = new FleetAgentGenerator(); export async function indexHostsAndAlerts( client: Client, @@ -47,8 +47,15 @@ export async function indexHostsAndAlerts( ) { const random = seedrandom(seed); const epmEndpointPackage = await getEndpointPackageInfo(kbnClient); + + // If `fleet` integration is true, then ensure a (fake) fleet-server is connected + if (fleet) { + await enableFleetServerIfNecessary(client); + } + // Keep a map of host applied policy ids (fake) to real ingest package configs (policy record) const realPolicies: Record = {}; + for (let i = 0; i < numHosts; i++) { const generator = new EndpointDocGenerator(random); await indexHostDocs({ @@ -71,9 +78,11 @@ export async function indexHostsAndAlerts( options, }); } + await client.indices.refresh({ index: eventIndex, }); + // TODO: Unclear why the documents are not showing up after the call to refresh. // Waiting 5 seconds allows the indices to refresh automatically and // the documents become available in API/integration tests. @@ -107,9 +116,10 @@ async function indexHostDocs({ }) { const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents const timestamp = new Date().getTime(); + const kibanaVersion = await fetchKibanaVersion(kbnClient); let hostMetadata: HostMetadata; let wasAgentEnrolled = false; - let enrolledAgent: undefined | Agent; + let enrolledAgent: undefined | estypes.Hit; for (let j = 0; j < numDocs; j++) { generator.updateHostData(); @@ -136,10 +146,12 @@ async function indexHostDocs({ // If we did not yet enroll an agent for this Host, do it now that we have good policy id if (!wasAgentEnrolled) { wasAgentEnrolled = true; - enrolledAgent = await fleetEnrollAgentForHost( - kbnClient, + + enrolledAgent = await indexFleetAgentForHost( + client, hostMetadata!, - realPolicies[appliedPolicyId].policy_id + realPolicies[appliedPolicyId].policy_id, + kibanaVersion ); } // Update the Host metadata record with the ID of the "real" policy along with the enrolled agent id @@ -149,7 +161,7 @@ async function indexHostDocs({ ...hostMetadata.elastic, agent: { ...hostMetadata.elastic.agent, - id: enrolledAgent?.id ?? hostMetadata.elastic.agent.id, + id: enrolledAgent?._id ?? hostMetadata.elastic.agent.id, }, }, Endpoint: { @@ -295,208 +307,93 @@ const getEndpointPackageInfo = async ( return endpointPackage; }; -const fleetEnrollAgentForHost = async ( - kbnClient: KbnClientWithApiKeySupport, - endpointHost: HostMetadata, - agentPolicyId: string -): Promise => { - // Get Enrollement key for host's applied policy - const enrollmentApiKey = await kbnClient - .request({ - path: ENROLLMENT_API_KEY_ROUTES.LIST_PATTERN, - method: 'GET', - query: { - kuery: `policy_id:"${agentPolicyId}"`, - }, - }) - .then((apiKeysResponse) => { - const apiKey = apiKeysResponse.data.list[0]; +const fetchKibanaVersion = async (kbnClient: KbnClientWithApiKeySupport) => { + const version = ((await kbnClient.request({ + path: '/api/status', + method: 'GET', + })) as AxiosResponse).data.version.number; - if (!apiKey) { - return Promise.reject( - new Error(`no API enrollment key found for agent policy id ${agentPolicyId}`) - ); - } + if (!version) { + // eslint-disable-next-line no-console + console.log('failed to retrieve kibana version'); + return '8.0.0'; + } - return kbnClient - .request({ - path: ENROLLMENT_API_KEY_ROUTES.INFO_PATTERN.replace('{keyId}', apiKey.id), - method: 'GET', - }) - .catch((error) => { - // eslint-disable-next-line no-console - console.log('unable to retrieve enrollment api key for policy'); - return Promise.reject(error); - }); - }) - .then((apiKeyDetailsResponse) => { - return apiKeyDetailsResponse.data.item.api_key; - }) - .catch((error) => { - // eslint-disable-next-line no-console - console.error(error); - return ''; - }); + return version; +}; + +/** + * Will ensure that at least one fleet server is present in the `.fleet-servers` index. This will + * enable the `Agent` section of kibana Fleet to be displayed + * + * @param esClient + * @param version + */ +const enableFleetServerIfNecessary = async (esClient: Client, version: string = '8.0.0') => { + const res = await esClient.search<{}, {}>({ + index: FLEET_SERVER_SERVERS_INDEX, + ignore_unavailable: true, + }); - if (enrollmentApiKey.length === 0) { + // @ts-expect-error value is number | TotalHits + if (res.body.hits.total.value > 0) { return; } - const fetchKibanaVersion = async () => { - const version = ((await kbnClient.request({ - path: '/api/status', - method: 'GET', - })) as AxiosResponse).data.version.number; - if (!version) { - // eslint-disable-next-line no-console - console.log('failed to retrieve kibana version'); - } - return version; - }; + // Create a Fake fleet-server in this kibana instance + await esClient.index({ + index: FLEET_SERVER_SERVERS_INDEX, + body: { + agent: { + id: '12988155-475c-430d-ac89-84dc84b67cd1', + version: '', + }, + host: { + architecture: 'linux', + id: 'c3e5f4f690b4a3ff23e54900701a9513', + ip: ['127.0.0.1', '::1', '10.201.0.213', 'fe80::4001:aff:fec9:d5'], + name: 'endpoint-data-generator', + }, + server: { + id: '12988155-475c-430d-ac89-84dc84b67cd1', + version: '8.0.0-SNAPSHOT', + }, + '@timestamp': '2021-05-12T18:42:52.009482058Z', + }, + }); +}; - // Enroll an agent for the Host - const body = { - type: 'PERMANENT', - metadata: { - local: { +const indexFleetAgentForHost = async ( + esClient: Client, + endpointHost: HostMetadata, + agentPolicyId: string, + kibanaVersion: string = '8.0.0' +): Promise> => { + const agentDoc = fleetAgentGenerator.generateEsHit({ + _source: { + local_metadata: { elastic: { agent: { - version: await fetchKibanaVersion(), + version: kibanaVersion, }, }, host: { ...endpointHost.host, }, os: { - family: 'windows', - kernel: '10.0.19041.388 (WinBuild.160101.0800)', - platform: 'windows', - version: '10.0', - name: 'Windows 10 Pro', - full: 'Windows 10 Pro(10.0)', + ...endpointHost.host.os, }, }, - user_provided: { - dev_agent_version: '0.0.1', - region: 'us-east', - }, + policy_id: agentPolicyId, }, - }; - - try { - // First enroll the agent - const res = await kbnClient.requestWithApiKey(AGENT_API_ROUTES.ENROLL_PATTERN, { - method: 'POST', - body: JSON.stringify(body), - headers: { - 'kbn-xsrf': 'xxx', - Authorization: `ApiKey ${enrollmentApiKey}`, - 'Content-Type': 'application/json', - }, - }); - - if (res) { - const enrollObj = await res.json(); - if (!res.ok) { - // eslint-disable-next-line no-console - console.error('unable to enroll agent', enrollObj); - return; - } - // ------------------------------------------------ - // now check the agent in so that it can complete enrollment - const checkinBody = { - events: [ - { - type: 'STATE', - subtype: 'RUNNING', - message: 'state changed from STOPPED to RUNNING', - timestamp: new Date().toISOString(), - payload: { - random: 'data', - state: 'RUNNING', - previous_state: 'STOPPED', - }, - agent_id: enrollObj.item.id, - }, - ], - }; - const checkinRes = await kbnClient - .requestWithApiKey( - AGENT_API_ROUTES.CHECKIN_PATTERN.replace('{agentId}', enrollObj.item.id), - { - method: 'POST', - body: JSON.stringify(checkinBody), - headers: { - 'kbn-xsrf': 'xxx', - Authorization: `ApiKey ${enrollObj.item.access_api_key}`, - 'Content-Type': 'application/json', - }, - } - ) - .catch((error) => { - return Promise.reject(error); - }); - - // Agent unenrolling? - if (checkinRes.status === 403) { - return; - } - - const checkinObj = await checkinRes.json(); - if (!checkinRes.ok) { - // eslint-disable-next-line no-console - console.error( - `failed to checkin agent [${enrollObj.item.id}] for endpoint [${endpointHost.host.id}]` - ); - return enrollObj.item; - } - - // ------------------------------------------------ - // If we have an action to ack(), then do it now - if (checkinObj.actions.length) { - const ackActionBody = { - // @ts-ignore - events: checkinObj.actions.map((action) => { - return { - action_id: action.id, - type: 'ACTION_RESULT', - subtype: 'CONFIG', - timestamp: new Date().toISOString(), - agent_id: action.agent_id, - policy_id: agentPolicyId, - message: `endpoint generator: Endpoint Started`, - }; - }), - }; - const ackActionResp = await kbnClient.requestWithApiKey( - AGENT_API_ROUTES.ACKS_PATTERN.replace('{agentId}', enrollObj.item.id), - { - method: 'POST', - body: JSON.stringify(ackActionBody), - headers: { - 'kbn-xsrf': 'xxx', - Authorization: `ApiKey ${enrollObj.item.access_api_key}`, - 'Content-Type': 'application/json', - }, - } - ); + }); - const ackActionObj = await ackActionResp.json(); - if (!ackActionResp.ok) { - // eslint-disable-next-line no-console - console.error( - `failed to ACK Actions provided to agent [${enrollObj.item.id}] for endpoint [${endpointHost.host.id}]` - ); - // eslint-disable-next-line no-console - console.error(JSON.stringify(ackActionObj, null, 2)); - return enrollObj.item; - } - } + await esClient.index({ + index: agentDoc._index, + id: agentDoc._id, + body: agentDoc._source!, + op_type: 'create', + }); - return enrollObj.item; - } - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - } + return agentDoc; }; 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 f9971dfc0f791..7c09b311807be 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 @@ -6,7 +6,7 @@ */ import { formatMitreAttackDescription } from '../../helpers/rules'; -import { indexPatterns, newThresholdRule } from '../../objects/rule'; +import { indexPatterns, newRule, newThresholdRule } from '../../objects/rule'; import { ALERT_RULE_METHOD, @@ -26,6 +26,7 @@ import { RULES_TABLE, SEVERITY, } from '../../screens/alerts_detection_rules'; +import { PREVIEW_HEADER_SUBTITLE } from '../../screens/create_new_rule'; import { ABOUT_DETAILS, ABOUT_INVESTIGATION_NOTES, @@ -64,13 +65,16 @@ import { goToRuleDetails, waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; +import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; import { createAndActivateRule, fillAboutRuleAndContinue, fillDefineThresholdRuleAndContinue, + fillDefineThresholdRule, fillScheduleRuleAndContinue, + previewResults, selectThresholdRuleType, waitForAlertsToPopulate, waitForTheRuleToBeExecuted, @@ -92,12 +96,12 @@ describe('Detection rules, threshold', () => { createTimeline(newThresholdRule.timeline).then((response) => { rule.timeline.id = response.body.data.persistTimeline.timeline.savedObjectId; }); - }); - - it('Creates and activates a new threshold rule', () => { loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); + }); + + it('Creates and activates a new threshold rule', () => { goToManageAlertsDetectionRules(); waitForRulesTableToBeLoaded(); goToCreateNewRule(); @@ -175,4 +179,19 @@ describe('Detection rules, threshold', () => { cy.get(ALERT_RULE_SEVERITY).first().should('have.text', rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', rule.riskScore); }); + + it('Preview results', () => { + const previewRule = { ...newThresholdRule }; + previewRule.index!.push('.siem-signals*'); + + createCustomRuleActivated(newRule); + goToManageAlertsDetectionRules(); + waitForRulesTableToBeLoaded(); + goToCreateNewRule(); + selectThresholdRuleType(); + fillDefineThresholdRule(previewRule); + previewResults(); + + cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '3 unique hits'); + }); }); 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 db8d93dfbbef9..a580068b636e4 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 @@ -127,6 +127,8 @@ export const MITRE_ATTACK_ADD_TECHNIQUE_BUTTON = '[data-test-subj="addMitreAttac export const MITRE_ATTACK_ADD_SUBTECHNIQUE_BUTTON = '[data-test-subj="addMitreAttackSubtechnique"]'; +export const PREVIEW_HEADER_SUBTITLE = '[data-test-subj="header-panel-subtitle"]'; + export const QUERY_PREVIEW_BUTTON = '[data-test-subj="queryPreviewButton"]'; export const REFERENCE_URLS_INPUT = 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 cd342e9456906..9c15b1f03932d 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 @@ -260,12 +260,18 @@ export const fillScheduleRuleAndContinue = (rule: CustomRule | MachineLearningRu cy.get(LOOK_BACK_TIME_TYPE).select(rule.lookBack.timeType); }; -export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { +export const fillDefineThresholdRule = (rule: ThresholdRule) => { const thresholdField = 0; const threshold = 1; cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); cy.get(TIMELINE(rule.timeline.id!)).click(); + cy.get(COMBO_BOX_CLEAR_BTN).click(); + + rule.index!.forEach((index) => { + cy.get(COMBO_BOX_INPUT).first().type(`${index}{enter}`); + }); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); cy.get(THRESHOLD_INPUT_AREA) .find(INPUT) @@ -274,6 +280,24 @@ export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { cy.get(THRESHOLD_FIELD_SELECTION).click({ force: true }); cy.wrap(inputs[threshold]).clear().type(rule.threshold); }); +}; + +export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { + const thresholdField = 0; + const threshold = 1; + + const typeThresholdField = ($el: Cypress.ObjectLike) => cy.wrap($el).type(rule.thresholdField); + + cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); + cy.get(TIMELINE(rule.timeline.id!)).click(); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); + cy.get(THRESHOLD_INPUT_AREA) + .find(INPUT) + .then((inputs) => { + cy.wrap(inputs[thresholdField]).pipe(typeThresholdField); + cy.get(THRESHOLD_FIELD_SELECTION).click({ force: true }); + cy.wrap(inputs[threshold]).clear().type(rule.threshold); + }); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); @@ -478,6 +502,10 @@ export const selectThresholdRuleType = () => { cy.get(THRESHOLD_TYPE).click({ force: true }); }; +export const previewResults = () => { + cy.get(QUERY_PREVIEW_BUTTON).click(); +}; + export const waitForAlertsToPopulate = async (alertCountThreshold = 1) => { cy.waitUntil( () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index bf15994f60cbc..5ec8999d20518 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -21,7 +21,7 @@ import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_e import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import * as helpers from '../helpers'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema } from '../../../../../../lists/common'; import { @@ -58,13 +58,14 @@ describe('When the add exception modal is opened', () => { ReturnType >; let ExceptionBuilderComponent: jest.SpyInstance< - ReturnType + ReturnType >; beforeEach(() => { + const emptyComp = ; defaultEndpointItems = jest.spyOn(helpers, 'defaultEndpointExceptionItems'); ExceptionBuilderComponent = jest - .spyOn(ExceptionBuilder, 'ExceptionBuilderComponent') - .mockReturnValue(<>); + .spyOn(ExceptionBuilder, 'getExceptionBuilderComponentLazy') + .mockReturnValue(emptyComp); (useAsync as jest.Mock).mockImplementation(() => ({ start: jest.fn(), 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 96335f8d85d90..120c4ad8efc1b 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 @@ -469,28 +469,27 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} - + {ExceptionBuilder.getExceptionBuilderComponentLazy({ + allowLargeValueLists: + !isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type), + httpService: http, + autocompleteService: data.autocomplete, + exceptionListItems: initialExceptionItems, + listType: exceptionListType, + osTypes: osTypesSelection, + listId: ruleExceptionList.list_id, + listNamespaceType: ruleExceptionList.namespace_type, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + ruleName, + indexPatterns, + isOrDisabled: isExceptionBuilderFormDisabled, + isAndDisabled: isExceptionBuilderFormDisabled, + isNestedDisabled: isExceptionBuilderFormDisabled, + dataTestSubj: 'alert-exception-builder', + idAria: 'alert-exception-builder', + onChange: handleBuilderOnChange, + isDisabled: isExceptionBuilderFormDisabled, + })} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index 7ee0e6888a42e..ab6d4b401bb41 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -20,7 +20,7 @@ import { import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; import { getRulesEqlSchemaMock, getRulesSchemaMock, @@ -49,11 +49,11 @@ jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_ jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); jest.mock('../../../../shared_imports', () => { const originalModule = jest.requireActual('../../../../shared_imports'); - + const emptyComp = ; return { ...originalModule, ExceptionBuilder: { - ExceptionBuilderComponent: () => ({} as JSX.Element), + getExceptionBuilderComponentLazy: () => emptyComp, }, }; }); @@ -62,13 +62,14 @@ describe('When the edit exception modal is opened', () => { const ruleName = 'test rule'; let ExceptionBuilderComponent: jest.SpyInstance< - ReturnType + ReturnType >; beforeEach(() => { + const emptyComp = ; ExceptionBuilderComponent = jest - .spyOn(ExceptionBuilder, 'ExceptionBuilderComponent') - .mockReturnValue(<>); + .spyOn(ExceptionBuilder, 'getExceptionBuilderComponentLazy') + .mockReturnValue(emptyComp); (useSignalIndex as jest.Mock).mockReturnValue({ loading: false, 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 8bf5ea9f8a80f..5fb52994fb0f5 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 @@ -342,27 +342,26 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} - + {ExceptionBuilder.getExceptionBuilderComponentLazy({ + allowLargeValueLists: + !isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type), + httpService: http, + autocompleteService: data.autocomplete, + exceptionListItems: [exceptionItem], + listType: exceptionListType, + listId: exceptionItem.list_id, + listNamespaceType: exceptionItem.namespace_type, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + ruleName, + isOrDisabled: true, + isAndDisabled: false, + osTypes: exceptionItem.os_types, + isNestedDisabled: false, + dataTestSubj: 'edit-exception-modal-builder', + idAria: 'edit-exception-modal-builder', + onChange: handleBuilderOnChange, + indexPatterns, + })} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 0560b790e4047..907b30fcaa879 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -56,7 +56,7 @@ import { ENTRIES_WITH_IDS, OLD_DATE_RELATIVE_TO_DATE_NOW, } from '../../../../../lists/common/constants.mock'; -import { EntriesArray, OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { CreateExceptionListItemSchema, ExceptionListItemSchema, diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx index 30ee7e77f3a7d..3897458e8459c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx @@ -21,7 +21,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useHostIsolation } from '../../containers/detection_engine/alerts/use_host_isolation'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; import { CANCEL, CASES_ASSOCIATED_WITH_ALERT, @@ -31,6 +30,9 @@ import { RETURN_TO_ALERT_DETAILS, } from './translations'; import { Maybe } from '../../../../../observability/common/typings'; +import { useCasesFromAlerts } from '../../containers/detection_engine/alerts/use_cases_from_alerts'; +import { CaseDetailsLink } from '../../../common/components/links'; +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; export const HostIsolationPanel = React.memo( ({ @@ -59,7 +61,13 @@ export const HostIsolationPanel = React.memo( return findAlertRule ? findAlertRule[0] : ''; }, [details]); - const { loading, isolateHost } = useHostIsolation({ agentId, comment }); + const alertId = useMemo(() => { + const findAlertId = find({ category: '_id', field: '_id' }, details)?.values; + return findAlertId ? findAlertId[0] : ''; + }, [details]); + + const { caseIds } = useCasesFromAlerts({ alertId }); + const { loading, isolateHost } = useHostIsolation({ agentId, comment, caseIds }); const confirmHostIsolation = useCallback(async () => { const hostIsolated = await isolateHost(); @@ -68,8 +76,25 @@ export const HostIsolationPanel = React.memo( const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]); - // a placeholder until we get the case count returned from a new case route in a future pr - const caseCount: number = 0; + const casesList = useMemo( + () => + caseIds.map((id, index) => { + return ( +
  • + + + +
  • + ); + }), + [caseIds] + ); + + const caseCount: number = useMemo(() => caseIds.length, [caseIds]); const hostIsolated = useMemo(() => { return ( @@ -92,20 +117,13 @@ export const HostIsolationPanel = React.memo(

    -
      -
    • - -
    • -
    +
      {casesList}
    )} @@ -121,7 +139,7 @@ export const HostIsolationPanel = React.memo( ); - }, [backToAlertDetails, hostName]); + }, [backToAlertDetails, hostName, caseCount, casesList]); const hostNotIsolated = useMemo(() => { return ( @@ -137,7 +155,7 @@ export const HostIsolationPanel = React.memo( cases: ( {caseCount} - {CASES_ASSOCIATED_WITH_ALERT} + {CASES_ASSOCIATED_WITH_ALERT(caseCount)} {alertRule} ), @@ -171,7 +189,15 @@ export const HostIsolationPanel = React.memo( ); - }, [alertRule, backToAlertDetails, comment, confirmHostIsolation, hostName, loading]); + }, [ + alertRule, + backToAlertDetails, + comment, + confirmHostIsolation, + hostName, + loading, + caseCount, + ]); return isIsolated ? hostIsolated : hostNotIsolated; } diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts index 97a1a278952a6..8d6334f6c340d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts @@ -31,12 +31,14 @@ export const CONFIRM = i18n.translate('xpack.securitySolution.endpoint.hostIsola defaultMessage: 'Confirm', }); -export const CASES_ASSOCIATED_WITH_ALERT = i18n.translate( - 'xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWihtAlert', - { - defaultMessage: ' cases associated with the rule ', - } -); +export const CASES_ASSOCIATED_WITH_ALERT = (caseCount: number): string => + i18n.translate( + 'xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWithAlert', + { + defaultMessage: ' {caseCount, plural, one {case} other {cases}} associated with the rule ', + values: { caseCount }, + } + ); export const RETURN_TO_ALERT_DETAILS = i18n.translate( 'xpack.securitySolution.endpoint.hostIsolation.returnToAlertDetails', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/__mocks__/api.ts index e1f5b53e2f4c3..ea64f39226cd2 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/__mocks__/api.ts @@ -5,8 +5,15 @@ * 2.0. */ -import { QueryAlerts, AlertSearchResponse, BasicSignals, AlertsIndex, Privilege } from '../types'; -import { alertsMock, mockSignalIndex, mockUserPrivilege } from '../mock'; +import { + QueryAlerts, + AlertSearchResponse, + BasicSignals, + AlertsIndex, + Privilege, + CasesFromAlertsResponse, +} from '../types'; +import { alertsMock, mockSignalIndex, mockUserPrivilege, mockCaseIdsFromAlertId } from '../mock'; export const fetchQueryAlerts = async ({ query, @@ -22,3 +29,9 @@ export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => Promise.resolve(mockSignalIndex); + +export const getCaseIdsFromAlertId = async ({ + alertId, +}: { + alertId: string; +}): Promise => Promise.resolve(mockCaseIdsFromAlertId); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts index 82f275f7dc9ba..9aa5cfd229292 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts @@ -12,6 +12,7 @@ import { mockStatusAlertQuery, mockSignalIndex, mockUserPrivilege, + mockHostIsolation, } from './mock'; import { fetchQueryAlerts, @@ -19,6 +20,7 @@ import { getSignalIndex, getUserPrivilege, createSignalIndex, + createHostIsolation, } from './api'; const abortCtrl = new AbortController(); @@ -163,4 +165,33 @@ describe('Detections Alerts API', () => { expect(alertsResp).toEqual(mockSignalIndex); }); }); + + describe('createHostIsolation', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(mockHostIsolation); + }); + + test('check parameter url', async () => { + await createHostIsolation({ + agentId: 'fd8a122b-4c54-4c05-b295-e5f8381fc59d', + comment: 'commento', + caseIds: ['88c04a90-b19c-11eb-b838-bf3c7840b969'], + }); + expect(fetchMock).toHaveBeenCalledWith('/api/endpoint/isolate', { + method: 'POST', + body: + '{"agent_ids":["fd8a122b-4c54-4c05-b295-e5f8381fc59d"],"comment":"commento","case_ids":["88c04a90-b19c-11eb-b838-bf3c7840b969"]}', + }); + }); + + test('happy path', async () => { + const hostIsolationResponse = await createHostIsolation({ + agentId: 'fd8a122b-4c54-4c05-b295-e5f8381fc59d', + comment: 'commento', + caseIds: ['88c04a90-b19c-11eb-b838-bf3c7840b969'], + }); + expect(hostIsolationResponse).toEqual(mockHostIsolation); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts index dbcb11383432f..300005b23caaa 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts @@ -6,6 +6,7 @@ */ import { UpdateDocumentByQueryResponse } from 'elasticsearch'; +import { getCasesFromAlertsUrl } from '../../../../../../cases/common'; import { HostIsolationResponse } from '../../../../../common/endpoint/types'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL, @@ -22,6 +23,7 @@ import { AlertSearchResponse, AlertsIndex, UpdateAlertStatusProps, + CasesFromAlertsResponse, } from './types'; /** @@ -109,20 +111,38 @@ export const createSignalIndex = async ({ signal }: BasicSignals): Promise => KibanaServices.get().http.fetch(ISOLATE_HOST_ROUTE, { method: 'POST', body: JSON.stringify({ agent_ids: [agentId], comment, + case_ids: caseIds, }), }); + +/** + * Get list of associated case ids from alert id + * + * @param alert id + */ +export const getCaseIdsFromAlertId = async ({ + alertId, +}: { + alertId: string; +}): Promise => + KibanaServices.get().http.fetch(getCasesFromAlertsUrl(alertId), { + method: 'get', + }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts index 18651063df8ca..69358958a395c 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { AlertSearchResponse, AlertsIndex, Privilege } from './types'; +import { HostIsolationResponse } from '../../../../../common/endpoint/types/actions'; +import { AlertSearchResponse, AlertsIndex, Privilege, CasesFromAlertsResponse } from './types'; export const alertsMock: AlertSearchResponse = { took: 7, @@ -1039,3 +1040,12 @@ export const mockUserPrivilege: Privilege = { is_authenticated: true, has_encryption_key: true, }; + +export const mockHostIsolation: HostIsolationResponse = { + action: '713085d6-ab45-4e9e-b41d-96563cafdd97', +}; + +export const mockCaseIdsFromAlertId: CasesFromAlertsResponse = [ + '818601a0-b26b-11eb-8759-6b318e8cf4bc', + '8a774850-b26b-11eb-8759-6b318e8cf4bc', +]; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts index 2998c97376c26..ed6a22375a776 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts @@ -32,3 +32,8 @@ export const HOST_ISOLATION_FAILURE = i18n.translate( 'xpack.securitySolution.endpoint.hostIsolation.failedToIsolate.title', { defaultMessage: 'Failed to isolate host' } ); + +export const CASES_FROM_ALERTS_FAILURE = i18n.translate( + 'xpack.securitySolution.endpoint.hostIsolation.casesFromAlerts.title', + { defaultMessage: 'Failed to find associated cases' } +); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts index 26108ca939a57..52b477d95076b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts @@ -48,6 +48,8 @@ export interface AlertsIndex { index_mapping_outdated: boolean; } +export type CasesFromAlertsResponse = string[]; + export interface Privilege { username: string; has_all_requested: boolean; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx new file mode 100644 index 0000000000000..0867fb001051a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx @@ -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 { renderHook } from '@testing-library/react-hooks'; +import { useCasesFromAlerts } from './use_cases_from_alerts'; +import * as api from './api'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; +import { mockCaseIdsFromAlertId } from './mock'; + +jest.mock('./api'); +jest.mock('../../../../common/hooks/use_app_toasts'); + +describe('useCasesFromAlerts hook', () => { + let appToastsMock: jest.Mocked>; + beforeEach(() => { + jest.resetAllMocks(); + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('returns an array of caseIds', async () => { + const spyOnCases = jest.spyOn(api, 'getCaseIdsFromAlertId'); + const { result, waitForNextUpdate } = renderHook(() => + useCasesFromAlerts({ alertId: 'anAlertId' }) + ); + await waitForNextUpdate(); + expect(spyOnCases).toHaveBeenCalledTimes(1); + expect(result.current).toEqual({ + loading: false, + caseIds: mockCaseIdsFromAlertId, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx new file mode 100644 index 0000000000000..fb130eb744700 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash'; +import { useEffect, useState } from 'react'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { getCaseIdsFromAlertId } from './api'; +import { CASES_FROM_ALERTS_FAILURE } from './translations'; +import { CasesFromAlertsResponse } from './types'; + +interface CasesFromAlertsStatus { + loading: boolean; + caseIds: CasesFromAlertsResponse; +} + +export const useCasesFromAlerts = ({ alertId }: { alertId: string }): CasesFromAlertsStatus => { + const [loading, setLoading] = useState(false); + const [cases, setCases] = useState([]); + const { addError } = useAppToasts(); + + useEffect(() => { + // isMounted tracks if a component is mounted before changing state + let isMounted = true; + setLoading(true); + const fetchData = async () => { + try { + const casesResponse = await getCaseIdsFromAlertId({ alertId }); + if (isMounted) { + setCases(casesResponse); + } + } catch (error) { + addError(error.message, { title: CASES_FROM_ALERTS_FAILURE }); + } + if (isMounted) { + setLoading(false); + } + }; + if (!isEmpty(alertId)) { + fetchData(); + } + return () => { + // updates to show component is unmounted + isMounted = false; + }; + }, [alertId, addError]); + return { loading, caseIds: cases }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx index 684bc6af5d2c7..ad3c6e91c03fe 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx @@ -18,11 +18,13 @@ interface HostIsolationStatus { interface UseHostIsolationProps { agentId: string; comment: string; + caseIds?: string[]; } export const useHostIsolation = ({ agentId, comment, + caseIds, }: UseHostIsolationProps): HostIsolationStatus => { const [loading, setLoading] = useState(false); const { addError } = useAppToasts(); @@ -30,7 +32,7 @@ export const useHostIsolation = ({ const isolateHost = useCallback(async () => { try { setLoading(true); - const isolationStatus = await createHostIsolation({ agentId, comment }); + const isolationStatus = await createHostIsolation({ agentId, comment, caseIds }); setLoading(false); return isolationStatus.action ? true : false; } catch (error) { @@ -38,6 +40,6 @@ export const useHostIsolation = ({ addError(error.message, { title: HOST_ISOLATION_FAILURE }); return false; } - }, [agentId, comment, addError]); + }, [agentId, comment, caseIds, addError]); return { loading, isolateHost }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.test.ts b/x-pack/plugins/security_solution/public/management/common/utils.test.ts similarity index 79% rename from x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.test.ts rename to x-pack/plugins/security_solution/public/management/common/utils.test.ts index 4193d9c37c8f2..59455ccd6bb04 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.test.ts +++ b/x-pack/plugins/security_solution/public/management/common/utils.test.ts @@ -8,27 +8,33 @@ import { parseQueryFilterToKQL } from './utils'; describe('utils', () => { + const searchableFields = [`name`, `description`, `entries.value`, `entries.entries.value`]; describe('parseQueryFilterToKQL', () => { it('should parse simple query without term', () => { - expect(parseQueryFilterToKQL('')).toBe(''); + expect(parseQueryFilterToKQL('', searchableFields)).toBe(''); }); it('should parse simple query with term', () => { - expect(parseQueryFilterToKQL('simpleQuery')).toBe( + expect(parseQueryFilterToKQL('simpleQuery', searchableFields)).toBe( 'exception-list-agnostic.attributes.name:(*simpleQuery*) OR exception-list-agnostic.attributes.description:(*simpleQuery*) OR exception-list-agnostic.attributes.entries.value:(*simpleQuery*) OR exception-list-agnostic.attributes.entries.entries.value:(*simpleQuery*)' ); }); it('should parse complex query with term', () => { - expect(parseQueryFilterToKQL('complex query')).toBe( + expect(parseQueryFilterToKQL('complex query', searchableFields)).toBe( 'exception-list-agnostic.attributes.name:(*complex*query*) OR exception-list-agnostic.attributes.description:(*complex*query*) OR exception-list-agnostic.attributes.entries.value:(*complex*query*) OR exception-list-agnostic.attributes.entries.entries.value:(*complex*query*)' ); }); it('should parse complex query with colon and backslash chars term', () => { - expect(parseQueryFilterToKQL('C:\\tmpes')).toBe( + expect(parseQueryFilterToKQL('C:\\tmpes', searchableFields)).toBe( 'exception-list-agnostic.attributes.name:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.description:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.entries.value:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.entries.entries.value:(*C\\:\\\\tmpes*)' ); }); it('should parse complex query with special chars term', () => { - expect(parseQueryFilterToKQL("this'is%&query{}[]!¿?with.,-+`´special<>ºª@#|·chars")).toBe( + expect( + parseQueryFilterToKQL( + "this'is%&query{}[]!¿?with.,-+`´special<>ºª@#|·chars", + searchableFields + ) + ).toBe( "exception-list-agnostic.attributes.name:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.description:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.entries.value:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.entries.entries.value:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*)" ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.ts b/x-pack/plugins/security_solution/public/management/common/utils.ts similarity index 78% rename from x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.ts rename to x-pack/plugins/security_solution/public/management/common/utils.ts index b14942e626d1b..c8cf761ccaf86 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.ts +++ b/x-pack/plugins/security_solution/public/management/common/utils.ts @@ -5,9 +5,9 @@ * 2.0. */ -export const parseQueryFilterToKQL = (filter: string): string => { +export const parseQueryFilterToKQL = (filter: string, fields: Readonly): string => { if (!filter) return ''; - const kuery = [`name`, `description`, `entries.value`, `entries.entries.value`] + const kuery = fields .map( (field) => `exception-list-agnostic.attributes.${field}:(*${filter diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/search_bar/index.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx similarity index 88% rename from x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/search_bar/index.test.tsx rename to x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx index f12c979c2afe4..6daea8e53282d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/search_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx @@ -28,7 +28,7 @@ describe('Search bar', () => { it('should have a default value', () => { const expectedDefaultValue = 'this is a default value'; const element = mount(getElement(expectedDefaultValue)); - const defaultValue = element.find('[data-test-subj="trustedAppSearchField"]').first().props() + const defaultValue = element.find('[data-test-subj="searchField"]').first().props() .defaultValue; expect(defaultValue).toBe(expectedDefaultValue); }); @@ -38,7 +38,7 @@ describe('Search bar', () => { const element = mount(getElement()); expect(onSearchMock).toHaveBeenCalledTimes(0); const searchFieldProps = element - .find('[data-test-subj="trustedAppSearchField"]') + .find('[data-test-subj="searchField"]') .first() .props() as EuiFieldSearchPropsFake; @@ -53,7 +53,7 @@ describe('Search bar', () => { const element = mount(getElement(expectedDefaultValue)); expect(onSearchMock).toHaveBeenCalledTimes(0); - element.find('[data-test-subj="trustedAppSearchButton"]').first().simulate('click'); + element.find('[data-test-subj="searchButton"]').first().simulate('click'); expect(onSearchMock).toHaveBeenCalledTimes(1); expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx similarity index 78% rename from x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/search_bar/index.tsx rename to x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx index 6fc6b2af4d585..0d4fcf8fec87b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/search_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx @@ -27,23 +27,20 @@ export const SearchBar = memo(({ defaultValue = '', onSearch }) - + - {i18n.translate('xpack.securitySolution.trustedapps.list.search.button', { + {i18n.translate('xpack.securitySolution.management.search.button', { defaultMessage: 'Refresh', })} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts index 882b964d54bb5..5d600f471994b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts @@ -24,4 +24,11 @@ export const EVENT_FILTER_LIST = { type: EVENT_FILTER_LIST_TYPE, }; +export const SEARCHABLE_FIELDS: Readonly = [ + `name`, + `entries.value`, + `entries.entries.value`, + `comments.comment`, +]; + export { ENDPOINT_EVENT_FILTERS_LIST_ID, EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL }; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx index 3457010181b98..86c2f2364961d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx @@ -19,6 +19,3 @@ export const EventFiltersContainer = () => { ); }; -export { EventFiltersService } from './types'; -export { EventFiltersListPageState } from './types'; -export { EventFiltersListPageData } from './types'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts index 0c01cbfc6a24f..6a95ac5c15e83 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts @@ -48,11 +48,13 @@ export class EventFiltersHttpService implements EventFiltersService { page, sortField, sortOrder, + filter, }: Partial<{ page: number; perPage: number; sortField: string; sortOrder: string; + filter: string; }> = {}): Promise { const http = await this.httpWrapper(); return http.get(`${EXCEPTION_LIST_ITEM_URL}/_find`, { @@ -63,6 +65,7 @@ export class EventFiltersHttpService implements EventFiltersService { sort_order: sortOrder, list_id: [ENDPOINT_EVENT_FILTERS_LIST_ID], namespace_type: ['agnostic'], + filter, }, }); } @@ -89,4 +92,13 @@ export class EventFiltersHttpService implements EventFiltersService { body: JSON.stringify(exception), }); } + + async deleteOne(id: string): Promise { + return (await this.httpWrapper()).delete(EXCEPTION_LIST_ITEM_URL, { + query: { + id, + namespace_type: 'agnostic', + }, + }); + } } diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts index ece1e7385b67b..4ae90e7abba90 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts @@ -22,6 +22,18 @@ export type EventFiltersListPageDataExistsChanged = Action<'eventFiltersListPage payload: EventFiltersListPageState['listPage']['dataExist']; }; +export type EventFilterForDeletion = Action<'eventFilterForDeletion'> & { + payload: ExceptionListItemSchema; +}; + +export type EventFilterDeletionReset = Action<'eventFilterDeletionReset'>; + +export type EventFilterDeleteSubmit = Action<'eventFilterDeleteSubmit'>; + +export type EventFilterDeleteStatusChanged = Action<'eventFilterDeleteStatusChanged'> & { + payload: EventFiltersListPageState['listPage']['deletion']['status']; +}; + export type EventFiltersInitForm = Action<'eventFiltersInitForm'> & { payload: { entry: UpdateExceptionListItemSchema | CreateExceptionListItemSchema; @@ -65,4 +77,8 @@ export type EventFiltersPageAction = | EventFiltersCreateStart | EventFiltersCreateSuccess | EventFiltersCreateError - | EventFiltersFormStateChanged; + | EventFiltersFormStateChanged + | EventFilterForDeletion + | EventFilterDeletionReset + | EventFilterDeleteSubmit + | EventFilterDeleteStatusChanged; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts index 5a8cb4834c4b5..30722a33270ee 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts @@ -7,6 +7,7 @@ import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants'; import { EventFiltersListPageState } from '../types'; +import { createLoadedResourceState, createUninitialisedResourceState } from '../../../state'; export const initialEventFiltersPageState = (): EventFiltersListPageState => ({ entries: [], @@ -16,7 +17,7 @@ export const initialEventFiltersPageState = (): EventFiltersListPageState => ({ hasItemsError: false, hasOSError: false, newComment: '', - submissionResourceState: { type: 'UninitialisedResourceState' }, + submissionResourceState: createUninitialisedResourceState(), }, location: { page_index: MANAGEMENT_DEFAULT_PAGE, @@ -26,8 +27,12 @@ export const initialEventFiltersPageState = (): EventFiltersListPageState => ({ listPage: { active: false, forceRefresh: false, - data: { type: 'UninitialisedResourceState' }, - /** We started off assuming data exists, until we can confirm othewise */ - dataExist: { type: 'LoadedResourceState', data: true }, + data: createUninitialisedResourceState(), + /** We started off assuming data exists, until we can confirm otherwise */ + dataExist: createLoadedResourceState(true), + deletion: { + item: undefined, + status: createUninitialisedResourceState(), + }, }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts index 787447797158d..b55a32a937c45 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts @@ -28,6 +28,7 @@ const createEventFiltersServiceMock = (): jest.Mocked => ({ getList: jest.fn(), getOne: jest.fn(), updateOne: jest.fn(), + deleteOne: jest.fn(), }); const createStoreSetup = (eventFiltersService: EventFiltersService) => { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts index 70afcaf43eabd..b32b39fb9293c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts @@ -14,8 +14,6 @@ import { import { EventFiltersHttpService } from '../service'; -import { getLastLoadedResourceState } from '../../../state/async_resource_state'; - import { CreateExceptionListItemSchema, ExceptionListItemSchema, @@ -33,7 +31,13 @@ import { getFormEntry, getSubmissionResource, getNewComment, + isDeletionInProgress, + getItemToDelete, + getDeletionState, } from './selector'; + +import { parseQueryFilterToKQL } from '../../../common/utils'; +import { SEARCHABLE_FIELDS } from '../constants'; import { EventFiltersListPageData, EventFiltersListPageState, @@ -44,7 +48,9 @@ import { createFailedResourceState, createLoadedResourceState, createLoadingResourceState, + getLastLoadedResourceState, } from '../../../state'; +import { ServerApiError } from '../../../../common/types'; const addNewComments = ( entry: UpdateExceptionListItemSchema | CreateExceptionListItemSchema, @@ -233,12 +239,13 @@ const refreshListDataIfNeeded: MiddlewareActionHandler = async (store, eventFilt }, }); - const { page_size: pageSize, page_index: pageIndex } = getCurrentLocation(state); + const { page_size: pageSize, page_index: pageIndex, filter } = getCurrentLocation(state); const query: EventFiltersServiceGetListOptions = { page: pageIndex + 1, perPage: pageSize, sortField: 'created_at', sortOrder: 'desc', + filter: parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined, }; try { @@ -275,6 +282,43 @@ const refreshListDataIfNeeded: MiddlewareActionHandler = async (store, eventFilt } }; +const eventFilterDeleteEntry: MiddlewareActionHandler = async ( + { getState, dispatch }, + eventFiltersService +) => { + const state = getState(); + + if (isDeletionInProgress(state)) { + return; + } + + const itemId = getItemToDelete(state)?.id; + + if (!itemId) { + return; + } + + dispatch({ + type: 'eventFilterDeleteStatusChanged', + // Ignore will be fixed with when AsyncResourceState is refactored (#830) + // @ts-ignore + payload: createLoadingResourceState(getDeletionState(state).status), + }); + + try { + const response = await eventFiltersService.deleteOne(itemId); + dispatch({ + type: 'eventFilterDeleteStatusChanged', + payload: createLoadedResourceState(response), + }); + } catch (e) { + dispatch({ + type: 'eventFilterDeleteStatusChanged', + payload: createFailedResourceState(e.body ?? e), + }); + } +}; + export const createEventFiltersPageMiddleware = ( eventFiltersService: EventFiltersService ): ImmutableMiddleware => { @@ -294,9 +338,12 @@ export const createEventFiltersPageMiddleware = ( if ( action.type === 'userChangedUrl' || action.type === 'eventFiltersCreateSuccess' || - action.type === 'eventFiltersUpdateSuccess' + action.type === 'eventFiltersUpdateSuccess' || + action.type === 'eventFilterDeleteStatusChanged' ) { refreshListDataIfNeeded(store, eventFiltersService); + } else if (action.type === 'eventFilterDeleteSubmit') { + eventFilterDeleteEntry(store, eventFiltersService); } } }; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts index 323d063f31b87..d69efb689c877 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts @@ -14,7 +14,10 @@ import { AppLocation, Immutable } from '../../../../../common/endpoint/types'; import { UserChangedUrl } from '../../../../common/store/routing/action'; import { MANAGEMENT_ROUTING_EVENT_FILTERS_PATH } from '../../../common/constants'; import { extractEventFiltetrsPageLocation } from '../../../common/routing'; -import { isUninitialisedResourceState } from '../../../state/async_resource_state'; +import { + isLoadedResourceState, + isUninitialisedResourceState, +} from '../../../state/async_resource_state'; import { EventFiltersInitForm, @@ -24,6 +27,9 @@ import { EventFiltersUpdateSuccess, EventFiltersListPageDataChanged, EventFiltersListPageDataExistsChanged, + EventFilterForDeletion, + EventFilterDeletionReset, + EventFilterDeleteStatusChanged, } from './action'; import { initialEventFiltersPageState } from './builders'; @@ -173,6 +179,46 @@ const userChangedUrl: CaseReducer = (state, action) => { } }; +const handleEventFilterForDeletion: CaseReducer = (state, action) => { + return { + ...state, + listPage: { + ...state.listPage, + deletion: { + ...state.listPage.deletion, + item: action.payload, + }, + }, + }; +}; + +const handleEventFilterDeletionReset: CaseReducer = (state) => { + return { + ...state, + listPage: { + ...state.listPage, + deletion: initialEventFiltersPageState().listPage.deletion, + }, + }; +}; + +const handleEventFilterDeleteStatusChanges: CaseReducer = ( + state, + action +) => { + return { + ...state, + listPage: { + ...state.listPage, + forceRefresh: isLoadedResourceState(action.payload) ? true : state.listPage.forceRefresh, + deletion: { + ...state.listPage.deletion, + status: action.payload, + }, + }, + }; +}; + export const eventFiltersPageReducer: StateReducer = ( state = initialEventFiltersPageState(), action @@ -199,6 +245,12 @@ export const eventFiltersPageReducer: StateReducer = ( return handleEventFiltersListPageDataChanges(state, action); case 'eventFiltersListPageDataExistsChanged': return handleEventFiltersListPageDataExistChanges(state, action); + case 'eventFilterForDeletion': + return handleEventFilterForDeletion(state, action); + case 'eventFilterDeletionReset': + return handleEventFilterDeletionReset(state, action); + case 'eventFilterDeleteStatusChanged': + return handleEventFilterDeleteStatusChanges(state, action); } } diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts index 368faae5fd7a2..e6f67f7329e6a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts @@ -178,7 +178,47 @@ export const listDataNeedsRefresh: EventFiltersSelector = createSelecto return ( forceRefresh || location.page_index + 1 !== currentQuery.page || - location.page_size !== currentQuery.perPage + location.page_size !== currentQuery.perPage || + (!!location.filter && location.filter !== currentQuery.filter) ); } ); + +export const getDeletionState = createSelector( + getCurrentListPageState, + (listState) => listState.deletion +); + +export const showDeleteModal: EventFiltersSelector = createSelector( + getDeletionState, + ({ item }) => { + return Boolean(item); + } +); + +export const getItemToDelete: EventFiltersSelector< + StoreState['listPage']['deletion']['item'] +> = createSelector(getDeletionState, ({ item }) => item); + +export const isDeletionInProgress: EventFiltersSelector = createSelector( + getDeletionState, + ({ status }) => { + return isLoadingResourceState(status); + } +); + +export const wasDeletionSuccessful: EventFiltersSelector = createSelector( + getDeletionState, + ({ status }) => { + return isLoadedResourceState(status); + } +); + +export const getDeleteError: EventFiltersSelector = createSelector( + getDeletionState, + ({ status }) => { + if (isFailedResourceState(status)) { + return status.error; + } + } +); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts index 0074141fe8474..cba069775a90f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts @@ -46,14 +46,14 @@ describe('event filters selectors', () => { const setToLoadedState = () => { initialState.listPage.data = createLoadedResourceState({ - query: { page: 2, perPage: 10 }, + query: { page: 2, perPage: 10, filter: '' }, content: getFoundExceptionListItemSchemaMock(), }); }; const setToLoadingState = ( previousState: EventFiltersListPageState['listPage']['data'] = createLoadedResourceState({ - query: { page: 5, perPage: 50 }, + query: { page: 5, perPage: 50, filter: '' }, content: getFoundExceptionListItemSchemaMock(), }) ) => { @@ -127,12 +127,12 @@ describe('event filters selectors', () => { it('should return query from current loaded state', () => { setToLoadedState(); - expect(getCurrentListItemsQuery(initialState)).toEqual({ page: 2, perPage: 10 }); + expect(getCurrentListItemsQuery(initialState)).toEqual({ page: 2, perPage: 10, filter: '' }); }); it('should return query from previous state while Loading new page', () => { setToLoadingState(); - expect(getCurrentListItemsQuery(initialState)).toEqual({ page: 5, perPage: 50 }); + expect(getCurrentListItemsQuery(initialState)).toEqual({ page: 5, perPage: 50, filter: '' }); }); }); @@ -236,6 +236,11 @@ describe('event filters selectors', () => { initialState.location.page_index = 10; expect(listDataNeedsRefresh(initialState)).toBe(true); }); + + it('should should return true if filter param differ from last api call', () => { + initialState.location.filter = 'query'; + expect(listDataNeedsRefresh(initialState)).toBe(true); + }); }); describe('getFormEntry()', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts index faef3b4976c79..cc70a2037a5af 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts @@ -37,6 +37,7 @@ export type EventFiltersServiceGetListOptions = Partial<{ perPage: number; sortField: keyof ExceptionListItemSchema; sortOrder: 'asc' | 'desc'; + filter: string; }>; export interface EventFiltersService { @@ -47,6 +48,7 @@ export interface EventFiltersService { getList(options?: EventFiltersServiceGetListOptions): Promise; getOne(id: string): Promise; updateOne(exception: Immutable): Promise; + deleteOne(id: string): Promise; } export interface EventFiltersListPageData { @@ -67,5 +69,10 @@ export interface EventFiltersListPageState { data: AsyncResourceState; /** tracks if the overall list (not filtered or with invalid page numbers) contains data */ dataExist: AsyncResourceState; + /** state for deletion of items from the list */ + deletion: { + item: ExceptionListItemSchema | undefined; + status: AsyncResourceState; + }; }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx new file mode 100644 index 0000000000000..cec3e34d9c98f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../common/mock/endpoint'; +import { act } from '@testing-library/react'; +import React from 'react'; +import { EventFilterDeleteModal } from './event_filter_delete_modal'; +import { fireEvent } from '@testing-library/dom'; +import { showDeleteModal } from '../../store/selector'; +import { isFailedResourceState, isLoadedResourceState } from '../../../../state'; + +describe('When event filters delete modal is shown', () => { + let renderAndSetup: () => Promise>; + let renderResult: ReturnType; + let coreStart: AppContextTestRender['coreStart']; + let history: AppContextTestRender['history']; + let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; + let store: AppContextTestRender['store']; + + const getBody = () => + renderResult.baseElement.querySelector('[data-test-subj="eventFilterDeleteModalBody"]')!; + + const getConfirmButton = () => + renderResult.baseElement.querySelector( + '[data-test-subj="eventFilterDeleteModalConfirmButton"]' + )! as HTMLButtonElement; + + const getCancelButton = () => + renderResult.baseElement.querySelector( + '[data-test-subj="eventFilterDeleteModalCancelButton"]' + )! as HTMLButtonElement; + + const getCurrentState = () => store.getState().management.eventFilters; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + + ({ history, store, coreStart } = mockedContext); + renderAndSetup = async () => { + renderResult = mockedContext.render(); + + await act(async () => { + history.push('/event_filters'); + + await waitForAction('userChangedUrl'); + + mockedContext.store.dispatch({ + type: 'eventFilterForDeletion', + payload: { + id: '123', + name: 'tic-tac-toe', + }, + }); + }); + + return renderResult; + }; + + waitForAction = mockedContext.middlewareSpy.waitForAction; + mockedContext.setExperimentalFlag({ eventFilteringEnabled: true }); + }); + + it('should display name of event filter in body message', async () => { + await renderAndSetup(); + expect(getBody().textContent).toMatch(/You are removing event filter "tic-tac-toe"/); + }); + + it('should close dialog if cancel button is clicked', async () => { + await renderAndSetup(); + act(() => { + fireEvent.click(getCancelButton()); + }); + + expect(showDeleteModal(getCurrentState())).toBe(false); + }); + + it('should close dialog if the close X button is clicked', async () => { + await renderAndSetup(); + const dialogCloseButton = renderResult.baseElement.querySelector( + '[aria-label="Closes this modal window"]' + )!; + act(() => { + fireEvent.click(dialogCloseButton); + }); + + expect(showDeleteModal(getCurrentState())).toBe(false); + }); + + it('should disable action buttons when confirmed', async () => { + await renderAndSetup(); + act(() => { + fireEvent.click(getConfirmButton()); + }); + + expect(getCancelButton().disabled).toBe(true); + expect(getConfirmButton().disabled).toBe(true); + }); + + it('should set confirm button to loading', async () => { + await renderAndSetup(); + act(() => { + fireEvent.click(getConfirmButton()); + }); + + expect(getConfirmButton().querySelector('.euiLoadingSpinner')).not.toBeNull(); + }); + + it('should show success toast', async () => { + await renderAndSetup(); + const updateCompleted = waitForAction('eventFilterDeleteStatusChanged', { + validate(action) { + return isLoadedResourceState(action.payload); + }, + }); + + await act(async () => { + fireEvent.click(getConfirmButton()); + await updateCompleted; + }); + + expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith( + '"tic-tac-toe" has been removed from the Event Filters list.' + ); + }); + + it('should show error toast if error is countered', async () => { + coreStart.http.delete.mockRejectedValue(new Error('oh oh')); + await renderAndSetup(); + const updateFailure = waitForAction('eventFilterDeleteStatusChanged', { + validate(action) { + return isFailedResourceState(action.payload); + }, + }); + + await act(async () => { + fireEvent.click(getConfirmButton()); + await updateFailure; + }); + + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith( + 'Unable to remove "tic-tac-toe" from the Event Filters list. Reason: oh oh' + ); + expect(showDeleteModal(getCurrentState())).toBe(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.tsx new file mode 100644 index 0000000000000..74a023965a57d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback, useEffect } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; +import { i18n } from '@kbn/i18n'; +import { useEventFiltersSelector } from '../hooks'; +import { + getDeleteError, + getItemToDelete, + isDeletionInProgress, + wasDeletionSuccessful, +} from '../../store/selector'; +import { AppAction } from '../../../../../common/store/actions'; +import { useToasts } from '../../../../../common/lib/kibana'; + +export const EventFilterDeleteModal = memo<{}>(() => { + const dispatch = useDispatch>(); + const toasts = useToasts(); + + const isDeleting = useEventFiltersSelector(isDeletionInProgress); + const eventFilter = useEventFiltersSelector(getItemToDelete); + const wasDeleted = useEventFiltersSelector(wasDeletionSuccessful); + const deleteError = useEventFiltersSelector(getDeleteError); + + const onCancel = useCallback(() => { + dispatch({ type: 'eventFilterDeletionReset' }); + }, [dispatch]); + + const onConfirm = useCallback(() => { + dispatch({ type: 'eventFilterDeleteSubmit' }); + }, [dispatch]); + + // Show toast for success + useEffect(() => { + if (wasDeleted) { + toasts.addSuccess( + i18n.translate('xpack.securitySolution.eventFilters.deletionDialog.deleteSuccess', { + defaultMessage: '"{name}" has been removed from the Event Filters list.', + values: { name: eventFilter?.name }, + }) + ); + + dispatch({ type: 'eventFilterDeletionReset' }); + } + }, [dispatch, eventFilter?.name, toasts, wasDeleted]); + + // show toast for failures + useEffect(() => { + if (deleteError) { + toasts.addDanger( + i18n.translate('xpack.securitySolution.eventFilters.deletionDialog.deleteFailure', { + defaultMessage: + 'Unable to remove "{name}" from the Event Filters list. Reason: {message}', + values: { name: eventFilter?.name, message: deleteError.message }, + }) + ); + } + }, [deleteError, eventFilter?.name, toasts]); + + return ( + + + + + + + + + +

    + {eventFilter?.name} }} + /> +

    +

    + +

    +
    +
    + + + + + + + + + + +
    + ); +}); + +EventFilterDeleteModal.displayName = 'EventFilterDeleteModal'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx index 940882d079a12..0867d0542e4c1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx @@ -17,6 +17,7 @@ import { createGlobalNoMiddlewareStore, ecsEventMock } from '../../../test_utils import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock'; import { NAME_ERROR, NAME_PLACEHOLDER } from './translations'; import { useCurrentUser, useKibana } from '../../../../../../common/lib/kibana'; +import { ExceptionBuilder } from '../../../../../../shared_imports'; jest.mock('../../../../../../common/lib/kibana'); jest.mock('../../../../../../common/containers/source'); @@ -53,6 +54,9 @@ describe('Event filter form', () => { }; beforeEach(() => { + const emptyComp = ; + jest.spyOn(ExceptionBuilder, 'getExceptionBuilderComponentLazy').mockReturnValue(emptyComp); + (useFetchIndex as jest.Mock).mockImplementation(() => [ false, { @@ -77,7 +81,7 @@ describe('Event filter form', () => { it('should renders correctly with data', () => { component = renderComponentWithdata(); - expect(component.getByText(ecsEventMock().process!.executable![0])).not.toBeNull(); + expect(component.getByTestId('alert-exception-builder')).not.toBeNull(); expect(component.getByText(NAME_ERROR)).not.toBeNull(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx index 744fb9930321d..d74baab0d2bbc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx @@ -115,26 +115,25 @@ export const EventFiltersForm: React.FC = memo( ); const exceptionBuilderComponentMemo = useMemo( - () => ( - - ), + () => + ExceptionBuilder.getExceptionBuilderComponentLazy({ + allowLargeValueLists: true, + httpService: http, + autocompleteService: data.autocomplete, + exceptionListItems: [exception as ExceptionListItemSchema], + listType: EVENT_FILTER_LIST_TYPE, + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + listNamespaceType: 'agnostic', + ruleName: RULE_NAME, + indexPatterns, + isOrDisabled: true, // TODO: pending to be validated + isAndDisabled: false, + isNestedDisabled: false, + dataTestSubj: 'alert-exception-builder', + idAria: 'alert-exception-builder', + onChange: handleOnBuilderChange, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + }), [data, handleOnBuilderChange, http, indexPatterns, exception] ); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx index 4604a5b112a04..2fbabad746cad 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx @@ -140,5 +140,40 @@ describe('When on the Event Filters List Page', () => { expect(renderResult.getByTestId('eventFiltersContent-error').textContent).toEqual(' oh no'); }); + + it('should show modal when delete is clicked on a card', async () => { + render(); + await dataReceived(); + act(() => { + fireEvent.click(renderResult.getByTestId('exceptionsViewerDeleteBtn')); + }); + + expect( + renderResult.baseElement.querySelector('[data-test-subj="eventFilterDeleteModalHeader"]') + ).not.toBeNull(); + }); + }); + + describe('And search is dispatched', () => { + beforeEach(async () => { + act(() => { + history.push('/event_filters?filter=test'); + }); + renderResult = render(); + await act(async () => { + await waitForAction('eventFiltersListPageDataChanged'); + }); + }); + + it('search bar is filled with query params', () => { + expect(renderResult.getByDisplayValue('test')).not.toBeNull(); + }); + + it('search action is dispatched', async () => { + await act(async () => { + fireEvent.click(renderResult.getByTestId('searchButton')); + expect(await waitForAction('userChangedUrl')).not.toBeNull(); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index 3026ca41f327d..0898f93cf6f9b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -11,7 +11,7 @@ import { Dispatch } from 'redux'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton } from '@elastic/eui'; +import { EuiButton, EuiSpacer, EuiHorizontalRule, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { AppAction } from '../../../../common/store/actions'; @@ -30,6 +30,7 @@ import { getListPageDoesDataExist, getActionError, getFormEntry, + showDeleteModal, } from '../store/selector'; import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content'; import { ExceptionListItemSchema } from '../../../../../../lists/common'; @@ -38,6 +39,9 @@ import { ExceptionItem, ExceptionItemProps, } from '../../../../common/components/exceptions/viewer/exception_item'; +import { EventFilterDeleteModal } from './components/event_filter_delete_modal'; + +import { SearchBar } from '../../../components/search_bar'; type EventListPaginatedContent = PaginatedContentProps< Immutable, @@ -65,6 +69,7 @@ export const EventFiltersListPage = memo(() => { const fetchError = useEventFiltersSelector(getListFetchError); const location = useEventFiltersSelector(getCurrentLocation); const doesDataExist = useEventFiltersSelector(getListPageDoesDataExist); + const showDelete = useEventFiltersSelector(showDeleteModal); const navigateCallback = useEventFiltersNavigateCallback(); const showFlyout = !!location.show; @@ -126,9 +131,16 @@ export const EventFiltersListPage = memo(() => { [navigateCallback] ); - const handleItemDelete: ExceptionItemProps['onDeleteException'] = useCallback((args) => { - // TODO: implement delete item - }, []); + const handleItemDelete: ExceptionItemProps['onDeleteException'] = useCallback( + ({ id }) => { + dispatch({ + type: 'eventFilterForDeletion', + // Casting below needed due to error around the comments array needing to be mutable + payload: listItems.find((item) => item.id === id)! as ExceptionListItemSchema, + }); + }, + [dispatch, listItems] + ); const handleItemComponentProps: EventListPaginatedContent['itemComponentProps'] = useCallback( (exceptionItem) => ({ @@ -154,6 +166,10 @@ export const EventFiltersListPage = memo(() => { [navigateCallback] ); + const handleOnSearch = useCallback((query: string) => navigateCallback({ filter: query }), [ + navigateCallback, + ]); + return ( { /> )} + {showDelete && } + + {doesDataExist && ( + <> + + + + + + + + )} + , typeof ExceptionItem> items={listItems} ItemComponent={ExceptionItem} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts new file mode 100644 index 0000000000000..0602ae18c1408 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const SEARCHABLE_FIELDS: Readonly = [ + `name`, + `description`, + `entries.value`, + `entries.entries.value`, +]; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts index 71a49caf66fd6..878938aa20e1b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -64,8 +64,9 @@ import { getListItems, editItemState, } from './selectors'; -import { parseQueryFilterToKQL } from './utils'; +import { parseQueryFilterToKQL } from '../../../common/utils'; import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app'; +import { SEARCHABLE_FIELDS } from '../constants'; const createTrustedAppsListResourceStateChangedAction = ( newState: Immutable> @@ -97,7 +98,7 @@ const refreshListIfNeeded = async ( const response = await trustedAppsService.getTrustedAppsList({ page: pageIndex + 1, per_page: pageSize, - kuery: parseQueryFilterToKQL(filter) || undefined, + kuery: parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined, }); store.dispatch( diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index 5efe8cfc16185..874d1a4a969c4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -882,7 +882,7 @@ describe('When on the Trusted Apps Page', () => { it('search action is dispatched', async () => { await act(async () => { - fireEvent.click(renderResult.getByTestId('trustedAppSearchButton')); + fireEvent.click(renderResult.getByTestId('searchButton')); expect(await waitForAction('userChangedUrl')).not.toBeNull(); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 1bf44769c15b4..39432f2d06505 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -39,7 +39,7 @@ import { TrustedAppsListPageRouteState } from '../../../../../common/endpoint/ty import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { ABOUT_TRUSTED_APPS } from './translations'; import { EmptyState } from './components/empty_state'; -import { SearchBar } from './components/search_bar'; +import { SearchBar } from '../../../components/search_bar'; export const TrustedAppsPage = memo(() => { const { state: routeState } = useLocation(); diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts index 7d345b084aca5..cadb3b91f66a6 100644 --- a/x-pack/plugins/security_solution/public/management/types.ts +++ b/x-pack/plugins/security_solution/public/management/types.ts @@ -10,7 +10,7 @@ import { SecurityPageName } from '../app/types'; import { PolicyDetailsState } from './pages/policy/types'; import { EndpointState } from './pages/endpoint_hosts/types'; import { TrustedAppsListPageState } from './pages/trusted_apps/state'; -import { EventFiltersListPageState } from './pages/event_filters'; +import { EventFiltersListPageState } from './pages/event_filters/types'; /** * The type for the management store global namespace. Used mostly internally to reference diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 015c4c0b45949..0824dea0803ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -115,6 +115,11 @@ const ActionsComponent: React.FC = ({ ); const eventType = getEventType(ecsData); + const isEventContextMenuEnabled = useMemo( + () => isEventFilteringEnabled && !!ecsData.event?.kind && ecsData.event?.kind[0] === 'event', + [ecsData.event?.kind, isEventFilteringEnabled] + ); + return ( <> {showCheckboxes && ( @@ -197,7 +202,7 @@ const ActionsComponent: React.FC = ({ key="alert-context-menu" ecsRowData={ecsData} timelineId={timelineId} - disabled={eventType !== 'signal' && (!isEventFilteringEnabled || eventType !== 'raw')} + disabled={eventType !== 'signal' && !isEventContextMenuEnabled} refetch={refetch} onRuleChange={onRuleChange} /> diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index ee6962f7e9535..5b4aed35bbc7c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -9,7 +9,7 @@ import { ExceptionListClient } from '../../../../../lists/server'; import { listMock } from '../../../../../lists/server/mocks'; import { getFoundExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray, EntryList } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray, EntryList } from '@kbn/securitysolution-io-ts-list-types'; import { buildArtifact, getEndpointExceptionList, diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index 7a5b906860f10..f3bc195b5a896 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -7,7 +7,7 @@ import { createHash } from 'crypto'; import { deflate } from 'zlib'; -import { Entry, EntryNested } from '@kbn/securitysolution-io-ts-utils'; +import { Entry, EntryNested } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { validate } from '../../../../common/validate'; diff --git a/x-pack/plugins/security_solution/server/index.ts b/x-pack/plugins/security_solution/server/index.ts index a4b9dddec812e..5b95ddf13e033 100644 --- a/x-pack/plugins/security_solution/server/index.ts +++ b/x-pack/plugins/security_solution/server/index.ts @@ -48,17 +48,4 @@ export const config: PluginConfigDescriptor = { export { ConfigType, Plugin, PluginSetup, PluginStart }; export { AppClient }; -// Exports to be shared with plugins such as x-pack/lists plugin -export { deleteTemplate } from './lib/detection_engine/index/delete_template'; -export { deletePolicy } from './lib/detection_engine/index/delete_policy'; -export { deleteAllIndex } from './lib/detection_engine/index/delete_all_index'; -export { setPolicy } from './lib/detection_engine/index/set_policy'; -export { setTemplate } from './lib/detection_engine/index/set_template'; -export { getTemplateExists } from './lib/detection_engine/index/get_template_exists'; -export { getPolicyExists } from './lib/detection_engine/index/get_policy_exists'; -export { createBootstrapIndex } from './lib/detection_engine/index/create_bootstrap_index'; -export { getIndexExists } from './lib/detection_engine/index/get_index_exists'; -export { buildRouteValidation } from './utils/build_validation/route_validation'; -export { transformError, buildSiemResponse } from './lib/detection_engine/routes/utils'; -export { readPrivileges } from './lib/detection_engine/privileges/read_privileges'; export type { AppRequestContext } from './types'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/create_bootstrap_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/create_bootstrap_index.ts index fd9b63152ddd3..02f8f3f7b36ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/create_bootstrap_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/create_bootstrap_index.ts @@ -10,6 +10,10 @@ import { ElasticsearchClient } from 'kibana/server'; // See the reference(s) below on explanations about why -000001 was chosen and // why the is_write_index is true as well as the bootstrapping step which is needed. // Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/applying-policy-to-template.html + +/** + * @deprecated Use the one from kbn-securitysolution-es-utils + */ export const createBootstrapIndex = async ( esClient: ElasticsearchClient, index: string diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts index 48bbbdcbf3a48..d76290921fac8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts @@ -7,6 +7,9 @@ import { ElasticsearchClient } from 'kibana/server'; +/** + * @deprecated Use the one from kbn-securitysolution-es-utils + */ export const deleteAllIndex = async ( esClient: ElasticsearchClient, pattern: string, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_policy.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_policy.ts index d671d256f56aa..924970d304c88 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_policy.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_policy.ts @@ -7,6 +7,9 @@ import { ElasticsearchClient } from 'kibana/server'; +/** + * @deprecated Use the one from kbn-securitysolution-es-utils + */ export const deletePolicy = async ( esClient: ElasticsearchClient, policy: string diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_template.ts index e57bbd77120f2..5466fd03f534c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_template.ts @@ -6,6 +6,9 @@ */ import { ElasticsearchClient } from 'kibana/server'; +/** + * @deprecated Use the one from kbn-securitysolution-es-utils + */ export const deleteTemplate = async ( esClient: ElasticsearchClient, name: string diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts index cc7f22064572c..7ca7f9818ba0b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts @@ -7,6 +7,9 @@ import { ElasticsearchClient } from 'kibana/server'; +/** + * @deprecated Use the one from kbn-securitysolution-es-utils + */ export const getIndexExists = async ( esClient: ElasticsearchClient, index: string diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_policy_exists.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_policy_exists.ts index c0d7c38a4bb02..6ebdac0d244cb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_policy_exists.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_policy_exists.ts @@ -7,6 +7,9 @@ import { ElasticsearchClient } from 'kibana/server'; +/** + * @deprecated Use the one from kbn-securitysolution-es-utils + */ export const getPolicyExists = async ( esClient: ElasticsearchClient, policy: string diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_template_exists.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_template_exists.ts index 50ec3bfc670d5..af5f874a05688 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_template_exists.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_template_exists.ts @@ -7,6 +7,9 @@ import { ElasticsearchClient } from 'kibana/server'; +/** + * @deprecated Use the one from kbn-securitysolution-es-utils + */ export const getTemplateExists = async ( esClient: ElasticsearchClient, template: string diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_policy.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_policy.ts index 9dbcdd795ac71..113b9d368e0d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_policy.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_policy.ts @@ -7,6 +7,9 @@ import { ElasticsearchClient } from 'kibana/server'; +/** + * @deprecated Use the one from kbn-securitysolution-es-utils + */ export const setPolicy = async ( esClient: ElasticsearchClient, policy: string, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_template.ts index e63dbbd6c3e8f..288377c306325 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_template.ts @@ -7,6 +7,9 @@ import { ElasticsearchClient } from 'kibana/server'; +/** + * @deprecated Use the one from kbn-securitysolution-es-utils + */ export const setTemplate = async ( esClient: ElasticsearchClient, name: string, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts index cedf1744ab06e..c2acbf9c5cc0a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts @@ -25,6 +25,9 @@ export interface OutputError { statusCode: number; } +/** + * @deprecated Use kbn-securitysolution-es-utils version + */ export const transformError = (err: Error & Partial): OutputError => { if (Boom.isBoom(err)) { return { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts index 58df4b3f11412..f50f0b521ed76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { entriesList } from '@kbn/securitysolution-io-ts-utils'; +import { entriesList } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas'; import { hasLargeValueList } from '../../../../../common/detection_engine/utils'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a2eb51e929fd4..244b294baffe5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8156,6 +8156,7 @@ "xpack.enterpriseSearch.appSearch.engine.sampleEngineBadge": "サンプルエンジン", "xpack.enterpriseSearch.appSearch.engine.schema.conflicts": "スキーマ競合", "xpack.enterpriseSearch.appSearch.engine.schema.title": "スキーマ", + "xpack.enterpriseSearch.appSearch.engine.schema.unconfirmedFieldLabel": "最近追加された項目", "xpack.enterpriseSearch.appSearch.engine.schema.unconfirmedFields": "新しい未確認のフィールド", "xpack.enterpriseSearch.appSearch.engine.searchUI.title": "Search UI", "xpack.enterpriseSearch.appSearch.engine.synonyms.title": "同義語", @@ -18751,11 +18752,6 @@ "xpack.security.checkup.insecureClusterMessage": "1 ビットを失わないでください。無料のセキュリティ機能を有効にしてください。", "xpack.security.checkup.insecureClusterTitle": "データが保護されていません", "xpack.security.common.extendedRoleDeprecationNotice": "{roleName} ロールは非推奨です。{reason}", - "xpack.security.components.sessionIdleTimeoutWarning.message": "操作がないため間もなくログアウト{timeout}します。再開するには [OK] をクリックしてください。", - "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "OK", - "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", - "xpack.security.components.sessionLifespanWarning.message": "セッションは最大時間制限{timeout}に達しました。もう一度ログインする必要があります。", - "xpack.security.components.sessionLifespanWarning.title": "警告", "xpack.security.conflictingSessionError": "申し訳ありません。すでに有効なKibanaセッションがあります。新しいセッションを開始する場合は、先に既存のセッションからログアウトしてください。", "xpack.security.copyTokenField.copyButton": "クリップボードにコピー", "xpack.security.copyTokenField.tokenLabel": "トークン", @@ -21847,8 +21843,6 @@ "xpack.securitySolution.trustedapps.list.backButton": "戻る", "xpack.securitySolution.trustedapps.list.columns.actions": "アクション", "xpack.securitySolution.trustedapps.list.pageTitle": "信頼できるアプリケーション", - "xpack.securitySolution.trustedapps.list.search.button": "更新", - "xpack.securitySolution.trustedapps.list.search.placeholder": "検索", "xpack.securitySolution.trustedapps.listEmptyState.message": "現在、エンドポイントには信頼できるアプリケーションがありません。", "xpack.securitySolution.trustedapps.listEmptyState.title": "最初の信頼できるアプリケーションを追加", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.hash": "md5、sha1、sha256", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b07661eaf5e26..8bdffce98d4ab 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8225,6 +8225,7 @@ "xpack.enterpriseSearch.appSearch.engine.sampleEngineBadge": "样本引擎", "xpack.enterpriseSearch.appSearch.engine.schema.conflicts": "架构冲突", "xpack.enterpriseSearch.appSearch.engine.schema.title": "架构", + "xpack.enterpriseSearch.appSearch.engine.schema.unconfirmedFieldLabel": "最近添加", "xpack.enterpriseSearch.appSearch.engine.schema.unconfirmedFields": "新的未确认字段", "xpack.enterpriseSearch.appSearch.engine.searchUI.title": "搜索 UI", "xpack.enterpriseSearch.appSearch.engine.synonyms.title": "同义词", @@ -19008,11 +19009,6 @@ "xpack.security.checkup.insecureClusterMessage": "切勿丢下任何一位。启用我们的免费安全功能。", "xpack.security.checkup.insecureClusterTitle": "您的数据并非安全无忧", "xpack.security.common.extendedRoleDeprecationNotice": "{roleName} 角色已弃用。 {reason}", - "xpack.security.components.sessionIdleTimeoutWarning.message": "由于不活动,您会在 {timeout} 后自动注销。单击“确定”可以恢复。", - "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "确定", - "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", - "xpack.security.components.sessionLifespanWarning.message": "您的会话将达到最大时间限制 {timeout}。您将需要重新登录。", - "xpack.security.components.sessionLifespanWarning.title": "警告", "xpack.security.conflictingSessionError": "抱歉,您已有活动的 Kibana 会话。如果希望开始新的会话,请首先从现有会话注销。", "xpack.security.copyTokenField.copyButton": "复制到剪贴板", "xpack.security.copyTokenField.tokenLabel": "令牌", @@ -22187,8 +22183,6 @@ "xpack.securitySolution.trustedapps.list.backButton": "返回", "xpack.securitySolution.trustedapps.list.columns.actions": "操作", "xpack.securitySolution.trustedapps.list.pageTitle": "受信任的应用程序", - "xpack.securitySolution.trustedapps.list.search.button": "刷新", - "xpack.securitySolution.trustedapps.list.search.placeholder": "搜索", "xpack.securitySolution.trustedapps.list.totalCount": "{totalItemCount, plural, other {# 个受信任的应用程序}}", "xpack.securitySolution.trustedapps.listEmptyState.message": "当前在您的终端上没有受信任应用程序。", "xpack.securitySolution.trustedapps.listEmptyState.title": "添加您的首个受信任应用程序", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index a41708f052dc8..1bbffc850ee18 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -8,6 +8,8 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from '@testing-library/react'; import { AlertDetails } from './alert_details'; import { Alert, ActionType, AlertTypeModel, AlertType } from '../../../../types'; import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiButtonEmpty, EuiText } from '@elastic/eui'; @@ -463,6 +465,74 @@ describe('disable button', () => { handler!({} as React.FormEvent); expect(enableAlert).toHaveBeenCalledTimes(1); }); + + it('should reset error banner dismissal after re-enabling the alert', async () => { + const alert = mockAlert({ + enabled: true, + executionStatus: { + status: 'error', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: { + reason: AlertExecutionStatusErrorReasons.Execute, + message: 'Fail', + }, + }, + }); + + const alertType: AlertType = { + id: '.noop', + name: 'No Op', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup, + actionVariables: { context: [], state: [], params: [] }, + defaultActionGroupId: 'default', + producer: ALERTS_FEATURE_ID, + authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, + }; + + const disableAlert = jest.fn(); + const enableAlert = jest.fn(); + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + // Dismiss the error banner + await act(async () => { + wrapper.find('[data-test-subj="dismiss-execution-error"]').first().simulate('click'); + await nextTick(); + }); + + // Disable the alert + await act(async () => { + wrapper.find('[data-test-subj="disableSwitch"] .euiSwitch__button').first().simulate('click'); + await nextTick(); + }); + expect(disableAlert).toHaveBeenCalled(); + + // Enable the alert + await act(async () => { + wrapper.find('[data-test-subj="disableSwitch"] .euiSwitch__button').first().simulate('click'); + await nextTick(); + }); + expect(enableAlert).toHaveBeenCalled(); + + // Ensure error banner is back + expect(wrapper.find('[data-test-subj="dismiss-execution-error"]').length).toBeGreaterThan(0); + }); }); describe('mute button', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 0796f09b13460..d85e792f4a9bb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -236,6 +236,8 @@ export const AlertDetails: React.FunctionComponent = ({ if (isEnabled) { setIsEnabled(false); await disableAlert(alert); + // Reset dismiss if previously clicked + setDissmissAlertErrors(false); } else { setIsEnabled(true); await enableAlert(alert); @@ -277,7 +279,7 @@ export const AlertDetails: React.FunctionComponent = ({
    - {!dissmissAlertErrors && alert.executionStatus.status === 'error' ? ( + {alert.enabled && !dissmissAlertErrors && alert.executionStatus.status === 'error' ? ( = ({ - setDissmissAlertErrors(true)}> + setDissmissAlertErrors(true)} + > { }; expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ instance: 'id', - status: { label: 'OK', healthColor: 'subdued' }, + status: { label: 'Recovered', healthColor: 'subdued' }, start: undefined, duration: 0, sortPriority: 1, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index d2919194125f8..5ba4c466f6fad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -226,7 +226,7 @@ const ACTIVE_LABEL = i18n.translate( const INACTIVE_LABEL = i18n.translate( 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive', - { defaultMessage: 'OK' } + { defaultMessage: 'Recovered' } ); function getActionGroupName(alertType: AlertType, actionGroupId?: string): string | undefined { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx index 1dbd37dc00803..0bc2fc8823cec 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx @@ -327,7 +327,6 @@ export const CustomFields = memo( defaultMessage="Timeout must be 0 or greater and less than schedule interval" /> } - labelAppend={} helpText={ ', () => { }); }); - it('handles updating each field', async () => { + it('handles updating fields', async () => { const { getByLabelText } = render(); const url = getByLabelText('URL') as HTMLInputElement; const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement; @@ -336,6 +336,54 @@ describe('', () => { expect(apmServiceName.value).toEqual('APM Service'); expect(maxRedirects.value).toEqual('2'); expect(timeout.value).toEqual('3'); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ + isValid: true, + updatedPolicy: { + ...defaultNewPolicy, + inputs: [ + { + ...defaultNewPolicy.inputs[0], + streams: [ + { + ...defaultNewPolicy.inputs[0].streams[0], + vars: { + ...defaultNewPolicy.inputs[0].streams[0].vars, + urls: { + value: 'http://elastic.co', + type: 'text', + }, + proxy_url: { + value: 'http://proxy.co', + type: 'text', + }, + schedule: { + value: '"@every 1m"', + type: 'text', + }, + 'service.name': { + value: 'APM Service', + type: 'text', + }, + max_redirects: { + value: '2', + type: 'integer', + }, + timeout: { + value: '3s', + type: 'text', + }, + }, + }, + ], + }, + defaultNewPolicy.inputs[1], + defaultNewPolicy.inputs[2], + ], + }, + }); + }); }); it('handles calling onChange', async () => { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx index 386d99add87b6..e29a5c6a363ed 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx @@ -15,7 +15,7 @@ import { TCPAdvancedFieldsContext, TLSFieldsContext, } from './contexts'; -import { Config, ConfigKeys } from './types'; +import { Config, ConfigKeys, DataStream } from './types'; import { CustomFields } from './custom_fields'; import { useUpdatePolicy } from './use_update_policy'; import { validate } from './validation'; @@ -48,6 +48,11 @@ export const SyntheticsPolicyEditExtension = memo', () => { expect(queryByLabelText('Monitor type')).not.toBeInTheDocument(); }); - it('handles updating each field', async () => { + it('handles updating fields', async () => { const { getByLabelText } = render(); const url = getByLabelText('URL') as HTMLInputElement; const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement; @@ -364,6 +364,54 @@ describe('', () => { expect(apmServiceName.value).toEqual('APM Service'); expect(maxRedirects.value).toEqual('2'); expect(timeout.value).toEqual('3'); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ + isValid: true, + updatedPolicy: { + ...defaultNewPolicy, + inputs: [ + { + ...defaultNewPolicy.inputs[0], + streams: [ + { + ...defaultNewPolicy.inputs[0].streams[0], + vars: { + ...defaultNewPolicy.inputs[0].streams[0].vars, + urls: { + value: 'http://elastic.co', + type: 'text', + }, + proxy_url: { + value: 'http://proxy.co', + type: 'text', + }, + schedule: { + value: '"@every 1m"', + type: 'text', + }, + 'service.name': { + value: 'APM Service', + type: 'text', + }, + max_redirects: { + value: '2', + type: 'integer', + }, + timeout: { + value: '3s', + type: 'text', + }, + }, + }, + ], + }, + defaultNewPolicy.inputs[1], + defaultNewPolicy.inputs[2], + ], + }, + }); + }); }); it('handles calling onChange', async () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/doc_link_body.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/doc_link_body.test.tsx.snap deleted file mode 100644 index 22dab492a94ae..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/doc_link_body.test.tsx.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PingListExpandedRow renders expected elements for valid props 1`] = ` - - - docs -   - - , - } - } - /> - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap index 8ace0445d0eb7..377a2c9389bbd 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap @@ -174,10 +174,7 @@ exports[`PingListExpandedRow renders link to docs if body is not recorded but it rel="noopener" target="_blank" > - docs  - + docs { it('renders expected elements for valid props', () => { - expect(shallowWithIntl()).toMatchSnapshot(); + render(); + + expect(screen.getByText(/Body not recorded. Read our/)); + expect( + screen.getByRole('link', { name: 'docs External link (opens in a new tab or window)' }) + ).toBeInTheDocument(); + expect(screen.getByText(/for more information on recording response bodies./)); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx index 32d0b1e4122d8..67561022bce93 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiIcon, EuiLink, EuiText } from '@elastic/eui'; +import { EuiLink, EuiText } from '@elastic/eui'; const bodyDocsLink = 'https://www.elastic.co/guide/en/beats/heartbeat/current/configuration-heartbeat-options.html#monitor-http-response'; @@ -20,8 +20,6 @@ export const DocLinkForBody = () => { defaultMessage: 'docs', description: 'Docs link to set response body', })} -   - ); diff --git a/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx b/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx index 0543e5868bb9e..9436f420f7740 100644 --- a/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx +++ b/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx @@ -34,11 +34,11 @@ export const isValidKuery = (query: string) => { export const QueryBar = () => { const { search: urlValue } = useGetUrlParams(); - const { query, setQuery } = useQueryBar(); + const { query, setQuery, submitImmediately } = useQueryBar(); const { index_pattern: indexPattern } = useIndexPattern(); - const [inputVal, setInputVal] = useState(query.query); + const [inputVal, setInputVal] = useState(query.query as string); const isInValid = () => { if (query.language === SyntaxType.text) { @@ -66,6 +66,7 @@ export const QueryBar = () => { }} onSubmit={(queryN) => { if (queryN) setQuery({ query: queryN.query as string, language: queryN.language }); + submitImmediately(); }} query={{ ...query, query: inputVal }} aria-label={i18n.translate('xpack.uptime.filterBar.ariaLabel', { diff --git a/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts b/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts index 0d8a2ee17994a..164231bfdd89b 100644 --- a/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts +++ b/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useDebounce } from 'react-use'; import { useDispatch } from 'react-redux'; +import { Query } from 'src/plugins/data/common'; import { useGetUrlParams, useUpdateKueryString, useUrlParams } from '../../../hooks'; import { setEsKueryString } from '../../../state/actions'; import { useIndexPattern } from './use_index_pattern'; @@ -20,7 +21,26 @@ export enum SyntaxType { } const SYNTAX_STORAGE = 'uptime:queryBarSyntax'; -export const useQueryBar = () => { +const DEFAULT_QUERY_UPDATE_DEBOUNCE_INTERVAL = 800; + +interface UseQueryBarUtils { + // The Query object used by the search bar + query: Query; + // Update the Query object + setQuery: React.Dispatch>; + /** + * By default the search bar uses a debounce to delay submitting input; + * this function will cancel the debounce and submit immediately. + */ + submitImmediately: () => void; +} + +/** + * Provides state management and automatic dispatching of a Query object. + * + * @returns {UseQueryBarUtils} + */ +export const useQueryBar = (): UseQueryBarUtils => { const dispatch = useDispatch(); const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams(); @@ -30,7 +50,7 @@ export const useQueryBar = () => { services: { storage }, } = useKibana(); - const [query, setQuery] = useState( + const [query, setQuery] = useState( queryParam ? { query: queryParam, @@ -59,23 +79,37 @@ export const useQueryBar = () => { [dispatch] ); - useEffect(() => { - setEsKueryFilters(esFilters ?? ''); - }, [esFilters, setEsKueryFilters]); + const setEs = useCallback(() => setEsKueryFilters(esFilters ?? ''), [ + esFilters, + setEsKueryFilters, + ]); + const [, cancelEsKueryUpdate] = useDebounce(setEs, DEFAULT_QUERY_UPDATE_DEBOUNCE_INTERVAL, [ + esFilters, + setEsKueryFilters, + ]); - useDebounce( - () => { - if (query.language === SyntaxType.text && queryParam !== query.query) { - updateUrlParams({ query: query.query as string }); - } - if (query.language === SyntaxType.kuery) { - updateUrlParams({ query: '' }); - } - }, - 350, + const handleQueryUpdate = useCallback(() => { + if (query.language === SyntaxType.text && queryParam !== query.query) { + updateUrlParams({ query: query.query as string }); + } + if (query.language === SyntaxType.kuery) { + updateUrlParams({ query: '' }); + } + }, [query.language, query.query, queryParam, updateUrlParams]); + + const [, cancelQueryUpdate] = useDebounce( + handleQueryUpdate, + DEFAULT_QUERY_UPDATE_DEBOUNCE_INTERVAL, [query] ); + const submitImmediately = useCallback(() => { + cancelQueryUpdate(); + cancelEsKueryUpdate(); + handleQueryUpdate(); + setEs(); + }, [cancelEsKueryUpdate, cancelQueryUpdate, handleQueryUpdate, setEs]); + useDebounce( () => { if (query.language === SyntaxType.kuery && !error && esFilters) { @@ -92,5 +126,5 @@ export const useQueryBar = () => { [esFilters, error] ); - return { query, setQuery }; + return { query, setQuery, submitImmediately }; }; diff --git a/x-pack/plugins/uptime/public/state/reducers/selected_filters.test.ts b/x-pack/plugins/uptime/public/state/reducers/selected_filters.test.ts new file mode 100644 index 0000000000000..f69af7bd1b5a3 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/selected_filters.test.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 { selectedFiltersReducer } from './selected_filters'; +import { + getSelectedFilters, + setSelectedFilters, + SelectedFilters, +} from '../actions/selected_filters'; +import { createAction } from 'redux-actions'; + +describe('selectedFiltersReducer', () => { + let state: SelectedFilters; + + beforeEach(() => { + state = { + locations: [], + ports: [], + schemes: ['http'], + tags: [], + }; + }); + + it('returns state by default', () => { + expect(selectedFiltersReducer(state, createAction('fake action')())).toEqual(state); + }); + + describe('setSelectedFilters', () => { + it('returns null for null payload', () => { + expect(selectedFiltersReducer(state, setSelectedFilters(null))).toBeNull(); + }); + + it('sets state to the action payload if state is null', () => { + expect( + selectedFiltersReducer( + null, + setSelectedFilters({ + locations: [], + ports: [5601], + schemes: [], + tags: [], + }) + ) + ).toEqual({ locations: [], ports: [5601], schemes: [], tags: [] }); + }); + + it('merges state and action payload', () => { + expect( + selectedFiltersReducer( + { + locations: [], + ports: [], + schemes: [], + tags: [], + }, + setSelectedFilters({ + locations: [], + ports: [5601], + schemes: [], + tags: ['prod'], + }) + ) + ).toEqual({ locations: [], ports: [5601], schemes: [], tags: ['prod'] }); + }); + }); + + describe('getSelectedFilters', () => { + it('returns the state', () => { + expect(selectedFiltersReducer(state, getSelectedFilters())).toEqual(state); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/state/reducers/selected_filters.ts b/x-pack/plugins/uptime/public/state/reducers/selected_filters.ts index fc24241331169..b5e3544b66156 100644 --- a/x-pack/plugins/uptime/public/state/reducers/selected_filters.ts +++ b/x-pack/plugins/uptime/public/state/reducers/selected_filters.ts @@ -19,14 +19,12 @@ export function selectedFiltersReducer( action: Action ): SelectedFilters | null { switch (action.type) { - case String(getSelectedFilters): - return state; case String(setSelectedFilters): - if (state === null) return { ...action.payload }; + if (action.payload === null) return null; return { - ...(state || {}), ...action.payload, }; + case String(getSelectedFilters): default: return state; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts index 6a533d558c721..b5ff1de44000c 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts @@ -14,10 +14,21 @@ export interface GetJourneyScreenshotParams { stepIndex: number; } +export interface GetJourneyScreenshotResults { + blob: string | null; + mimeType: string | null; + stepName: string; + totalSteps: number; +} + export const getJourneyScreenshot: UMElasticsearchQueryFn< GetJourneyScreenshotParams, any -> = async ({ uptimeEsClient, checkGroup, stepIndex }) => { +> = async ({ + uptimeEsClient, + checkGroup, + stepIndex, +}): Promise => { const params = { track_total_hits: true, size: 0, @@ -48,7 +59,7 @@ export const getJourneyScreenshot: UMElasticsearchQueryFn< image: { top_hits: { size: 1, - _source: ['synthetics.blob', 'synthetics.step.name'], + _source: ['synthetics.blob', 'synthetics.blob_mime', 'synthetics.step.name'], }, }, }, @@ -65,6 +76,7 @@ export const getJourneyScreenshot: UMElasticsearchQueryFn< return { blob: stepHit?.synthetics?.blob ?? null, + mimeType: stepHit?.synthetics?.blob_mime ?? null, stepName: stepHit?.synthetics?.step?.name ?? '', totalSteps: result?.hits?.total.value, }; diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts index ab8a01cfb9c3f..4d8c8f86ddf2d 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts @@ -37,7 +37,7 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ return response.ok({ body: Buffer.from(result.blob, 'base64'), headers: { - 'content-type': 'image/png', + 'content-type': result.mimeType || 'image/png', // falls back to 'image/png' for earlier versions of synthetics 'cache-control': 'max-age=600', 'caption-name': result.stepName, 'max-steps': result.totalSteps, diff --git a/x-pack/test/api_integration/apis/monitoring/apm/fixtures/cluster.json b/x-pack/test/api_integration/apis/monitoring/apm/fixtures/cluster.json index 1b89349785f26..42ad837963ef4 100644 --- a/x-pack/test/api_integration/apis/monitoring/apm/fixtures/cluster.json +++ b/x-pack/test/api_integration/apis/monitoring/apm/fixtures/cluster.json @@ -2,7 +2,6 @@ "stats": { "totalEvents": 18, "memRss": 3821568, - "memTotal": 2404475016, "apms": { "total": 2 }, diff --git a/x-pack/test/api_integration/apis/monitoring/apm/instances.js b/x-pack/test/api_integration/apis/monitoring/apm/instances.js index a18569711cd63..9d0be53cff97f 100644 --- a/x-pack/test/api_integration/apis/monitoring/apm/instances.js +++ b/x-pack/test/api_integration/apis/monitoring/apm/instances.js @@ -67,6 +67,7 @@ export default function ({ getService }) { time_of_last_event: '2018-08-31T13:59:21.163Z', }, ], + cgroup: false, }; expect(body).to.eql(expected); diff --git a/x-pack/test/api_integration/apis/monitoring/apm/instances_mb.js b/x-pack/test/api_integration/apis/monitoring/apm/instances_mb.js index 4ebbd50691d12..879549659936b 100644 --- a/x-pack/test/api_integration/apis/monitoring/apm/instances_mb.js +++ b/x-pack/test/api_integration/apis/monitoring/apm/instances_mb.js @@ -67,6 +67,7 @@ export default function ({ getService }) { time_of_last_event: '2018-08-31T13:59:21.163Z', }, ], + cgroup: false, }; expect(body).to.eql(expected); diff --git a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json index 6abd3a8ecff9d..080517f46f9c9 100644 --- a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json +++ b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json @@ -76,7 +76,6 @@ "apm": { "totalEvents": 0, "memRss": 0, - "memTotal": 0, "apms": { "total": 0 }, @@ -173,7 +172,6 @@ "apm": { "totalEvents": 0, "memRss": 0, - "memTotal": 0, "apms": { "total": 0 }, @@ -270,7 +268,6 @@ "apm": { "totalEvents": 0, "memRss": 0, - "memTotal": 0, "apms": { "total": 0 }, diff --git a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json index 4f1024f2c94b0..a779546624ee1 100644 --- a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json +++ b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json @@ -90,7 +90,6 @@ "apm": { "totalEvents": 0, "memRss": 0, - "memTotal": 0, "apms": { "total": 0 }, diff --git a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json index 3e590656753f1..13535347f437c 100644 --- a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json +++ b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json @@ -49,7 +49,6 @@ "apm": { "totalEvents": 0, "memRss": 0, - "memTotal": 0, "apms": { "total": 0 }, diff --git a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json index 8f20dce44ee8a..87c269068ed33 100644 --- a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json +++ b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json @@ -76,7 +76,6 @@ "apm": { "totalEvents": 0, "memRss": 0, - "memTotal": 0, "apms": { "total": 0 }, @@ -153,7 +152,6 @@ "apm": { "totalEvents": 0, "memRss": 0, - "memTotal": 0, "apms": { "total": 0 }, diff --git a/x-pack/test/api_integration/apis/uptime/rest/certs.ts b/x-pack/test/api_integration/apis/uptime/rest/certs.ts index 76be0af03e908..d7c0a4eed4a72 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/certs.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/certs.ts @@ -15,7 +15,7 @@ import { makeChecksWithStatus } from './helper/make_checks'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const legacyEsService = getService('legacyEs'); + const esService = getService('es'); const esArchiver = getService('esArchiver'); describe('certs api', () => { @@ -33,7 +33,7 @@ export default function ({ getService }: FtrProviderContext) { const monitorId = 'monitor1'; before(async () => { makeChecksWithStatus( - legacyEsService, + esService, monitorId, 3, 1, diff --git a/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts index 829ae07eccf20..51338d2bb6e03 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts @@ -7,11 +7,12 @@ import uuid from 'uuid'; import { merge, flattenDeep } from 'lodash'; +import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { makePing } from './make_ping'; import { TlsProps } from './make_tls'; interface CheckProps { - es: any; + es: KibanaClient; monitorId?: string; numIps?: number; fields?: { [key: string]: any }; @@ -76,7 +77,7 @@ export const makeCheck = async ({ }; export const makeChecks = async ( - es: any, + es: KibanaClient, monitorId: string, numChecks: number = 1, numIps: number = 1, @@ -121,7 +122,7 @@ export const makeChecks = async ( }; export const makeChecksWithStatus = async ( - es: any, + es: KibanaClient, monitorId: string, numChecks: number, numIps: number, diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts index abd3c5d51928b..5268a2146d123 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts @@ -36,7 +36,7 @@ export default function ({ getService }: FtrProviderContext) { describe('checks with no summaries', async () => { const testMonitorId = 'scope-test-id'; before(async () => { - const es = getService('legacyEs'); + const es = getService('es'); dateRangeStart = new Date().toISOString(); await makeChecksWithStatus(es, testMonitorId, 1, numIps, 1, {}, 'up', (d) => { delete d.summary; @@ -64,7 +64,7 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { - const es = getService('legacyEs'); + const es = getService('es'); dateRangeStart = new Date().toISOString(); checks = await makeChecksWithStatus(es, testMonitorId, 1, numIps, 1, {}, 'up', (d) => { // turn an all up status into having at least one down @@ -141,7 +141,7 @@ export default function ({ getService }: FtrProviderContext) { before('generate three monitors with up, down, mix state', async () => { await getService('esArchiver').load('uptime/blank'); - const es = getService('legacyEs'); + const es = getService('es'); const observer = { geo: { diff --git a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts index 7a5487c611612..358e667bcb05b 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts @@ -52,7 +52,7 @@ export default function ({ getService }: FtrProviderContext) { const makeMonitorChecks = async (monitorId: string, status: 'up' | 'down') => { return makeChecksWithStatus( - getService('legacyEs'), + getService('es'), monitorId, checksPerMonitor, numIps, diff --git a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts index f0f7a520d098a..fd8894d7e9164 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts @@ -12,7 +12,7 @@ import { makeChecksWithStatus } from './helper/make_checks'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); describe('telemetry collectors heartbeat', () => { before('generating data', async () => { diff --git a/x-pack/test/apm_api_integration/configs/index.ts b/x-pack/test/apm_api_integration/configs/index.ts index 97d18c2419840..3393580153215 100644 --- a/x-pack/test/apm_api_integration/configs/index.ts +++ b/x-pack/test/apm_api_integration/configs/index.ts @@ -18,7 +18,7 @@ const apmFtrConfigs = { rules: { license: 'trial' as const, kibanaConfig: { - 'xpack.ruleRegistry.unsafe.write.enabled': 'true', + 'xpack.ruleRegistry.index': '.kibana-alerts', }, }, }; diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts index 8d0b87782ff7c..e0a3e4d3a3f8b 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { get, merge, omit } from 'lodash'; +import { merge, omit } from 'lodash'; import { format } from 'url'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; @@ -30,7 +30,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertestAsApmWriteUser'); const es = getService('es'); - const MAX_POLLS = 5; + const MAX_POLLS = 10; const BULK_INDEX_DELAY = 1000; const INDEXING_DELAY = 5000; @@ -108,11 +108,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { } registry.when('Rule registry with write enabled', { config: 'rules', archives: [] }, () => { - it('bootstraps the apm alert indices', async () => { + it('does not bootstrap indices on plugin startup', async () => { const { body } = await es.indices.get({ index: ALERTS_INDEX_TARGET, expand_wildcards: 'open', - allow_no_indices: false, + allow_no_indices: true, }); const indices = Object.entries(body).map(([indexName, index]) => { @@ -122,23 +122,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; }); - const indexNames = indices.map((index) => index.indexName); - - const apmIndex = indices[0]; - - // make sure it only creates one index - expect(indices.length).to.be(1); - - const apmIndexName = apmIndex.indexName; - - expect(apmIndexName.split('-').includes('observability')).to.be(true); - expect(apmIndexName.split('-').includes('apm')).to.be(true); - - expect(indexNames[0].startsWith('.kibana-alerts-observability-apm')).to.be(true); - - expect(get(apmIndex, 'index.mappings.properties.service.properties.environment.type')).to.be( - 'keyword' - ); + expect(indices.length).to.be(0); }); describe('when creating a rule', () => { @@ -335,12 +319,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { sort: { '@timestamp': 'desc', }, + _source: false, + fields: [{ field: '*', include_unmapped: true }], }, }); expect(afterViolatingDataResponse.body.hits.hits.length).to.be(1); - const alertEvent = afterViolatingDataResponse.body.hits.hits[0]._source as Record< + const alertEvent = afterViolatingDataResponse.body.hits.hits[0].fields as Record< string, any >; @@ -354,23 +340,56 @@ export default function ApiTest({ getService }: FtrProviderContext) { const toCompare = omit(alertEvent, exclude); - expect(toCompare).to.eql({ - 'event.action': 'open', - 'event.kind': 'state', - 'kibana.rac.alert.duration.us': 0, - 'kibana.rac.alert.id': 'apm.transaction_error_rate_opbeans-go_request', - 'kibana.rac.alert.status': 'open', - 'kibana.rac.producer': 'apm', - 'kibana.observability.evaluation.threshold': 30, - 'kibana.observability.evaluation.value': 50, - 'processor.event': 'transaction', - 'rule.category': 'Transaction error rate threshold', - 'rule.id': 'apm.transaction_error_rate', - 'rule.name': 'Transaction error rate threshold | opbeans-go', - 'service.name': 'opbeans-go', - tags: ['apm', 'service.name:opbeans-go'], - 'transaction.type': 'request', - }); + expectSnapshot(toCompare).toMatchInline(` + Object { + "event.action": Array [ + "open", + ], + "event.kind": Array [ + "state", + ], + "kibana.rac.alert.duration.us": Array [ + 0, + ], + "kibana.rac.alert.evaluation.threshold": Array [ + 30, + ], + "kibana.rac.alert.evaluation.value": Array [ + 50, + ], + "kibana.rac.alert.id": Array [ + "apm.transaction_error_rate_opbeans-go_request", + ], + "kibana.rac.alert.producer": Array [ + "apm", + ], + "kibana.rac.alert.status": Array [ + "open", + ], + "processor.event": Array [ + "transaction", + ], + "rule.category": Array [ + "Transaction error rate threshold", + ], + "rule.id": Array [ + "apm.transaction_error_rate", + ], + "rule.name": Array [ + "Transaction error rate threshold | opbeans-go", + ], + "service.name": Array [ + "opbeans-go", + ], + "tags": Array [ + "apm", + "service.name:opbeans-go", + ], + "transaction.type": Array [ + "request", + ], + } + `); const now = new Date().getTime(); @@ -390,7 +409,56 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(topAlerts.length).to.be.greaterThan(0); - expect(omit(topAlerts[0], exclude)).to.eql(toCompare); + expectSnapshot(omit(topAlerts[0], exclude)).toMatchInline(` + Object { + "event.action": Array [ + "open", + ], + "event.kind": Array [ + "state", + ], + "kibana.rac.alert.duration.us": Array [ + 0, + ], + "kibana.rac.alert.evaluation.threshold": Array [ + 30, + ], + "kibana.rac.alert.evaluation.value": Array [ + 50, + ], + "kibana.rac.alert.id": Array [ + "apm.transaction_error_rate_opbeans-go_request", + ], + "kibana.rac.alert.producer": Array [ + "apm", + ], + "kibana.rac.alert.status": Array [ + "open", + ], + "processor.event": Array [ + "transaction", + ], + "rule.category": Array [ + "Transaction error rate threshold", + ], + "rule.id": Array [ + "apm.transaction_error_rate", + ], + "rule.name": Array [ + "Transaction error rate threshold | opbeans-go", + ], + "service.name": Array [ + "opbeans-go", + ], + "tags": Array [ + "apm", + "service.name:opbeans-go", + ], + "transaction.type": Array [ + "request", + ], + } + `); await es.bulk({ index: APM_TRANSACTION_INDEX_NAME, @@ -423,43 +491,76 @@ export default function ApiTest({ getService }: FtrProviderContext) { sort: { '@timestamp': 'desc', }, + _source: false, + fields: [{ field: '*', include_unmapped: true }], }, }); expect(afterRecoveryResponse.body.hits.hits.length).to.be(1); - const recoveredAlertEvent = afterRecoveryResponse.body.hits.hits[0]._source as Record< + const recoveredAlertEvent = afterRecoveryResponse.body.hits.hits[0].fields as Record< string, any >; - expect(recoveredAlertEvent['kibana.rac.alert.status']).to.eql('closed'); - expect(recoveredAlertEvent['kibana.rac.alert.duration.us']).to.be.greaterThan(0); - expect(new Date(recoveredAlertEvent['kibana.rac.alert.end']).getTime()).to.be.greaterThan( - 0 - ); - + expect(recoveredAlertEvent['kibana.rac.alert.status']?.[0]).to.eql('closed'); + expect(recoveredAlertEvent['kibana.rac.alert.duration.us']?.[0]).to.be.greaterThan(0); expect( + new Date(recoveredAlertEvent['kibana.rac.alert.end']?.[0]).getTime() + ).to.be.greaterThan(0); + + expectSnapshot( omit( recoveredAlertEvent, exclude.concat(['kibana.rac.alert.duration.us', 'kibana.rac.alert.end']) ) - ).to.eql({ - 'event.action': 'close', - 'event.kind': 'state', - 'kibana.rac.alert.id': 'apm.transaction_error_rate_opbeans-go_request', - 'kibana.rac.alert.status': 'closed', - 'kibana.rac.producer': 'apm', - 'kibana.observability.evaluation.threshold': 30, - 'kibana.observability.evaluation.value': 50, - 'processor.event': 'transaction', - 'rule.category': 'Transaction error rate threshold', - 'rule.id': 'apm.transaction_error_rate', - 'rule.name': 'Transaction error rate threshold | opbeans-go', - 'service.name': 'opbeans-go', - tags: ['apm', 'service.name:opbeans-go'], - 'transaction.type': 'request', - }); + ).toMatchInline(` + Object { + "event.action": Array [ + "close", + ], + "event.kind": Array [ + "state", + ], + "kibana.rac.alert.evaluation.threshold": Array [ + 30, + ], + "kibana.rac.alert.evaluation.value": Array [ + 50, + ], + "kibana.rac.alert.id": Array [ + "apm.transaction_error_rate_opbeans-go_request", + ], + "kibana.rac.alert.producer": Array [ + "apm", + ], + "kibana.rac.alert.status": Array [ + "closed", + ], + "processor.event": Array [ + "transaction", + ], + "rule.category": Array [ + "Transaction error rate threshold", + ], + "rule.id": Array [ + "apm.transaction_error_rate", + ], + "rule.name": Array [ + "Transaction error rate threshold | opbeans-go", + ], + "service.name": Array [ + "opbeans-go", + ], + "tags": Array [ + "apm", + "service.name:opbeans-go", + ], + "transaction.type": Array [ + "request", + ], + } + `); const { body: topAlertsAfterRecovery, @@ -480,7 +581,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(topAlertsAfterRecovery.length).to.be(1); - expect(topAlertsAfterRecovery[0]['kibana.rac.alert.status']).to.be('closed'); + expect(topAlertsAfterRecovery[0]['kibana.rac.alert.status']?.[0]).to.be('closed'); }); }); }); diff --git a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts index 5f38a6e050f40..25fb71ae42807 100644 --- a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts @@ -103,7 +103,7 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); - it('should allow to create an enrollment api key with an agent policy', async () => { + it('should allow to create an enrollment api key with only an agent policy', async () => { const { body: apiResponse } = await supertest .post(`/api/fleet/enrollment-api-keys`) .set('kbn-xsrf', 'xxx') @@ -115,6 +115,55 @@ export default function (providerContext: FtrProviderContext) { expect(apiResponse.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id'); }); + it('should allow to create an enrollment api key with agent policy and unique name', async () => { + const { body: noSpacesRes } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + name: 'something', + }); + expect(noSpacesRes.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id'); + + const { body: hasSpacesRes } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + name: 'something else', + }); + expect(hasSpacesRes.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id'); + + const { body: noSpacesDupe } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + name: 'something', + }) + .expect(400); + + expect(noSpacesDupe).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'An enrollment key named something already exists for agent policy policy1', + }); + + const { body: hasSpacesDupe } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + name: 'something else', + }) + .expect(400); + expect(hasSpacesDupe).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'An enrollment key named something else already exists for agent policy policy1', + }); + }); + it('should create an ES ApiKey with metadata', async () => { const { body: apiResponse } = await supertest .post(`/api/fleet/enrollment-api-keys`) diff --git a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts index 44cd2cda7e1af..50194552aec0a 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts @@ -41,7 +41,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return (await testSubjects.getVisibleText('sectionHeading')) === 'Index Lifecycle Policies'; }); + await pageObjects.indexLifecycleManagement.increasePolicyListPageSize(); + const allPolicies = await pageObjects.indexLifecycleManagement.getPolicyList(); + const filteredPolicies = allPolicies.filter(function (policy) { return policy.name === policyName; }); diff --git a/x-pack/test/functional/apps/lens/geo_field.ts b/x-pack/test/functional/apps/lens/geo_field.ts new file mode 100644 index 0000000000000..2ba833177a135 --- /dev/null +++ b/x-pack/test/functional/apps/lens/geo_field.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'lens', 'header', 'maps', 'timePicker']); + + describe('lens visualize geo field tests', () => { + it('should visualize geo fields in maps', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 22, 2015 @ 00:00:00.000', + 'Sep 22, 2015 @ 04:00:00.000' + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToGeoFieldWorkspace('geo.coordinates'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + const doesLayerExist = await PageObjects.maps.doesLayerExist('logstash-*'); + expect(doesLayerExist).to.equal(true); + const hits = await PageObjects.maps.getHits(); + expect(hits).to.equal('66'); + await PageObjects.maps.refreshAndClearUnsavedChangesWarning(); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index bfb0aad7177f4..ab7cee13ffebd 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -37,6 +37,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./colors')); loadTestFile(require.resolve('./chart_data')); loadTestFile(require.resolve('./drag_and_drop')); + loadTestFile(require.resolve('./geo_field')); loadTestFile(require.resolve('./lens_reporting')); loadTestFile(require.resolve('./lens_tagging')); diff --git a/x-pack/test/functional/apps/uptime/locations.ts b/x-pack/test/functional/apps/uptime/locations.ts index e3f1d04640754..1e71607845ea4 100644 --- a/x-pack/test/functional/apps/uptime/locations.ts +++ b/x-pack/test/functional/apps/uptime/locations.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const { uptime: uptimePage } = getPageObjects(['uptime']); const uptime = getService('uptime'); - const es = getService('legacyEs'); + const es = getService('es'); const monitor = () => uptime.monitor; const MONITOR_ID = 'location-testing-id'; diff --git a/x-pack/test/functional/apps/uptime/ping_redirects.ts b/x-pack/test/functional/apps/uptime/ping_redirects.ts index 9c39ed7017721..39d9292dbbbb8 100644 --- a/x-pack/test/functional/apps/uptime/ping_redirects.ts +++ b/x-pack/test/functional/apps/uptime/ping_redirects.ts @@ -35,7 +35,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { beforeEach(async () => { await makeChecksWithStatus( - getService('legacyEs'), + getService('es'), MONITOR_ID, 5, 2, diff --git a/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts b/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts index 525e0d91e2f4d..2dd70f8a95717 100644 --- a/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts +++ b/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts @@ -69,6 +69,11 @@ export function IndexLifecycleManagementPageProvider({ getService }: FtrProvider await this.saveNewPolicy(); }, + async increasePolicyListPageSize() { + await testSubjects.click('tablePaginationPopoverButton'); + await testSubjects.click(`tablePagination-100-rows`); + }, + async getPolicyList() { const policies = await testSubjects.findAll('policyTableRow'); return mapAsync(policies, async (policy) => { diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 100ed8e079d37..f73440e331466 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -174,6 +174,19 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await PageObjects.header.waitUntilLoadingHasFinished(); }, + /** + * Drags field to geo field workspace + * + * @param field - the desired geo_point or geo_shape field + * */ + async dragFieldToGeoFieldWorkspace(field: string) { + await browser.html5DragAndDrop( + testSubjects.getCssSelector(`lnsFieldListPanelField-${field}`), + testSubjects.getCssSelector('lnsGeoFieldWorkspace') + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + }, + /** * Drags field to workspace * diff --git a/x-pack/test/functional/services/ml/dashboard_embeddables.ts b/x-pack/test/functional/services/ml/dashboard_embeddables.ts index db5c3f35a2e14..03b69854744d4 100644 --- a/x-pack/test/functional/services/ml/dashboard_embeddables.ts +++ b/x-pack/test/functional/services/ml/dashboard_embeddables.ts @@ -35,46 +35,40 @@ export function MachineLearningDashboardEmbeddablesProvider( async assertSelectMaxSeriesToPlotValue(expectedValue: number) { const subj = 'mlAnomalyChartsInitializerMaxSeries'; - await testSubjects.existOrFail(subj); - const input = await testSubjects.find(subj); - const actualValue = await input.getAttribute('value'); - - expect(actualValue).to.eql( - expectedValue, - `Expected max series to plot value to be ${expectedValue}, got ${actualValue}` - ); - }, - - async assertPanelTitle(expectedValue: string) { - const actualValue = await testSubjects.getAttribute('panelTitleInput', 'value'); - expect(actualValue).to.eql( - expectedValue, - `Panel title should be '${expectedValue}' (got '${actualValue}')` - ); - }, - - async setPanelTitle(panelTitle: string) { - await mlCommonUI.setValueWithChecks('panelTitleInput', panelTitle, { - clearWithKeyboard: true, + await retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail(subj); + const input = await testSubjects.find(subj); + const actualValue = await input.getAttribute('value'); + + expect(actualValue).to.eql( + expectedValue, + `Expected max series to plot value to be ${expectedValue}, got ${actualValue}` + ); }); - await this.assertPanelTitle(panelTitle); }, async assertInitializerConfirmButtonEnabled() { const subj = 'mlAnomalyChartsInitializerConfirmButton'; - await testSubjects.existOrFail(subj); - await testSubjects.isEnabled(subj); + + await retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail(subj); + await testSubjects.isEnabled(subj); + }); }, async clickInitializerConfirmButtonEnabled() { const subj = 'mlAnomalyChartsInitializerConfirmButton'; - await this.assertInitializerConfirmButtonEnabled(); - await testSubjects.clickWhenNotDisabled(subj); - await this.assertAnomalyChartsEmbeddableInitializerNotExists(); + await retry.tryForTime(60 * 1000, async () => { + await this.assertInitializerConfirmButtonEnabled(); + await testSubjects.clickWhenNotDisabled(subj); + await this.assertAnomalyChartsEmbeddableInitializerNotExists(); + }); }, async assertDashboardIsEmpty() { - await testSubjects.existOrFail('emptyDashboardWidget'); + await retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail('emptyDashboardWidget'); + }); }, async assertDashboardPanelExists(title: string) { @@ -84,15 +78,21 @@ export function MachineLearningDashboardEmbeddablesProvider( }, async assertAnomalyChartsSeverityThresholdControlExists() { - await testSubjects.existOrFail(`mlAnomalySeverityThresholdControls`); + await retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail(`mlAnomalySeverityThresholdControls`); + }); }, async assertNoMatchingAnomaliesMessageExists() { - await testSubjects.existOrFail(`mlNoMatchingAnomaliesMessage`); + await retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail(`mlNoMatchingAnomaliesMessage`); + }); }, async assertAnomalyChartsExists() { - await testSubjects.existOrFail(`mlExplorerChartsContainer`); + await retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail(`mlExplorerChartsContainer`); + }); }, async openJobSelectionFlyout() { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index b38b605bc1b67..34e08ad257f84 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -655,7 +655,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ).to.eql([ { instance: 'eu/east', - status: 'OK', + status: 'Recovered', start: '', duration: '', }, diff --git a/x-pack/test/security_api_integration/tests/session_idle/extension.ts b/x-pack/test/security_api_integration/tests/session_idle/extension.ts index dfd72dae82346..71621f4e3db8a 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/extension.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/extension.ts @@ -59,17 +59,15 @@ export default function ({ getService }: FtrProviderContext) { describe('GET /internal/security/session', () => { it('should return current session information', async () => { const { body } = await getSessionInfo(); - expect(body.now).to.be.a('number'); - expect(body.idleTimeoutExpiration).to.be.a('number'); - expect(body.lifespanExpiration).to.be(null); + expect(body.expiresInMs).to.be.a('number'); + expect(body.canBeExtended).to.be(true); expect(body.provider).to.eql({ type: 'basic', name: 'basic1' }); }); it('should not extend the session', async () => { const { body } = await getSessionInfo(); const { body: body2 } = await getSessionInfo(); - expect(body2.now).to.be.greaterThan(body.now); - expect(body2.idleTimeoutExpiration).to.equal(body.idleTimeoutExpiration); + expect(body2.expiresInMs).to.be.lessThan(body.expiresInMs); }); }); @@ -85,8 +83,7 @@ export default function ({ getService }: FtrProviderContext) { const { body } = await getSessionInfo(); await extendSession(); const { body: body2 } = await getSessionInfo(); - expect(body2.now).to.be.greaterThan(body.now); - expect(body2.idleTimeoutExpiration).to.be.greaterThan(body.idleTimeoutExpiration); + expect(body2.expiresInMs).not.to.be.lessThan(body.expiresInMs); }); }); }); diff --git a/yarn.lock b/yarn.lock index 8a6dd285fb0e8..4857c7c908293 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1204,10 +1204,10 @@ resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.15.10.tgz#cf0cff1aec6d8e7bb23e1fc618d09fbd39b7a13f" integrity sha512-0v+OwCQ6fsGFa50r6MXWbUkSGuWOoZ22K4pMSdtWiL5LKFIE4kfmMmtQS+M7/ICNwk2EIYob+NRreyi/DGUz5A== -"@bazel/typescript@^3.4.2": - version "3.4.2" - resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.4.2.tgz#183cb14d1f4149cc67ed2723c4b8a7366da5ec36" - integrity sha512-JtLdPOC7rytALJBxawxTCnxVopGstk2eXFs56zHBy+JWSeqrnwujeWZyK5qZHzpag02/JtIQ/ZKkM/DQtrXC8Q== +"@bazel/typescript@^3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.5.0.tgz#605493f4f0a5297df8a7fcccb86a1a80ea2090bb" + integrity sha512-BtGFp4nYFkQTmnONCzomk7dkmOwaINBL3piq+lykBlcc6UxLe9iCAnZpOyPypB1ReN3k3SRNAa53x6oGScQxMg== dependencies: protobufjs "6.8.8" semver "5.6.0" @@ -2634,7 +2634,7 @@ version "0.0.0" uid "" -"@kbn/docs-utils@link:packages/kbn-docs-utils": +"@kbn/docs-utils@link:bazel-bin/packages/kbn-docs-utils/npm_module": version "0.0.0" uid "" @@ -2698,10 +2698,27 @@ version "0.0.0" uid "" +"@kbn/rule-data-utils@link:packages/kbn-rule-data-utils": + version "0.0.0" + uid "" + "@kbn/securitysolution-constants@link:bazel-bin/packages/kbn-securitysolution-constants/npm_module": version "0.0.0" uid "" +"@kbn/securitysolution-es-utils@link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module": +"@kbn/securitysolution-io-ts-alerting-types@link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types/npm_module": + version "0.0.0" + uid "" + +"@kbn/securitysolution-io-ts-list-types@link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types/npm_module": + version "0.0.0" + uid "" + +"@kbn/securitysolution-io-ts-types@link:bazel-bin/packages/kbn-securitysolution-io-ts-types/npm_module": + version "0.0.0" + uid "" + "@kbn/securitysolution-io-ts-utils@link:bazel-bin/packages/kbn-securitysolution-io-ts-utils/npm_module": version "0.0.0" uid "" @@ -2726,7 +2743,7 @@ version "0.0.0" uid "" -"@kbn/telemetry-tools@link:packages/kbn-telemetry-tools": +"@kbn/telemetry-tools@link:bazel-bin/packages/kbn-telemetry-tools/npm_module": version "0.0.0" uid ""