diff --git a/.eslintrc.js b/.eslintrc.js index 09de32a91bca3..67c52117399cc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -446,6 +446,7 @@ module.exports = { '!(src|x-pack)/plugins/**/(public|server)/mocks/index.{js,mjs,ts}', '!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,mjs,ts,tsx}', '!(src|x-pack)/plugins/**/__stories__/index.{js,mjs,ts,tsx}', + '!(src|x-pack)/plugins/**/__fixtures__/index.{js,mjs,ts,tsx}', ], allowSameFolder: true, errorMessage: 'Plugins may only import from top-level public and server modules.', diff --git a/.i18nrc.json b/.i18nrc.json index 732644b43e1f7..bdfe444bb99b5 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -1,5 +1,6 @@ { "paths": { + "autocomplete": "packages/kbn-securitysolution-autocomplete/src", "console": "src/plugins/console", "core": "src/core", "discover": "src/plugins/discover", @@ -17,7 +18,9 @@ "devTools": "src/plugins/dev_tools", "expressions": "src/plugins/expressions", "expressionError": "src/plugins/expression_error", + "expressionRepeatImage": "src/plugins/expression_repeat_image", "expressionRevealImage": "src/plugins/expression_reveal_image", + "expressionShape": "src/plugins/expression_shape", "inputControl": "src/plugins/input_control_vis", "inspector": "src/plugins/inspector", "inspectorViews": "src/legacy/core_plugins/inspector_views", @@ -28,6 +31,7 @@ "management": ["src/legacy/core_plugins/management", "src/plugins/management"], "maps_legacy": "src/plugins/maps_legacy", "monaco": "packages/kbn-monaco/src", + "esQuery": "packages/kbn-es-query/src", "presentationUtil": "src/plugins/presentation_util", "indexPatternFieldEditor": "src/plugins/index_pattern_field_editor", "indexPatternManagement": "src/plugins/index_pattern_management", diff --git a/api_docs/alerting.json b/api_docs/alerting.json index ec784dc8fc991..7c860a5c19e34 100644 --- a/api_docs/alerting.json +++ b/api_docs/alerting.json @@ -669,10 +669,10 @@ "children": [ { "parentPluginId": "alerting", - "id": "def-server.AlertingApiRequestHandlerContext.getAlertsClient", + "id": "def-server.AlertingApiRequestHandlerContext.getRulesClient", "type": "Function", "tags": [], - "label": "getAlertsClient", + "label": "getRulesClient", "description": [], "signature": [ "() => ", @@ -680,8 +680,8 @@ "pluginId": "alerting", "scope": "server", "docId": "kibAlertingPluginApi", - "section": "def-server.AlertsClient", - "text": "AlertsClient" + "section": "def-server.RulesClient", + "text": "RulesClient" } ], "source": { @@ -1170,7 +1170,7 @@ "" ], "source": { - "path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", "lineNumber": 140 }, "deprecated": false, @@ -1183,7 +1183,7 @@ "label": "page", "description": [], "source": { - "path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", "lineNumber": 141 }, "deprecated": false @@ -1196,7 +1196,7 @@ "label": "perPage", "description": [], "source": { - "path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", "lineNumber": 142 }, "deprecated": false @@ -1209,7 +1209,7 @@ "label": "total", "description": [], "source": { - "path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", "lineNumber": 143 }, "deprecated": false @@ -1233,7 +1233,7 @@ ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">[]" ], "source": { - "path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts", + "path": "x-pack/plugins/alerting/server/rules_client/rules_client.ts", "lineNumber": 144 }, "deprecated": false @@ -1343,10 +1343,10 @@ }, { "parentPluginId": "alerting", - "id": "def-server.PluginStartContract.getAlertsClientWithRequest", + "id": "def-server.PluginStartContract.getRulesClientWithRequest", "type": "Function", "tags": [], - "label": "getAlertsClientWithRequest", + "label": "getRulesClientWithRequest", "description": [], "signature": [ "(request: ", @@ -1362,8 +1362,8 @@ "pluginId": "alerting", "scope": "server", "docId": "kibAlertingPluginApi", - "section": "def-server.AlertsClient", - "text": "AlertsClient" + "section": "def-server.RulesClient", + "text": "RulesClient" }, ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"aggregate\" | \"enable\" | \"disable\" | \"muteAll\" | \"getAlertState\" | \"getAlertInstanceSummary\" | \"updateApiKey\" | \"unmuteAll\" | \"muteInstance\" | \"unmuteInstance\" | \"listAlertTypes\">" ], @@ -1375,7 +1375,7 @@ "children": [ { "parentPluginId": "alerting", - "id": "def-server.PluginStartContract.getAlertsClientWithRequest.$1", + "id": "def-server.PluginStartContract.getRulesClientWithRequest.$1", "type": "Object", "tags": [], "label": "request", @@ -1571,10 +1571,10 @@ }, { "parentPluginId": "alerting", - "id": "def-server.AlertsClient", + "id": "def-server.RulesClient", "type": "Type", "tags": [], - "label": "AlertsClient", + "label": "RulesClient", "description": [], "signature": [ "{ get: = never>({ id, }: { id: string; }) => Promise | [audit_logger.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/authorization/audit_logger.ts#L8) | - | | | [audit_logger.ts#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/authorization/audit_logger.ts#L21) | - | | | [audit_logger.ts#L23](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/authorization/audit_logger.ts#L23) | - | -| | [alerts_client_factory.test.ts#L23](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/alerts_client_factory.test.ts#L23) | - | -| | [alerts_client_factory.test.ts#L98](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/alerts_client_factory.test.ts#L98) | - | +| | [rules_client_factory.test.ts#L23](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/rules_client_factory.test.ts#L23) | - | +| | [rules_client_factory.test.ts#L98](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerting/server/rules_client_factory.test.ts#L98) | - | diff --git a/api_docs/security.json b/api_docs/security.json index ac0982be3f19e..a02e4cebc5fbf 100644 --- a/api_docs/security.json +++ b/api_docs/security.json @@ -1739,14 +1739,14 @@ { "plugin": "alerting", "link": { - "path": "x-pack/plugins/alerting/server/alerts_client_factory.test.ts", + "path": "x-pack/plugins/alerting/server/rules_client_factory.test.ts", "lineNumber": 23 } }, { "plugin": "alerting", "link": { - "path": "x-pack/plugins/alerting/server/alerts_client_factory.test.ts", + "path": "x-pack/plugins/alerting/server/rules_client_factory.test.ts", "lineNumber": 98 } } diff --git a/docs/dev-tools/painlesslab/index.asciidoc b/docs/dev-tools/painlesslab/index.asciidoc index 7b4e9101a9901..4077ffe87ca1a 100644 --- a/docs/dev-tools/painlesslab/index.asciidoc +++ b/docs/dev-tools/painlesslab/index.asciidoc @@ -7,7 +7,7 @@ beta::[] The *Painless Lab* is an interactive code editor that lets you test and debug {ref}/modules-scripting-painless.html[Painless scripts] in real-time. You can use the Painless scripting -language to create <>, +language to create <>, process {ref}/docs-reindex.html[reindexed data], define complex <>, and work with data in other contexts. diff --git a/docs/developer/advanced/running-elasticsearch.asciidoc b/docs/developer/advanced/running-elasticsearch.asciidoc index e5c86fafd1ce7..324d2af2ed3af 100644 --- a/docs/developer/advanced/running-elasticsearch.asciidoc +++ b/docs/developer/advanced/running-elasticsearch.asciidoc @@ -25,7 +25,7 @@ See all available options, like how to specify a specific license, with the `--h yarn es snapshot --help ---- -`trial` will give you access to all capabilities. +`--license trial` will give you access to all capabilities. **Keeping data between snapshots** diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index b656405b173d8..0b635df68aca4 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -91,6 +91,7 @@ yarn kbn watch-bazel - @kbn/optimizer - @kbn/plugin-helpers - @kbn/rule-data-utils +- @kbn/securitysolution-autocomplete - @kbn/securitysolution-es-utils - @kbn/securitysolution-hook-utils - @kbn/securitysolution-io-ts-alerting-types diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index c1535e8a2146f..77f16a9d69d46 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -76,6 +76,10 @@ This API doesn't support angular, for registering angular dev tools, bootstrap a |Expression Error plugin adds an error renderer to the expression plugin. The renderer will display the error image. +|{kib-repo}blob/{branch}/src/plugins/expression_repeat_image/README.md[expressionRepeatImage] +|Expression Repeat Image plugin adds a repeatImage function to the expression plugin and an associated renderer. The renderer will display the given image in mutliple instances. + + |{kib-repo}blob/{branch}/src/plugins/expression_reveal_image/README.md[expressionRevealImage] |Expression Reveal Image plugin adds a revealImage function to the expression plugin and an associated renderer. The renderer will display the given percentage of a given image. @@ -99,6 +103,10 @@ want to incorporate their own functions, types, and renderers into the service for use in their own application. +|{kib-repo}blob/{branch}/src/plugins/expression_shape/README.md[expressionShape] +|Expression Shape plugin adds a shape function to the expression plugin and an associated renderer. The renderer will display the given shape with selected decorations. + + |{kib-repo}blob/{branch}/src/plugins/home/README.md[home] |Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getsearchsourcetimefilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getsearchsourcetimefilter.md index 1f8bc1300a0a8..9ebc685f2a77d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getsearchsourcetimefilter.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getsearchsourcetimefilter.md @@ -7,7 +7,7 @@ Signature: ```typescript -getSearchSourceTimeFilter(forceNow?: Date): RangeFilter[] | { +getSearchSourceTimeFilter(forceNow?: Date): import("@kbn/es-query").RangeFilter[] | { meta: { index: string | undefined; params: {}; @@ -43,7 +43,7 @@ getSearchSourceTimeFilter(forceNow?: Date): RangeFilter[] | { Returns: -`RangeFilter[] | { +`import("@kbn/es-query").RangeFilter[] | { meta: { index: string | undefined; params: {}; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.customfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.customfilter.md index 0a3b4e54cfe55..6763a8d2ba0bf 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.customfilter.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.customfilter.md @@ -4,10 +4,13 @@ ## CustomFilter type +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript -export declare type CustomFilter = Filter & { - query: any; -}; +declare type CustomFilter = oldCustomFilter; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.es_field_types.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.es_field_types.md index 30f9189ddb551..61929355decc2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.es_field_types.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.es_field_types.md @@ -50,4 +50,5 @@ export declare enum ES_FIELD_TYPES | TEXT | "text" | | | TOKEN\_COUNT | "token_count" | | | UNSIGNED\_LONG | "unsigned_long" | | +| VERSION | "version" | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md index d06ce1b2ef2bc..eb06d99426197 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -10,23 +10,23 @@ esFilters: { FilterLabel: (props: import("./ui/filter_bar/filter_editor/lib/filter_label").FilterLabelProps) => JSX.Element; FilterItem: (props: import("./ui/filter_bar/filter_item").FilterItemProps) => JSX.Element; - FILTERS: typeof FILTERS; + FILTERS: typeof import("@kbn/es-query").FILTERS; FilterStateStore: typeof FilterStateStore; - buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; - buildPhrasesFilter: (field: import("../common").IndexPatternFieldBase, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter; - buildExistsFilter: (field: import("../common").IndexPatternFieldBase, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter; - buildPhraseFilter: (field: import("../common").IndexPatternFieldBase, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter; - buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter; - buildRangeFilter: (field: import("../common").IndexPatternFieldBase, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter; - isPhraseFilter: (filter: any) => filter is import("../common").PhraseFilter; - isExistsFilter: (filter: any) => filter is import("../common").ExistsFilter; - isPhrasesFilter: (filter: any) => filter is import("../common").PhrasesFilter; - isRangeFilter: (filter: any) => filter is import("../common").RangeFilter; - isMatchAllFilter: (filter: any) => filter is import("../common").MatchAllFilter; - isMissingFilter: (filter: any) => filter is import("../common").MissingFilter; - isQueryStringFilter: (filter: any) => filter is import("../common").QueryStringFilter; - isFilterPinned: (filter: import("../common").Filter) => boolean | undefined; - toggleFilterNegated: (filter: import("../common").Filter) => { + buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("@kbn/es-query").Filter; + buildPhrasesFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, params: any[], indexPattern: import("@kbn/es-query").IndexPatternBase) => import("@kbn/es-query").PhrasesFilter; + buildExistsFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, indexPattern: import("@kbn/es-query").IndexPatternBase) => import("@kbn/es-query").ExistsFilter; + buildPhraseFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, value: any, indexPattern: import("@kbn/es-query").IndexPatternBase) => import("@kbn/es-query").PhraseFilter; + buildQueryFilter: (query: any, index: string, alias: string) => import("@kbn/es-query").QueryStringFilter; + buildRangeFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, params: import("@kbn/es-query").RangeFilterParams, indexPattern: import("@kbn/es-query").IndexPatternBase, formattedValue?: string | undefined) => import("@kbn/es-query").RangeFilter; + isPhraseFilter: (filter: any) => filter is import("@kbn/es-query").PhraseFilter; + isExistsFilter: (filter: any) => filter is import("@kbn/es-query").ExistsFilter; + isPhrasesFilter: (filter: any) => filter is import("@kbn/es-query").PhrasesFilter; + isRangeFilter: (filter: any) => filter is import("@kbn/es-query").RangeFilter; + isMatchAllFilter: (filter: any) => filter is import("@kbn/es-query").MatchAllFilter; + isMissingFilter: (filter: any) => filter is import("@kbn/es-query").MissingFilter; + isQueryStringFilter: (filter: any) => filter is import("@kbn/es-query").QueryStringFilter; + isFilterPinned: (filter: import("@kbn/es-query").Filter) => boolean | undefined; + toggleFilterNegated: (filter: import("@kbn/es-query").Filter) => { meta: { negate: boolean; alias: string | null; @@ -39,20 +39,20 @@ esFilters: { params?: any; value?: string | undefined; }; - $state?: import("../common").FilterState | undefined; + $state?: import("@kbn/es-query/target_types/filters/types").FilterState | undefined; query?: any; }; - disableFilter: (filter: import("../common").Filter) => import("../common").Filter; - getPhraseFilterField: (filter: import("../common").PhraseFilter) => string; - getPhraseFilterValue: (filter: import("../common").PhraseFilter) => string | number | boolean; + disableFilter: (filter: import("@kbn/es-query").Filter) => import("@kbn/es-query").Filter; + getPhraseFilterField: (filter: import("@kbn/es-query").PhraseFilter) => string; + getPhraseFilterValue: (filter: import("@kbn/es-query").PhraseFilter) => string | number | boolean; getDisplayValueFromFilter: typeof getDisplayValueFromFilter; - compareFilters: (first: import("../common").Filter | import("../common").Filter[], second: import("../common").Filter | import("../common").Filter[], comparatorOptions?: import("../common").FilterCompareOptions) => boolean; + compareFilters: (first: import("@kbn/es-query").Filter | import("@kbn/es-query").Filter[], second: import("@kbn/es-query").Filter | import("@kbn/es-query").Filter[], comparatorOptions?: import("../common").FilterCompareOptions) => boolean; COMPARE_ALL_OPTIONS: import("../common").FilterCompareOptions; generateFilters: typeof generateFilters; - onlyDisabledFiltersChanged: (newFilters?: import("../common").Filter[] | undefined, oldFilters?: import("../common").Filter[] | undefined) => boolean; + onlyDisabledFiltersChanged: (newFilters?: import("@kbn/es-query").Filter[] | undefined, oldFilters?: import("@kbn/es-query").Filter[] | undefined) => boolean; changeTimeFilter: typeof changeTimeFilter; convertRangeFilterToTimeRangeString: typeof convertRangeFilterToTimeRangeString; - mapAndFlattenFilters: (filters: import("../common").Filter[]) => import("../common").Filter[]; + mapAndFlattenFilters: (filters: import("@kbn/es-query").Filter[]) => import("@kbn/es-query").Filter[]; extractTimeFilter: typeof extractTimeFilter; extractTimeRange: typeof extractTimeRange; } diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md index 332114e637586..6ed9898ddd718 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md @@ -4,12 +4,17 @@ ## esKuery variable +> Warning: This API is now obsolete. +> +> Please import helpers from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript esKuery: { - nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; - fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; + fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; + toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md index 0bc9c0c12fc3a..fa2ee4faa7466 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md @@ -4,19 +4,24 @@ ## esQuery variable +> Warning: This API is now obsolete. +> +> Please import helpers from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript esQuery: { - buildEsQuery: typeof buildEsQuery; + buildEsQuery: typeof import("@kbn/es-query").buildEsQuery; getEsQueryConfig: typeof getEsQueryConfig; - buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { + buildQueryFromFilters: (filters: import("@kbn/es-query").Filter[] | undefined, indexPattern: import("@kbn/es-query").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean | undefined) => { must: never[]; - filter: import("../common").Filter[]; + filter: import("@kbn/es-query").Filter[]; should: never[]; - must_not: import("../common").Filter[]; + must_not: import("@kbn/es-query").Filter[]; }; - luceneStringToDsl: typeof luceneStringToDsl; - decorateQuery: typeof decorateQuery; + luceneStringToDsl: typeof import("@kbn/es-query").luceneStringToDsl; + decorateQuery: typeof import("@kbn/es-query").decorateQuery; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.allowleadingwildcards.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.allowleadingwildcards.md deleted file mode 100644 index 71eb23ac6299b..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.allowleadingwildcards.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) > [allowLeadingWildcards](./kibana-plugin-plugins-data-public.esqueryconfig.allowleadingwildcards.md) - -## EsQueryConfig.allowLeadingWildcards property - -Signature: - -```typescript -allowLeadingWildcards: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.dateformattz.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.dateformattz.md deleted file mode 100644 index e9c4c26878a97..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.dateformattz.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) > [dateFormatTZ](./kibana-plugin-plugins-data-public.esqueryconfig.dateformattz.md) - -## EsQueryConfig.dateFormatTZ property - -Signature: - -```typescript -dateFormatTZ?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.ignorefilteriffieldnotinindex.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.ignorefilteriffieldnotinindex.md deleted file mode 100644 index 9f765c51d0a69..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.ignorefilteriffieldnotinindex.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) > [ignoreFilterIfFieldNotInIndex](./kibana-plugin-plugins-data-public.esqueryconfig.ignorefilteriffieldnotinindex.md) - -## EsQueryConfig.ignoreFilterIfFieldNotInIndex property - -Signature: - -```typescript -ignoreFilterIfFieldNotInIndex: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.md index 5252f8058b488..4480329c2df19 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.md @@ -2,20 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) -## EsQueryConfig interface +## EsQueryConfig type + +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> Signature: ```typescript -export interface EsQueryConfig +declare type EsQueryConfig = oldEsQueryConfig; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [allowLeadingWildcards](./kibana-plugin-plugins-data-public.esqueryconfig.allowleadingwildcards.md) | boolean | | -| [dateFormatTZ](./kibana-plugin-plugins-data-public.esqueryconfig.dateformattz.md) | string | | -| [ignoreFilterIfFieldNotInIndex](./kibana-plugin-plugins-data-public.esqueryconfig.ignorefilteriffieldnotinindex.md) | boolean | | -| [queryStringOptions](./kibana-plugin-plugins-data-public.esqueryconfig.querystringoptions.md) | Record<string, any> | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.querystringoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.querystringoptions.md deleted file mode 100644 index feaa8f1821e30..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esqueryconfig.querystringoptions.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) > [queryStringOptions](./kibana-plugin-plugins-data-public.esqueryconfig.querystringoptions.md) - -## EsQueryConfig.queryStringOptions property - -Signature: - -```typescript -queryStringOptions: Record; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.existsfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.existsfilter.md index f1279934db84c..ab756295eac8c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.existsfilter.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.existsfilter.md @@ -4,11 +4,13 @@ ## ExistsFilter type +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript -export declare type ExistsFilter = Filter & { - meta: ExistsFilterMeta; - exists?: FilterExistsProperty; -}; +declare type ExistsFilter = oldExistsFilter; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.md index 9212b757e07df..bf8d4ced016a3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.md @@ -4,12 +4,13 @@ ## Filter type +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript -export declare type Filter = { - $state?: FilterState; - meta: FilterMeta; - query?: any; -}; +declare type Filter = oldFilter; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.extract.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.extract.md new file mode 100644 index 0000000000000..60ea060cf6323 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.extract.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) > [extract](./kibana-plugin-plugins-data-public.filtermanager.extract.md) + +## FilterManager.extract property + +Signature: + +```typescript +extract: any; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.getallmigrations.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.getallmigrations.md new file mode 100644 index 0000000000000..0d46d806f0563 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.getallmigrations.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) > [getAllMigrations](./kibana-plugin-plugins-data-public.filtermanager.getallmigrations.md) + +## FilterManager.getAllMigrations property + +Signature: + +```typescript +getAllMigrations: () => {}; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.inject.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.inject.md new file mode 100644 index 0000000000000..0e3b84cd3cf80 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.inject.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) > [inject](./kibana-plugin-plugins-data-public.filtermanager.inject.md) + +## FilterManager.inject property + +Signature: + +```typescript +inject: any; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.migratetolatest.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.migratetolatest.md new file mode 100644 index 0000000000000..2235c55947865 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.migratetolatest.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) > [migrateToLatest](./kibana-plugin-plugins-data-public.filtermanager.migratetolatest.md) + +## FilterManager.migrateToLatest property + +Signature: + +```typescript +migrateToLatest: any; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md new file mode 100644 index 0000000000000..bab6452c34903 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) > [telemetry](./kibana-plugin-plugins-data-public.filtermanager.telemetry.md) + +## FilterManager.telemetry property + +Signature: + +```typescript +telemetry: (filters: import("../../../../kibana_utils/common/persistable_state").SerializableState, collector: unknown) => {}; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md index 3969a97fa7789..7fd1914d1a4a5 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md @@ -10,7 +10,7 @@ export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: { forceNow?: Date; fieldName?: string; -}): import("../..").RangeFilter | undefined; +}): import("@kbn/es-query").RangeFilter | undefined; ``` ## Parameters @@ -23,5 +23,5 @@ export declare function getTime(indexPattern: IIndexPattern | undefined, timeRan Returns: -`import("../..").RangeFilter | undefined` +`import("@kbn/es-query").RangeFilter | undefined` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldsubtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldsubtype.md index 7e6ea86d7f3e8..d5d8a0b62d3c5 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldsubtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldsubtype.md @@ -2,18 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IFieldSubType](./kibana-plugin-plugins-data-public.ifieldsubtype.md) -## IFieldSubType interface +## IFieldSubType type + +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> Signature: ```typescript -export interface IFieldSubType +declare type IFieldSubType = oldIFieldSubType; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [multi](./kibana-plugin-plugins-data-public.ifieldsubtype.multi.md) | {
parent: string;
} | | -| [nested](./kibana-plugin-plugins-data-public.ifieldsubtype.nested.md) | {
path: string;
} | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldsubtype.multi.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldsubtype.multi.md deleted file mode 100644 index 6cfc6f037d013..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldsubtype.multi.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IFieldSubType](./kibana-plugin-plugins-data-public.ifieldsubtype.md) > [multi](./kibana-plugin-plugins-data-public.ifieldsubtype.multi.md) - -## IFieldSubType.multi property - -Signature: - -```typescript -multi?: { - parent: string; - }; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldsubtype.nested.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldsubtype.nested.md deleted file mode 100644 index f9308b90a1b96..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldsubtype.nested.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IFieldSubType](./kibana-plugin-plugins-data-public.ifieldsubtype.md) > [nested](./kibana-plugin-plugins-data-public.ifieldsubtype.nested.md) - -## IFieldSubType.nested property - -Signature: - -```typescript -nested?: { - path: string; - }; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index 16546ceca958d..dc206ceabefe2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -37,7 +37,7 @@ export declare class IndexPatternField implements IFieldType | [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) | | boolean | | | [sortable](./kibana-plugin-plugins-data-public.indexpatternfield.sortable.md) | | boolean | | | [spec](./kibana-plugin-plugins-data-public.indexpatternfield.spec.md) | | FieldSpec | | -| [subType](./kibana-plugin-plugins-data-public.indexpatternfield.subtype.md) | | import("../..").IFieldSubType | undefined | | +| [subType](./kibana-plugin-plugins-data-public.indexpatternfield.subtype.md) | | import("@kbn/es-query").IFieldSubType | undefined | | | [type](./kibana-plugin-plugins-data-public.indexpatternfield.type.md) | | string | | | [visualizable](./kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.subtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.subtype.md index 6cd5247291602..f5e25e3191f72 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.subtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.subtype.md @@ -7,5 +7,5 @@ Signature: ```typescript -get subType(): import("../..").IFieldSubType | undefined; +get subType(): import("@kbn/es-query").IFieldSubType | undefined; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md index b77f3d1f374fb..9afcef6afed3a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md @@ -19,7 +19,7 @@ toJSON(): { searchable: boolean; aggregatable: boolean; readFromDocValues: boolean; - subType: import("../..").IFieldSubType | undefined; + subType: import("@kbn/es-query").IFieldSubType | undefined; customLabel: string | undefined; }; ``` @@ -37,7 +37,7 @@ toJSON(): { searchable: boolean; aggregatable: boolean; readFromDocValues: boolean; - subType: import("../..").IFieldSubType | undefined; + subType: import("@kbn/es-query").IFieldSubType | undefined; customLabel: string | undefined; }` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md index f1916e89c2c98..2848e20edde1b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md @@ -4,8 +4,13 @@ ## isFilter variable +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript -isFilter: (x: unknown) => x is Filter +isFilter: (x: unknown) => x is oldFilter ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md index 558da72cc26bb..881d50b8a49e1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md @@ -4,8 +4,13 @@ ## isFilters variable +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript -isFilters: (x: unknown) => x is Filter[] +isFilters: (x: unknown) => x is oldFilter[] ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kuerynode.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kuerynode.md index 276f25da8cb9f..9cea144ff9d46 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kuerynode.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kuerynode.md @@ -2,17 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) -## KueryNode interface +## KueryNode type + +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> Signature: ```typescript -export interface KueryNode +declare type KueryNode = oldKueryNode; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [type](./kibana-plugin-plugins-data-public.kuerynode.type.md) | keyof NodeTypes | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kuerynode.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kuerynode.type.md deleted file mode 100644 index 2ff96b6421c2e..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kuerynode.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) > [type](./kibana-plugin-plugins-data-public.kuerynode.type.md) - -## KueryNode.type property - -Signature: - -```typescript -type: keyof NodeTypes; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.matchallfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.matchallfilter.md index 740b83bb5c563..39ae82865808c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.matchallfilter.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.matchallfilter.md @@ -4,11 +4,13 @@ ## MatchAllFilter type +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript -export declare type MatchAllFilter = Filter & { - meta: MatchAllFilterMeta; - match_all: any; -}; +declare type MatchAllFilter = oldMatchAllFilter; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 7c2911875ee05..e60e26bcb503e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -62,11 +62,9 @@ | [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | Data plugin public Start contract | | [DataPublicPluginStartActions](./kibana-plugin-plugins-data-public.datapublicpluginstartactions.md) | utilities to generate filters from action context | | [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) | Data plugin prewired UI components | -| [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-public.fieldformatconfig.md) | | | [IDataPluginServices](./kibana-plugin-plugins-data-public.idatapluginservices.md) | | | [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) | | -| [IFieldSubType](./kibana-plugin-plugins-data-public.ifieldsubtype.md) | | | [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) | | | [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) | | | [IIndexPatternFieldList](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.md) | | @@ -79,7 +77,6 @@ | [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. | | [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) | search service | | [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | high level search service | -| [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | | | [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state | | [QueryStateChange](./kibana-plugin-plugins-data-public.querystatechange.md) | | @@ -87,7 +84,6 @@ | [QuerySuggestionBasic](./kibana-plugin-plugins-data-public.querysuggestionbasic.md) | \* | | [QuerySuggestionField](./kibana-plugin-plugins-data-public.querysuggestionfield.md) | \* | | [QuerySuggestionGetFnArgs](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md) | \* | -| [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) | | | [Reason](./kibana-plugin-plugins-data-public.reason.md) | | | [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) | | | [SavedQuery](./kibana-plugin-plugins-data-public.savedquery.md) | | @@ -151,6 +147,7 @@ | [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | | | [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md) | | | [EsdslExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esdslexpressionfunctiondefinition.md) | | +| [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) | | | [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | | | [EsRawResponseExpressionTypeDefinition](./kibana-plugin-plugins-data-public.esrawresponseexpressiontypedefinition.md) | | | [ExecutionContextSearch](./kibana-plugin-plugins-data-public.executioncontextsearch.md) | | @@ -170,6 +167,7 @@ | [IFieldFormat](./kibana-plugin-plugins-data-public.ifieldformat.md) | | | [IFieldFormatsRegistry](./kibana-plugin-plugins-data-public.ifieldformatsregistry.md) | | | [IFieldParamType](./kibana-plugin-plugins-data-public.ifieldparamtype.md) | | +| [IFieldSubType](./kibana-plugin-plugins-data-public.ifieldsubtype.md) | | | [IMetricAggType](./kibana-plugin-plugins-data-public.imetricaggtype.md) | | | [IndexPatternAggRestrictions](./kibana-plugin-plugins-data-public.indexpatternaggrestrictions.md) | | | [IndexPatternLoadExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.indexpatternloadexpressionfunctiondefinition.md) | | @@ -181,16 +179,17 @@ | [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | | | [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | | [KibanaContext](./kibana-plugin-plugins-data-public.kibanacontext.md) | | +| [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | | | [MatchAllFilter](./kibana-plugin-plugins-data-public.matchallfilter.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | | | [PhraseFilter](./kibana-plugin-plugins-data-public.phrasefilter.md) | | | [PhrasesFilter](./kibana-plugin-plugins-data-public.phrasesfilter.md) | | -| [Query](./kibana-plugin-plugins-data-public.query.md) | | | [QueryStart](./kibana-plugin-plugins-data-public.querystart.md) | | | [QuerySuggestion](./kibana-plugin-plugins-data-public.querysuggestion.md) | \* | | [QuerySuggestionGetFn](./kibana-plugin-plugins-data-public.querysuggestiongetfn.md) | | | [RangeFilter](./kibana-plugin-plugins-data-public.rangefilter.md) | | | [RangeFilterMeta](./kibana-plugin-plugins-data-public.rangefiltermeta.md) | | +| [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) | | | [SavedQueryTimeFilter](./kibana-plugin-plugins-data-public.savedquerytimefilter.md) | | | [SearchBarProps](./kibana-plugin-plugins-data-public.searchbarprops.md) | | | [StatefulSearchBarProps](./kibana-plugin-plugins-data-public.statefulsearchbarprops.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.phrasefilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.phrasefilter.md index 8d0447d58634c..ca38ac25dcf50 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.phrasefilter.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.phrasefilter.md @@ -4,17 +4,13 @@ ## PhraseFilter type +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript -export declare type PhraseFilter = Filter & { - meta: PhraseFilterMeta; - script?: { - script: { - source?: any; - lang?: estypes.ScriptLanguage; - params: any; - }; - }; -}; +declare type PhraseFilter = oldPhraseFilter; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.phrasesfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.phrasesfilter.md index ab205cb62fd14..0c293cb909276 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.phrasesfilter.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.phrasesfilter.md @@ -4,10 +4,13 @@ ## PhrasesFilter type +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript -export declare type PhrasesFilter = Filter & { - meta: PhrasesFilterMeta; -}; +declare type PhrasesFilter = oldPhrasesFilter; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.md deleted file mode 100644 index e15b04236a0b5..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Query](./kibana-plugin-plugins-data-public.query.md) - -## Query type - -Signature: - -```typescript -export declare type Query = { - query: string | { - [key: string]: any; - }; - language: string; -}; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilter.md index 1cb627ec3a8f9..3d9af100a707a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilter.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilter.md @@ -4,18 +4,13 @@ ## RangeFilter type +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript -export declare type RangeFilter = Filter & EsRangeFilter & { - meta: RangeFilterMeta; - script?: { - script: { - params: any; - lang: estypes.ScriptLanguage; - source: any; - }; - }; - match_all?: any; -}; +declare type RangeFilter = oldRangeFilter; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefiltermeta.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefiltermeta.md index 609e98cb6faa8..4060a71e62cd0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefiltermeta.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefiltermeta.md @@ -4,12 +4,13 @@ ## RangeFilterMeta type +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript -export declare type RangeFilterMeta = FilterMeta & { - params: RangeFilterParams; - field?: any; - formattedValue?: string; -}; +declare type RangeFilterMeta = oldRangeFilterMeta; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.format.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.format.md deleted file mode 100644 index 15926481923ab..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.format.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) > [format](./kibana-plugin-plugins-data-public.rangefilterparams.format.md) - -## RangeFilterParams.format property - -Signature: - -```typescript -format?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.from.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.from.md deleted file mode 100644 index 99b8d75e9c316..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.from.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) > [from](./kibana-plugin-plugins-data-public.rangefilterparams.from.md) - -## RangeFilterParams.from property - -Signature: - -```typescript -from?: number | string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.gt.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.gt.md deleted file mode 100644 index 32bfc6eeb68cb..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.gt.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) > [gt](./kibana-plugin-plugins-data-public.rangefilterparams.gt.md) - -## RangeFilterParams.gt property - -Signature: - -```typescript -gt?: number | string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.gte.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.gte.md deleted file mode 100644 index 81345e4a81610..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.gte.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) > [gte](./kibana-plugin-plugins-data-public.rangefilterparams.gte.md) - -## RangeFilterParams.gte property - -Signature: - -```typescript -gte?: number | string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.lt.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.lt.md deleted file mode 100644 index 6250fecfe59d6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.lt.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) > [lt](./kibana-plugin-plugins-data-public.rangefilterparams.lt.md) - -## RangeFilterParams.lt property - -Signature: - -```typescript -lt?: number | string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.lte.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.lte.md deleted file mode 100644 index c4f3cbf00b304..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.lte.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) > [lte](./kibana-plugin-plugins-data-public.rangefilterparams.lte.md) - -## RangeFilterParams.lte property - -Signature: - -```typescript -lte?: number | string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.md index 977559f5e6cb2..cdf237ea5a1ec 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.md @@ -2,23 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) -## RangeFilterParams interface +## RangeFilterParams type + +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> Signature: ```typescript -export interface RangeFilterParams +declare type RangeFilterParams = oldRangeFilterParams; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [format](./kibana-plugin-plugins-data-public.rangefilterparams.format.md) | string | | -| [from](./kibana-plugin-plugins-data-public.rangefilterparams.from.md) | number | string | | -| [gt](./kibana-plugin-plugins-data-public.rangefilterparams.gt.md) | number | string | | -| [gte](./kibana-plugin-plugins-data-public.rangefilterparams.gte.md) | number | string | | -| [lt](./kibana-plugin-plugins-data-public.rangefilterparams.lt.md) | number | string | | -| [lte](./kibana-plugin-plugins-data-public.rangefilterparams.lte.md) | number | string | | -| [to](./kibana-plugin-plugins-data-public.rangefilterparams.to.md) | number | string | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.to.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.to.md deleted file mode 100644 index c9d0069fb75f5..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.rangefilterparams.to.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) > [to](./kibana-plugin-plugins-data-public.rangefilterparams.to.md) - -## RangeFilterParams.to property - -Signature: - -```typescript -to?: number | string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md index 981d956a9e89b..22dc6fa9f627b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md @@ -25,7 +25,7 @@ export interface SearchSourceFields | [highlightAll](./kibana-plugin-plugins-data-public.searchsourcefields.highlightall.md) | boolean | | | [index](./kibana-plugin-plugins-data-public.searchsourcefields.index.md) | IndexPattern | | | [parent](./kibana-plugin-plugins-data-public.searchsourcefields.parent.md) | SearchSourceFields | | -| [query](./kibana-plugin-plugins-data-public.searchsourcefields.query.md) | Query | [Query](./kibana-plugin-plugins-data-public.query.md) | +| [query](./kibana-plugin-plugins-data-public.searchsourcefields.query.md) | Query | | | [searchAfter](./kibana-plugin-plugins-data-public.searchsourcefields.searchafter.md) | EsQuerySearchAfter | | | [size](./kibana-plugin-plugins-data-public.searchsourcefields.size.md) | number | | | [sort](./kibana-plugin-plugins-data-public.searchsourcefields.sort.md) | EsQuerySortValue | EsQuerySortValue[] | [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.query.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.query.md index 661ce94a06afb..78bf800c58c20 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.query.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.query.md @@ -4,7 +4,6 @@ ## SearchSourceFields.query property -[Query](./kibana-plugin-plugins-data-public.query.md) Signature: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.es_field_types.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.es_field_types.md index 02199fcc24fe2..5061e3e97a346 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.es_field_types.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.es_field_types.md @@ -50,4 +50,5 @@ export declare enum ES_FIELD_TYPES | TEXT | "text" | | | TOKEN\_COUNT | "token_count" | | | UNSIGNED\_LONG | "unsigned_long" | | +| VERSION | "version" | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md index 594afcf9ee0dd..9006b088993a1 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md @@ -8,14 +8,14 @@ ```typescript esFilters: { - buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter; - buildCustomFilter: typeof buildCustomFilter; - buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; - buildExistsFilter: (field: import("../common").IndexPatternFieldBase, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter; - buildFilter: typeof buildFilter; - buildPhraseFilter: (field: import("../common").IndexPatternFieldBase, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter; - buildPhrasesFilter: (field: import("../common").IndexPatternFieldBase, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter; - buildRangeFilter: (field: import("../common").IndexPatternFieldBase, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter; - isFilterDisabled: (filter: import("../common").Filter) => boolean; + buildQueryFilter: (query: any, index: string, alias: string) => import("@kbn/es-query").QueryStringFilter; + buildCustomFilter: typeof import("@kbn/es-query").buildCustomFilter; + buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("@kbn/es-query").Filter; + buildExistsFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, indexPattern: import("@kbn/es-query").IndexPatternBase) => import("@kbn/es-query").ExistsFilter; + buildFilter: typeof import("@kbn/es-query").buildFilter; + buildPhraseFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, value: any, indexPattern: import("@kbn/es-query").IndexPatternBase) => import("@kbn/es-query").PhraseFilter; + buildPhrasesFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, params: any[], indexPattern: import("@kbn/es-query").IndexPatternBase) => import("@kbn/es-query").PhrasesFilter; + buildRangeFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, params: import("@kbn/es-query").RangeFilterParams, indexPattern: import("@kbn/es-query").IndexPatternBase, formattedValue?: string | undefined) => import("@kbn/es-query").RangeFilter; + isFilterDisabled: (filter: import("@kbn/es-query").Filter) => boolean; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md index fce25a899de8e..4989b2b5ad584 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md @@ -8,8 +8,8 @@ ```typescript esKuery: { - nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; - fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; + fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; + toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md index 68507f3fb9b81..8dfea00081d89 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md @@ -8,13 +8,13 @@ ```typescript esQuery: { - buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { + buildQueryFromFilters: (filters: import("@kbn/es-query").Filter[] | undefined, indexPattern: import("@kbn/es-query").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean | undefined) => { must: never[]; - filter: import("../common").Filter[]; + filter: import("@kbn/es-query").Filter[]; should: never[]; - must_not: import("../common").Filter[]; + must_not: import("@kbn/es-query").Filter[]; }; getEsQueryConfig: typeof getEsQueryConfig; - buildEsQuery: typeof buildEsQuery; + buildEsQuery: typeof import("@kbn/es-query").buildEsQuery; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.allowleadingwildcards.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.allowleadingwildcards.md deleted file mode 100644 index ce8303d720747..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.allowleadingwildcards.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) > [allowLeadingWildcards](./kibana-plugin-plugins-data-server.esqueryconfig.allowleadingwildcards.md) - -## EsQueryConfig.allowLeadingWildcards property - -Signature: - -```typescript -allowLeadingWildcards: boolean; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.dateformattz.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.dateformattz.md deleted file mode 100644 index d3e86f19709f8..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.dateformattz.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) > [dateFormatTZ](./kibana-plugin-plugins-data-server.esqueryconfig.dateformattz.md) - -## EsQueryConfig.dateFormatTZ property - -Signature: - -```typescript -dateFormatTZ?: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.ignorefilteriffieldnotinindex.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.ignorefilteriffieldnotinindex.md deleted file mode 100644 index 93b3e8915c482..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.ignorefilteriffieldnotinindex.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) > [ignoreFilterIfFieldNotInIndex](./kibana-plugin-plugins-data-server.esqueryconfig.ignorefilteriffieldnotinindex.md) - -## EsQueryConfig.ignoreFilterIfFieldNotInIndex property - -Signature: - -```typescript -ignoreFilterIfFieldNotInIndex: boolean; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.md index 9ae604e07cabd..5c736f40cdbf4 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.md @@ -2,20 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) -## EsQueryConfig interface +## EsQueryConfig type + +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> Signature: ```typescript -export interface EsQueryConfig +declare type EsQueryConfig = oldEsQueryConfig; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [allowLeadingWildcards](./kibana-plugin-plugins-data-server.esqueryconfig.allowleadingwildcards.md) | boolean | | -| [dateFormatTZ](./kibana-plugin-plugins-data-server.esqueryconfig.dateformattz.md) | string | | -| [ignoreFilterIfFieldNotInIndex](./kibana-plugin-plugins-data-server.esqueryconfig.ignorefilteriffieldnotinindex.md) | boolean | | -| [queryStringOptions](./kibana-plugin-plugins-data-server.esqueryconfig.querystringoptions.md) | Record<string, any> | | - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.querystringoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.querystringoptions.md deleted file mode 100644 index 437d36112d015..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esqueryconfig.querystringoptions.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) > [queryStringOptions](./kibana-plugin-plugins-data-server.esqueryconfig.querystringoptions.md) - -## EsQueryConfig.queryStringOptions property - -Signature: - -```typescript -queryStringOptions: Record; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.md index 519bbaf8f9416..f46ff36277d93 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.md @@ -4,12 +4,13 @@ ## Filter type +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> + Signature: ```typescript -export declare type Filter = { - $state?: FilterState; - meta: FilterMeta; - query?: any; -}; +declare type Filter = oldFilter; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.gettime.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.gettime.md index 54e7cf92f500c..7f2267aff7049 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.gettime.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.gettime.md @@ -10,7 +10,7 @@ export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: { forceNow?: Date; fieldName?: string; -}): import("../..").RangeFilter | undefined; +}): import("@kbn/es-query").RangeFilter | undefined; ``` ## Parameters @@ -23,5 +23,5 @@ export declare function getTime(indexPattern: IIndexPattern | undefined, timeRan Returns: -`import("../..").RangeFilter | undefined` +`import("@kbn/es-query").RangeFilter | undefined` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldsubtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldsubtype.md index 70140e51a7316..e8e872577b46b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldsubtype.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldsubtype.md @@ -2,18 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) -## IFieldSubType interface +## IFieldSubType type + +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> Signature: ```typescript -export interface IFieldSubType +declare type IFieldSubType = oldIFieldSubType; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [multi](./kibana-plugin-plugins-data-server.ifieldsubtype.multi.md) | {
parent: string;
} | | -| [nested](./kibana-plugin-plugins-data-server.ifieldsubtype.nested.md) | {
path: string;
} | | - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldsubtype.multi.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldsubtype.multi.md deleted file mode 100644 index 31a3bc53d6343..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldsubtype.multi.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) > [multi](./kibana-plugin-plugins-data-server.ifieldsubtype.multi.md) - -## IFieldSubType.multi property - -Signature: - -```typescript -multi?: { - parent: string; - }; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldsubtype.nested.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldsubtype.nested.md deleted file mode 100644 index b53a4406aedc2..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldsubtype.nested.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) > [nested](./kibana-plugin-plugins-data-server.ifieldsubtype.nested.md) - -## IFieldSubType.nested property - -Signature: - -```typescript -nested?: { - path: string; - }; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kuerynode.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kuerynode.md index 3a258a5b98616..a5c14ee8627b1 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kuerynode.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kuerynode.md @@ -2,17 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [KueryNode](./kibana-plugin-plugins-data-server.kuerynode.md) -## KueryNode interface +## KueryNode type + +> Warning: This API is now obsolete. +> +> Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. +> Signature: ```typescript -export interface KueryNode +declare type KueryNode = oldKueryNode; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [type](./kibana-plugin-plugins-data-server.kuerynode.type.md) | keyof NodeTypes | | - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kuerynode.type.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kuerynode.type.md deleted file mode 100644 index 192a2c05191c7..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kuerynode.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [KueryNode](./kibana-plugin-plugins-data-server.kuerynode.md) > [type](./kibana-plugin-plugins-data-server.kuerynode.type.md) - -## KueryNode.type property - -Signature: - -```typescript -type: keyof NodeTypes; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index ab14abdd74e87..8e23f47976bd9 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -48,11 +48,9 @@ | [AggParamOption](./kibana-plugin-plugins-data-server.aggparamoption.md) | | | [AsyncSearchResponse](./kibana-plugin-plugins-data-server.asyncsearchresponse.md) | | | [AsyncSearchStatusResponse](./kibana-plugin-plugins-data-server.asyncsearchstatusresponse.md) | | -| [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) | | | [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-server.fieldformatconfig.md) | | | [IEsSearchRequest](./kibana-plugin-plugins-data-server.iessearchrequest.md) | | -| [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) | | | [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) | | | [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) | Interface for an index pattern saved object | | [IScopedSearchClient](./kibana-plugin-plugins-data-server.iscopedsearchclient.md) | | @@ -61,7 +59,6 @@ | [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) | | | [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) | | | [ISearchStrategy](./kibana-plugin-plugins-data-server.isearchstrategy.md) | Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response. | -| [KueryNode](./kibana-plugin-plugins-data-server.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-server.optionedvalueprop.md) | | | [PluginSetup](./kibana-plugin-plugins-data-server.pluginsetup.md) | | | [PluginStart](./kibana-plugin-plugins-data-server.pluginstart.md) | | @@ -97,6 +94,7 @@ | [AggGroupName](./kibana-plugin-plugins-data-server.agggroupname.md) | | | [AggParam](./kibana-plugin-plugins-data-server.aggparam.md) | | | [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md) | | +| [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) | | | [ExecutionContextSearch](./kibana-plugin-plugins-data-server.executioncontextsearch.md) | | | [ExpressionFunctionKibana](./kibana-plugin-plugins-data-server.expressionfunctionkibana.md) | | | [ExpressionFunctionKibanaContext](./kibana-plugin-plugins-data-server.expressionfunctionkibanacontext.md) | | @@ -108,11 +106,12 @@ | [IEsSearchResponse](./kibana-plugin-plugins-data-server.iessearchresponse.md) | | | [IFieldFormatsRegistry](./kibana-plugin-plugins-data-server.ifieldformatsregistry.md) | | | [IFieldParamType](./kibana-plugin-plugins-data-server.ifieldparamtype.md) | | +| [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) | | | [IMetricAggType](./kibana-plugin-plugins-data-server.imetricaggtype.md) | | | [IndexPatternLoadExpressionFunctionDefinition](./kibana-plugin-plugins-data-server.indexpatternloadexpressionfunctiondefinition.md) | | | [KibanaContext](./kibana-plugin-plugins-data-server.kibanacontext.md) | | +| [KueryNode](./kibana-plugin-plugins-data-server.kuerynode.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | | -| [Query](./kibana-plugin-plugins-data-server.query.md) | | | [SearchRequestHandlerContext](./kibana-plugin-plugins-data-server.searchrequesthandlercontext.md) | | | [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.md deleted file mode 100644 index 6a7bdfe51f1c0..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [Query](./kibana-plugin-plugins-data-server.query.md) - -## Query type - -Signature: - -```typescript -export declare type Query = { - query: string | { - [key: string]: any; - }; - language: string; -}; -``` diff --git a/docs/index-extra-title-page.html b/docs/index-extra-title-page.html index 7bcbc9f075124..2621848ebea8a 100644 --- a/docs/index-extra-title-page.html +++ b/docs/index-extra-title-page.html @@ -1,37 +1,151 @@
-

From creating beautiful visualizations to managing the Elastic Stack, learn how Kibana helps you get the most of your data.

-

Watch our videos

- - - - - - - - - -

New to Kibana?

Popular topics

- - -
-

All topics

+

+ From creating beautiful visualizations to managing the Elastic Stack, learn how Kibana helps you + get the most of your data. +

+

+ How-to videos +

+ + + + + + + + + + + + + + + + + + +

New to Kibana?

Popular topics

+ + + +

Analyze your data

Manage all things Stack

+ + + +
+ +

All topics

diff --git a/docs/management/upgrade-assistant/index.asciidoc b/docs/management/upgrade-assistant/index.asciidoc index 209b55faf4f56..dbff211cef372 100644 --- a/docs/management/upgrade-assistant/index.asciidoc +++ b/docs/management/upgrade-assistant/index.asciidoc @@ -2,60 +2,22 @@ [[upgrade-assistant]] == Upgrade Assistant -The Upgrade Assistant helps you prepare for your upgrade to the next major {es} version. -For example, if you are using 6.8, the Upgrade Assistant helps you to upgrade to 7.0. -To access the assistant, open the main menu, then click *Stack Management > Upgrade Assistant*. +The Upgrade Assistant helps you prepare for your upgrade +to the next major version of the Elastic Stack. +To access the assistant, open the main menu and go to *Stack Management > Upgrade Assistant*. -The assistant identifies the deprecated settings in your cluster and indices -and guides you through the process of resolving issues, including reindexing. +The assistant identifies deprecated settings in your configuration, +enables you to see if you are using deprecated features, +and guides you through the process of resolving issues. -Before you upgrade, make sure that you are using the latest released minor -version of {es} to see the most up-to-date deprecation issues. -For example, if you want to upgrade to 7.0, make sure that you are using 6.8. +If you have indices that were created prior to 7.0, +you can use the assistant to reindex them so they can be accessed from 8.0. -[float] +IMPORTANT: To see the most up-to-date deprecation information before +upgrading to 8.0, upgrade to the latest 7.n release. + +[discrete] === Required permissions The `manage` cluster privilege is required to access the *Upgrade assistant*. -Additional privileges may be needed to perform certain actions. - -To add the privilege, open the main menu, then click *Stack Management > Roles*. - -[float] -=== Reindexing - -The *Indices* page lists the indices that are incompatible with the next -major version of {es}. You can initiate a reindex to resolve the issues. - -[role="screenshot"] -image::images/management-upgrade-assistant-9.0.png[] - -For a preview of how the data will change during the reindex, select the -index name. A warning appears if the index requires destructive changes. -Back up your index, then proceed with the reindex by accepting each breaking change. - -You can follow the progress as the Upgrade Assistant makes the index read-only, -creates a new index, reindexes the documents, and creates an alias that points -from the old index to the new one. - -If the reindexing fails or is cancelled, the changes are rolled back, the -new index is deleted, and the original index becomes writable. An error -message explains the reason for the failure. - -You can reindex multiple indices at a time, but keep an eye on the -{es} metrics, including CPU usage, memory pressure, and disk usage. If a -metric is so high it affects query performance, cancel the reindex and -continue by reindexing fewer indices at a time. - -Additional considerations: - -* If you use {alert-features}, when you reindex the internal indices -(`.watches`), the {watcher} process pauses and no alerts are triggered. - -* If you use {ml-features}, when you reindex the internal indices (`.ml-state`), -the {ml} jobs pause and models are not trained or updated. - -* If you use {security-features}, before you reindex the internal indices -(`.security*`), it is a good idea to create a temporary superuser account in the -`file` realm. For more information, see -{ref}/configuring-file-realm.html[Configuring a file realm]. +Additional privileges may be needed to perform certain actions. \ No newline at end of file diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index b49b669a00780..bcc00951de3c1 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -318,18 +318,26 @@ This content has moved. Refer to <> and <>. +[role="exclude",id="graph-getting-started"] +== Create a graph + +This content has moved. Refer to <>. + +[role="exclude",id="graph-limitations"] +== Graph limitations + +This content has moved. Refer to <>. + [role="exclude",id="profiler-getting-started"] == Getting start with Search Profiler This content has moved. Refer to <>. - [role="exclude",id="profiler-complicated"] == Profiling a more complicated querying This content has moved. Refer to <>. - [role="exclude",id="profiler-render"] == Rendering pre-captured profiler JSON diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 15abd0fa4ad96..a0611b79aae4c 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -782,13 +782,11 @@ out through *Advanced Settings*. *Default: `true`* | Set this value to true to allow Vega to use any URL to access external data sources and images. When false, Vega can only get data from {es}. *Default: `false`* -a| -`xpack.discoverEnhanced.actions.` +|[[settings-explore-data-in-context]] `xpack.discoverEnhanced.actions.` `exploreDataInContextMenu.enabled` | Enables the *Explore underlying data* option that allows you to open *Discover* from a dashboard panel and view the panel data. *Default: `false`* -a| -`xpack.discoverEnhanced.actions.` +|[[settings-explore-data-in-chart]] `xpack.discoverEnhanced.actions.` `exploreDataInChart.enabled` | Enables you to view the underlying documents in a data series from a dashboard panel. *Default: `false`* diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 0b4104ec1f31b..b53788b518fa0 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -125,7 +125,7 @@ image::images/rule-concepts-summary.svg[Rules, connectors, alerts and actions wo [[alerting-concepts-differences]] == Differences from Watcher -{kib} alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems. +{kib} alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems. This section will clarify some of the important differences in the function and intent of the two systems. diff --git a/docs/user/alerting/create-and-manage-rules.asciidoc b/docs/user/alerting/create-and-manage-rules.asciidoc index 2dd9a41205121..23e79911fd5ee 100644 --- a/docs/user/alerting/create-and-manage-rules.asciidoc +++ b/docs/user/alerting/create-and-manage-rules.asciidoc @@ -158,7 +158,7 @@ image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, A rule can have one of the following statuses: `active`:: The conditions for the rule have been met, and the associated actions should be invoked. -`ok`:: The conditions for the rule were previously met, but no longer. Changed to `recovered` in the 7.14 release. +`ok`:: The conditions for the rule have not been met, and the associated actions are not invoked. `error`:: An error was encountered during rule execution. `pending`:: The rule has not yet executed. The rule was either just created, or enabled after being disabled. `unknown`:: A problem occurred when calculating the status. Most likely, something went wrong with the alerting code. diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 516f4c66d47bb..c251ce7307968 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -265,33 +265,19 @@ Copy panels from one dashboard to another dashboard. [[explore-the-underlying-documents]] == Explore the underlying documents -To gain insight to the data, open the underlying panel or data series documents in *Discover*. The panel documents that you open in *Discover* have the same time range and filters as the source panel. +You can add additional interactions that allow you to open *Discover* from dashboard panels. To use the interactions, the panel must use only one index pattern. -[float] -[[explore-underlying-panel-documents]] -=== Explore the underlying panel documents - -When your visualization panel contains a single index pattern, you can open the panel documents in *Discover*. - -. Open the panel menu. - -. Click *Explore underlying data*. +Panel interaction:: Opens the data in *Discover* with the current dashboard filters, but does not take the filters +saved with the panel. + -[role="screenshot"] -image::images/explore_data_context_menu.png[Explore underlying data from panel context menu] +To enable panel interactions, refere to <>. -[float] -[[explore-underlying-data-series-documents]] -=== Explore the underlying data series documents - -To gain insight to a data series, open the documents in *Discover*. - -. Click the data series in the panel that you want to view. - -. Select *Explore underlying data*. +Series interaction:: +Opens the series data in *Discover* from inside the panel. + -[role="screenshot"] -image::images/explore_data_in_chart.png[Explore underlying data from chart] +To enable series interactions, refer to <>. + +NOTE: In {kib} 7.13 and earlier, the panel interaction was enabled by default. [float] [[download-csv]] diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc deleted file mode 100644 index 84c33db31d575..0000000000000 --- a/docs/user/dashboard/drilldowns.asciidoc +++ /dev/null @@ -1,252 +0,0 @@ -[role="xpack"] -[[drilldowns]] -== Create custom dashboard actions - -Custom dashboard actions, or _drilldowns_, allow you to create workflows for analyzing and troubleshooting your data. -Drilldowns apply only to the panel that you created the drilldown from, and are not shared across all panels. Each panel can have multiple drilldowns. - -Third-party developers can create drilldowns. To learn how to code drilldowns, refer to {kib-repo}blob/{branch}/x-pack/examples/ui_actions_enhanced_examples[this example plugin]. - -[float] -[[supported-drilldowns]] -=== Supported drilldowns - -{kib} supports dashboard and URL drilldowns. - -[float] -[[dashboard-drilldowns]] -==== Dashboard drilldowns - -Dashboard drilldowns enable you to open a dashboard from another dashboard, -taking the time range, filters, and other parameters with you -so the context remains the same. Dashboard drilldowns help you to continue your analysis from a new perspective. - -For example, if you have a dashboard that shows the overall status of multiple data center, -you can create a drilldown that navigates from the overall status dashboard to a dashboard -that shows a single data center or server. - -[role="screenshot"] -image:images/drilldown_on_piechart.gif[Drilldown on pie chart that navigates to another dashboard] - -[float] -[[url-drilldowns]] -==== URL drilldowns - -URL drilldowns enable you to navigate from a dashboard to internal or external URLs. -Destination URLs can be dynamic, depending on the dashboard context or user interaction with a panel. -For example, if you have a dashboard that shows data from a Github repository, you can create a URL drilldown -that opens Github from the dashboard panel. - -[role="screenshot"] -image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigates to Github] - -Some panels support multiple interactions, also known as triggers. -The <> you use to create a <> depends on the trigger you choose. URL drilldowns support these types of triggers: - -* *Single click* — A single data point in the panel. - -* *Range selection* — A range of values in a panel. - -For example, *Single click* has `{{event.value}}` and *Range selection* has `{{event.from}}` and `{{event.to}}`. - -[float] -[[dashboard-drilldown-supported-panels]] -=== Supported panel types - -The following panel types support drilldowns. - -[options="header"] -|=== - -| Panel | Dashboard drilldown | URL drilldown - -| Lens -^| X -^| X - -| Area -^| X -^| X - -| Controls -^| -^| - -| Data Table -^| X -^| X - -| Gauge -^| -^| - -| Goal -^| -^| - -| Heat map -^| X -^| X - -| Horizontal Bar -^| X -^| X - -| Line -^| X -^| X - -| Maps -^| X -^| X - -| Markdown -^| -^| - -| Metric -^| -^| - -| Pie -^| X -^| X - -| TSVB (only for time series visualizations) -^| X -^| X - -| Tag Cloud -^| X -^| X - -| Timelion -^| X -^| - -| Vega -^| X -^| - -| Vertical Bar -^| X -^| X - -|=== - -[float] -[[drilldowns-example]] -=== Create a dashboard drilldown - -To create dashboard drilldowns, you create or locate the dashboards you want to connect, then configure the drilldown that allows you to easily open one dashboard from the other dashboard. - -[float] -==== Create the dashboard - -. Add the *Sample web logs* data. - -. Create a new dashboard, then add the following panels from the *Visualize Library*: - -* *[Logs] Heatmap* -* *[Logs] Host, Visits, and Bytes Table* -* *[Logs] Total Requests and Bytes* -* *[Logs] Visitors by OS* -+ -If you don’t see the data on a panel, change the <>. - -. Save the dashboard. In the *Title* field, enter `Host Overview`. - -. Open the *[Logs] Web traffic* dashboard. - -. Set a search and filter. -+ -[%hardbreaks] -Search: `extension.keyword: ("gz" or "css" or "deb")` -Filter: `geo.src: CN` - -[float] -==== Create the drilldown - -. In the toolbar, click *Edit*. - -. Open the *[Logs] Visitors by OS* panel menu, then select *Create drilldown*. - -. Click *Go to dashboard*. - -.. Give the drilldown a name. For example, `My Drilldown`. - -.. From the *Choose a destination dashboard* dropdown, select *Host Overview*. - -.. To use the geo.src filter, KQL query, and time filter, select *Use filters and query from origin dashboard* and *Use date range from origin dashboard*. - -.. Click *Create drilldown*. - -. Save the dashboard. - -. In the *[Logs] Visitors by OS* panel, click *win 8*, then select `My Drilldown`. -+ -[role="screenshot"] -image::images/drilldown_on_panel.png[Drilldown on pie chart that navigates to another dashboard] - -. On the *Host Overview* dashboard, verify that the geo.src filter, KQL query, and time filter are applied. - -[float] -[[create-a-url-drilldown]] -=== Create a URL drilldown - -To create URL drilldowns, you add <> to a URL template, which configures the behavior of the drilldown. - -. Add the *Sample web logs* data. - -. Open the *[Logs] Web traffic* dashboard. - -. In the toolbar, click *Edit*. - -. Open the *[Logs] Visitors by OS* panel menu, then select *Create drilldown*. - -. Click *Go to URL*. - -.. Give the drilldown a name. For example, `Show on Github`. - -.. For the *Trigger*, select *Single click*. - -.. To navigate to the {kib} repository Github issues, enter the following in the *Enter URL* field: -+ -[source, bash] ----- -https://github.com/elastic/kibana/issues?q=is:issue+is:open+{{event.value}} ----- -+ -`{{event.value}}` is substituted with a value associated with a selected pie slice. - -.. Click *Create drilldown*. - -. Save the dashboard. - -. On the *[Logs] Visitors by OS* panel, click any chart slice, then select *Show on Github*. -+ -[role="screenshot"] -image:images/url_drilldown_popup.png[URL drilldown popup] - -. In the list of {kib} repository issues, verify that the slice value appears. -+ -[role="screenshot"] -image:images/url_drilldown_github.png[Github] - -[float] -[[manage-drilldowns]] -=== Manage drilldowns - -Make changes to your drilldowns, make a copy of your drilldowns for another panel, and delete drilldowns. - -. Open the panel menu that includes the drilldown, then click *Manage drilldowns*. - -. On the *Manage* tab, use the following options: - -* To change drilldowns, click *Edit* next to the drilldown you want to change, make your changes, then click *Save*. - -* To make a copy, click *Copy* next to the drilldown you want to change, enter the drilldown name, then click *Create drilldown*. - -* To delete a drilldown, select the drilldown you want to delete, then click *Delete*. - -include::url-drilldown.asciidoc[] diff --git a/docs/user/dashboard/images/drilldown_on_piechart.gif b/docs/user/dashboard/images/drilldown_on_piechart.gif deleted file mode 100644 index c438e14371887..0000000000000 Binary files a/docs/user/dashboard/images/drilldown_on_piechart.gif and /dev/null differ diff --git a/docs/user/dashboard/images/explore_data_context_menu.png b/docs/user/dashboard/images/explore_data_context_menu.png deleted file mode 100644 index 5742991030c89..0000000000000 Binary files a/docs/user/dashboard/images/explore_data_context_menu.png and /dev/null differ diff --git a/docs/user/dashboard/images/explore_data_in_chart.png b/docs/user/dashboard/images/explore_data_in_chart.png deleted file mode 100644 index 05d4f5fac9b2f..0000000000000 Binary files a/docs/user/dashboard/images/explore_data_in_chart.png and /dev/null differ diff --git a/docs/user/graph/configuring-graph.asciidoc b/docs/user/graph/configuring-graph.asciidoc index 4eb8939b004ba..968e08db33d49 100644 --- a/docs/user/graph/configuring-graph.asciidoc +++ b/docs/user/graph/configuring-graph.asciidoc @@ -55,7 +55,7 @@ is displayed. For more information on granting access to Kibana, see <>. [role="screenshot"] -image::user/graph/images/graph-read-only-badge.png[Example of Graph's read only access indicator in Kibana's header] +image::user/graph/images/graph-read-only-badge.png[Example of Graph's read only access indicator in Kibana's header, width=50%] [discrete] [[disable-drill-down]] diff --git a/docs/user/graph/images/graph-control-bar.png b/docs/user/graph/images/graph-control-bar.png new file mode 100644 index 0000000000000..6dcf0d693d4c2 Binary files /dev/null and b/docs/user/graph/images/graph-control-bar.png differ diff --git a/docs/user/graph/images/graph-link-summary.png b/docs/user/graph/images/graph-link-summary.png deleted file mode 100644 index a3dfdc0f79d96..0000000000000 Binary files a/docs/user/graph/images/graph-link-summary.png and /dev/null differ diff --git a/docs/user/graph/images/graph-menu.png b/docs/user/graph/images/graph-menu.png new file mode 100644 index 0000000000000..5a46bd595baf8 Binary files /dev/null and b/docs/user/graph/images/graph-menu.png differ diff --git a/docs/user/graph/images/graph-url-connections.png b/docs/user/graph/images/graph-url-connections.png index 34b57d489b048..18b1b0354ee51 100644 Binary files a/docs/user/graph/images/graph-url-connections.png and b/docs/user/graph/images/graph-url-connections.png differ diff --git a/docs/user/graph/index.asciidoc b/docs/user/graph/index.asciidoc index 40c75c868e237..5e7b689b8d8f1 100644 --- a/docs/user/graph/index.asciidoc +++ b/docs/user/graph/index.asciidoc @@ -5,7 +5,7 @@ [partintro] -- The {graph-features} enable you to discover how items in an -Elasticsearch index are related. You can explore the connections between +{es} index are related. You can explore the connections between indexed terms and see which connections are the most meaningful. This can be useful in a variety of applications, from fraud detection to recommendation engines. @@ -15,15 +15,15 @@ that hackers are targeting so you can harden your website. Or, you might provide graph-based personalized recommendations to your e-commerce customers. The {graph-features} provide a simple, yet powerful {ref}/graph-explore-api.html[graph exploration API], -and an interactive graph visualization tool for Kibana. Both work out of the -box with existing Elasticsearch indices--you don't need to store any +and an interactive graph visualization app for {kib}. Both work out of the +box with existing {es} indices—you don't need to store any additional data to use these features. [discrete] [[how-graph-works]] == How Graph works The graph API provides an alternative way to extract and summarize information -about the documents and terms in your Elasticsearch index. A _graph_ is really +about the documents and terms in your {es} index. A _graph_ is really just a network of related items. In our case, this means a network of related terms in the index. @@ -37,14 +37,14 @@ image::user/graph/images/graph-vertices-connections.jpg["Graph components"] NOTE: If you're into https://en.wikipedia.org/wiki/Graph_theory[graph theory], you might know vertices and connections as _nodes_ and _edges_. They're the same thing, we just want to use terminology that makes sense to people who -aren't graph geeks and avoid any confusion with the nodes in an Elasticsearch +aren't graph geeks and avoid any confusion with the nodes in an {es} cluster. The graph vertices are simply the terms that you've already indexed. The -connections are derived on the fly using Elasticsearch aggregations. To +connections are derived on the fly using {es} aggregations. To identify the most _meaningful_ connections, the graph API leverages -Elasticsearch relevance scoring. The same data structures and relevance ranking -tools built into Elasticsearch to support text searches enable the graph API to +{es} relevance scoring. The same data structures and relevance ranking +tools built into {es} to support text searches enable the graph API to separate useful signals from the noise that is typical of most connected data. This foundation lets you easily answer questions like: @@ -55,21 +55,143 @@ be interested in? * Which people on Stack Overflow have expertise in both Hadoop-related technologies and Python-related tech? -But what about performance, you ask? The Elasticsearch aggregation framework +But what about performance? The {es} aggregation framework enables the graph API to quickly summarize millions of documents as a single super-connection. Instead of retrieving every banking transaction between accounts A and B, it derives a single connection that represents that relationship. And, of course, this summarization process works across -multi-node clusters and scales with your Elasticsearch deployment. +multi-node clusters and scales with your {es} deployment. Advanced options let you control how your data is sampled and summarized. You can also set timeouts to prevent graph queries from adversely affecting the cluster. --- -include::getting-started.asciidoc[] +[float] +[[graph-connection]] +== Create a graph + +Use *Graph* to reveal the relationships in your data. + +. Open the main menu, and then click *Graph*. ++ +If you're new to {kib}, and don't yet have any data, follow the link to add sample data. +This example uses the {kib} sample web logs data set. + +. Select the data source that you want to explore. ++ +{kib} graphs the relationships between the top fields. ++ +[role="screenshot"] +image::user/graph/images/graph-url-connections.png["URL connections"] + +. Add more fields, or click an existing field to edit, disable or deselect it. ++ +[role="screenshot"] +image::user/graph/images/graph-menu.png["menu for editing, disabling, or removing a field from the graph", width=75%] + + +. Enter a query to discover relationships between terms in the selected +fields. ++ +For example, +to generate a graph of the successful requests to a +particular location, search for the `geo.src` +field. The weight of the connection between two vertices indicates how strongly they +are related. + +. To view more information about a relationship, click any connection or vertex. ++ +[role="screenshot"] +image::user/graph/images/graph-control-bar.png["Graph toolbar", width=50%] + +. Use the graph toolbar to display additional connections: ++ +* To display additional vertices that connect to your graph, click the expand icon +image:user/graph/images/graph-expand-button.png[Expand Selection]. +* To display additional +connections between the displayed vertices, click the link icon +image:user/graph/images/graph-link-button.png[Add links to existing terms]. +* To explore a particular area of the +graph, select the vertices you are interested in, and then click expand or link. +* To step back through your changes to the graph, click undo +image:user/graph/images/graph-undo-button.png[Undo] and redo +image:user/graph/images/graph-redo-button.png[Redo]. + +. To view more relationships in your data, submit additional queries. + +. *Save* your graph. + +[float] +[[graph-customize]] +== Customize your graph + +Apply custom colors and icons to vertices, configure the number of vertices that +a search adds to the graph, block terms, and more. + +[float] +[[style-vertex-properties]] +==== Style vertex properties + +Each vertex has a color, icon, and label. To change +the color or icon of all vertices +of a certain field, click it's field, and then +select *Edit settings*. + +To change the color and label of selected vertices, +click the style icon image:user/graph/images/graph-style-button.png[Style] +in the control bar. + + +[float] +[[edit-graph-settings]] +==== Tune the noise level + +By default, *Graph* is configured to tune out noise in your data. +If this isn't a good fit for your data, open *Settings > Advanced settings*, +and then adjust the way *Graph* queries your data. You can tune the graph to show +only the results relevant to you and to improve performance. +For more information, see <>. + +You can configure the number of vertices that a search or +expand operation adds to the graph. +By default, only the five most relevant terms for any given field are added +at a time. This keeps the graph from overflowing. To increase this number, click +a field, select *Edit Settings*, and change *Terms per hop*. + +[float] +[[graph-block-terms]] +==== Block terms from the graph +Documents that match a blocked term are not allowed in the graph. +To block a term, select its vertex and click +the block icon +image:user/graph/images/graph-block-button.png[Block selection] +in the graph toolbar. +For a list of blocked terms, open *Settings > Blocked terms*. + +[float] +[[graph-drill-down]] +==== Drill down into raw documents +With drilldowns, you can display additional information about a +selected vertex in a new browser window. For example, you might +configure a drilldown URL to perform a web search for the selected vertex term. + +Use the drilldown icon image:user/graph/images/graph-info-icon.png[Drilldown selection] +in the graph toolbar to show the drilldown buttons for the selected vertices. +To configure drilldowns, go to *Settings > Drilldowns*. See also +<>. + +[float] +[[graph-run-layout]] +==== Run and pause the layout +Graph uses a "force layout", where vertices behave like magnets, +pushing off of one another. By default, when you add a new vertex to +the graph, all vertices begin moving. In some cases, the movement might +go on for some time. To freeze the current vertex position, +click the pause icon +image:user/graph/images/graph-pause-button.png[Block selection] +in the graph toolbar. + +-- include::configuring-graph.asciidoc[] include::troubleshooting.asciidoc[] - -include::limitations.asciidoc[] diff --git a/docs/user/graph/limitations.asciidoc b/docs/user/graph/limitations.asciidoc deleted file mode 100644 index e96910bd27b4c..0000000000000 --- a/docs/user/graph/limitations.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[role="xpack"] -[[graph-limitations]] -== Graph limitations -++++ -Limitations -++++ - -[discrete] -=== Limited support for multiple indices -The graph API can explore multiple indices, types, or aliases in a -single API request, but the assumption is that each "hop" it performs -is querying the same set of indices. Currently, it is not possible to -take a term found in a field from one index and use that value to explore -connections in _a different field_ held in another type or index. - -A good example of where this might be useful is if an IP address is -found in the `remote_host` field of an index called "weblogs20160101", -you might want to follow that up by looking for the same address in -the `ip_address` field of an index called "knownthreats". - -Supporting this behaviour would require extra mappings to indicate that -the weblogs' `remote_host` field contained values that had currency and -meaning in the `ip_address` field of the threats index. - -Since we do not currently support this translation, you would have to -perform multiple calls to take the values from the weblogs index -response and build them into a separate request to the threats index. diff --git a/docs/user/graph/troubleshooting.asciidoc b/docs/user/graph/troubleshooting.asciidoc index eaac105c57358..f73d9142ff7e2 100644 --- a/docs/user/graph/troubleshooting.asciidoc +++ b/docs/user/graph/troubleshooting.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[graph-troubleshooting]] -== Graph troubleshooting +== Graph troubleshooting and limitations ++++ -Troubleshoot +Troubleshooting and limitations ++++ [discrete] @@ -22,7 +22,7 @@ but they can miss details from individual documents. If you need to perform a detailed forensic analysis, you can adjust the following settings to ensure a graph exploration produces all of the relevant data: -* Increase the `sample_size` to a larger number of documents to analyse more +* Increase the `sample_size` to a larger number of documents to analyze more data on each shard. * Set the `use_significance` setting to `false` to retrieve terms regardless of any statistical correlation with the sample. @@ -53,7 +53,28 @@ large documents with your seed and guiding queries. so even increasing the frequency threshold by one can massively reduce the number of candidate terms whose background frequencies are checked. -Keep in mind that all of these options reduce the scope of information analysed +Keep in mind that all of these options reduce the scope of information analyzed and can increase the potential to miss what could be interesting details. However, the information that's lost tends to be associated with lower-quality documents with lower-frequency terms, which can be an acceptable trade-off. + +[discrete] +=== Limited support for multiple indices +The graph API can explore multiple indices, types, or aliases in a +single API request, but the assumption is that each "hop" it performs +is querying the same set of indices. Currently, it is not possible to +take a term found in a field from one index and use that value to explore +connections in _a different field_ held in another type or index. + +A good example of where this might be useful is if an IP address is +found in the `remote_host` field of an index called "weblogs20160101", +you might want to follow that up by looking for the same address in +the `ip_address` field of an index called "knownthreats". + +Supporting this behavior would require extra mappings to indicate that +the weblogs' `remote_host` field contained values that had currency and +meaning in the `ip_address` field of the threats index. + +Since we do not currently support this translation, you would have to +perform multiple calls to take the values from the weblogs index +response and build them into a separate request to the threats index. diff --git a/docs/user/images/app-navigation-search.png b/docs/user/images/app-navigation-search.png index 3b89eed44b28f..9a644909ceb88 100644 Binary files a/docs/user/images/app-navigation-search.png and b/docs/user/images/app-navigation-search.png differ diff --git a/docs/user/images/home-page.png b/docs/user/images/home-page.png deleted file mode 100755 index 9ca4b7f43f427..0000000000000 Binary files a/docs/user/images/home-page.png and /dev/null differ diff --git a/docs/user/images/kibana-main-menu.png b/docs/user/images/kibana-main-menu.png old mode 100755 new mode 100644 index 79e0a3dca8658..7ee18d9844c66 Binary files a/docs/user/images/kibana-main-menu.png and b/docs/user/images/kibana-main-menu.png differ diff --git a/docs/user/images/login-screen.png b/docs/user/images/login-screen.png deleted file mode 100755 index 7a97c952e1039..0000000000000 Binary files a/docs/user/images/login-screen.png and /dev/null differ diff --git a/docs/user/images/roles-and-privileges.png b/docs/user/images/roles-and-privileges.png deleted file mode 100755 index 28bff6d13c871..0000000000000 Binary files a/docs/user/images/roles-and-privileges.png and /dev/null differ diff --git a/docs/user/images/rules-and-connectors.png b/docs/user/images/rules-and-connectors.png index 5cda25b54536f..1a85eeb6c0bc2 100644 Binary files a/docs/user/images/rules-and-connectors.png and b/docs/user/images/rules-and-connectors.png differ diff --git a/docs/user/images/stack-management.png b/docs/user/images/stack-management.png new file mode 100644 index 0000000000000..a0600b53bd836 Binary files /dev/null and b/docs/user/images/stack-management.png differ diff --git a/docs/user/images/tags-search.png b/docs/user/images/tags-search.png old mode 100755 new mode 100644 index 67458200c50d1..5b0134f25282f Binary files a/docs/user/images/tags-search.png and b/docs/user/images/tags-search.png differ diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index 783fa2b1c521f..dc7b1ecc50f6e 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -59,7 +59,7 @@ and everything you need to visualize and analyze your data. To access all of {kib} features, use the main menu. Open this menu by clicking the -menu icon. To keep the main menu visible at all times, click *Dock navigation*. +menu icon. For a quick reference of all {kib} features, refer to <> [role="screenshot"] @@ -81,10 +81,6 @@ that it ran in, trace the transaction, and check the overall service availabilit * Designed for security analysts, {security-guide}/es-overview.html[*Elastic Security*] provides an overview of the events and alerts from your environment. Elastic Security helps you defend your organization from threats before damage and loss occur. -+ -[role="screenshot"] -image::siem/images/detections-ui.png[Detections view in Elastic Security] - [float] [[visualize-and-analyze]] @@ -165,7 +161,7 @@ guided processes for administering all things Elastic Stack, including data, indices, clusters, alerts, and security. [role="screenshot"] -image::images/intro-management.png[Index Management view in Stack Management] +image::images/stack-management.png[Index Management view in Stack Management] [float] ==== Manage your data, indices, and clusters @@ -429,7 +425,7 @@ the <>. |<> |Share your data -|<>, <> +|<>, <>, <> 2+|*Administer your Kibana instance* diff --git a/package.json b/package.json index 85343fd8023d0..f7856b9f92e74 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "@kbn/config": "link:bazel-bin/packages/kbn-config", "@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema", "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto", + "@kbn/es-query": "link:bazel-bin/packages/kbn-es-query", "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n", "@kbn/interpreter": "link:bazel-bin/packages/kbn-interpreter", "@kbn/io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils", @@ -139,6 +140,7 @@ "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl", "@kbn/monaco": "link:bazel-bin/packages/kbn-monaco", "@kbn/rule-data-utils": "link:bazel-bin/packages/kbn-rule-data-utils", + "@kbn/securitysolution-autocomplete": "link:bazel-bin/packages/kbn-securitysolution-autocomplete", "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils", "@kbn/securitysolution-hook-utils": "link:bazel-bin/packages/kbn-securitysolution-hook-utils", "@kbn/securitysolution-io-ts-alerting-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types", @@ -153,9 +155,9 @@ "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils", "@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools", "@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository", - "@kbn/typed-react-router-config": "link:bazel-bin/packages/kbn-typed-react-router-config", "@kbn/std": "link:bazel-bin/packages/kbn-std", "@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath", + "@kbn/typed-react-router-config": "link:bazel-bin/packages/kbn-typed-react-router-config", "@kbn/ui-framework": "link:bazel-bin/packages/kbn-ui-framework", "@kbn/ui-shared-deps": "link:bazel-bin/packages/kbn-ui-shared-deps", "@kbn/utility-types": "link:bazel-bin/packages/kbn-utility-types", @@ -321,7 +323,7 @@ "p-retry": "^4.2.0", "papaparse": "^5.2.0", "pdfmake": "^0.1.65", - "peggy": "^1.0.0", + "peggy": "^1.2.0", "pegjs": "0.10.0", "pluralize": "3.1.0", "pngjs": "^3.4.0", @@ -689,7 +691,7 @@ "copy-webpack-plugin": "^6.0.2", "cpy": "^8.1.1", "css-loader": "^3.4.2", - "css-minimizer-webpack-plugin": "^1.3.0", + "cssnano": "^4.1.11", "cypress": "^6.8.0", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", @@ -741,7 +743,11 @@ "grunt-peg": "^2.0.1", "gulp": "4.0.2", "gulp-babel": "^8.0.0", + "gulp-brotli": "^3.0.0", + "gulp-postcss": "^8.0.0", "gulp-sourcemaps": "2.6.5", + "gulp-terser": "^2.0.1", + "gulp-gzip": "^1.4.2", "gulp-zip": "^5.0.2", "has-ansi": "^3.0.0", "hdr-histogram-js": "^1.2.0", @@ -829,6 +835,7 @@ "tempy": "^0.3.0", "terminal-link": "^2.1.1", "terser-webpack-plugin": "^2.1.2", + "terser": "^5.7.1", "ts-loader": "^7.0.5", "ts-morph": "^9.1.0", "tsd": "^0.13.1", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 938afdc205a44..778a7c7a0f2d4 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -21,6 +21,7 @@ filegroup( "//packages/kbn-docs-utils:build", "//packages/kbn-es:build", "//packages/kbn-es-archiver:build", + "//packages/kbn-es-query:build", "//packages/kbn-eslint-import-resolver-kibana:build", "//packages/kbn-eslint-plugin-eslint:build", "//packages/kbn-expect:build", @@ -35,6 +36,7 @@ filegroup( "//packages/kbn-plugin-generator:build", "//packages/kbn-plugin-helpers:build", "//packages/kbn-rule-data-utils:build", + "//packages/kbn-securitysolution-autocomplete:build", "//packages/kbn-securitysolution-list-constants:build", "//packages/kbn-securitysolution-io-ts-types:build", "//packages/kbn-securitysolution-io-ts-alerting-types:build", diff --git a/packages/kbn-es-query/BUILD.bazel b/packages/kbn-es-query/BUILD.bazel new file mode 100644 index 0000000000000..9639a1057cac3 --- /dev/null +++ b/packages/kbn-es-query/BUILD.bazel @@ -0,0 +1,133 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@npm//peggy:index.bzl", "peggy") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-es-query" +PKG_REQUIRE_NAME = "@kbn/es-query" + +SOURCE_FILES = glob( + [ + "src/**/*", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__/**", + "**/__mocks__/**", + "**/__snapshots__/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-common-utils", + "//packages/kbn-config-schema", + "//packages/kbn-i18n", + "@npm//@elastic/elasticsearch", + "@npm//load-json-file", + "@npm//lodash", + "@npm//moment-timezone", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/moment-timezone", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +peggy( + name = "grammar", + data = [ + ":grammar/grammar.peggy" + ], + output_dir = True, + args = [ + "--allowed-start-rules", + "start,Literal", + "-o", + "$(@D)/index.js", + "./%s/grammar/grammar.peggy" % package_name() + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_config( + name = "tsconfig_browser", + src = "tsconfig.browser.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.browser.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_dir = "target_types", + declaration_map = True, + incremental = True, + out_dir = "target_node", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +ts_project( + name = "tsc_browser", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = False, + incremental = True, + out_dir = "target_web", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig_browser", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES + [":grammar"], + deps = DEPS + [":tsc", ":tsc_browser"], + 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-es-query/README.md b/packages/kbn-es-query/README.md new file mode 100644 index 0000000000000..644fc4d559eb6 --- /dev/null +++ b/packages/kbn-es-query/README.md @@ -0,0 +1,3 @@ +# @kbn/es-query + +Shared common (client and server sie) utilities shared across packages and plugins. \ No newline at end of file diff --git a/src/plugins/data/common/es_query/kuery/ast/kuery.peg b/packages/kbn-es-query/grammar/grammar.peggy similarity index 95% rename from src/plugins/data/common/es_query/kuery/ast/kuery.peg rename to packages/kbn-es-query/grammar/grammar.peggy index dbea96eaac5b2..8f5ef17340aa9 100644 --- a/src/plugins/data/common/es_query/kuery/ast/kuery.peg +++ b/packages/kbn-es-query/grammar/grammar.peggy @@ -1,6 +1,9 @@ -/** - * To generate the parsing module (kuery.js), run `grunt peg` - * To watch changes and generate on file change, run `grunt watch:peg` +/* + * Copyright 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. */ // Initialization block diff --git a/packages/kbn-es-query/jest.config.js b/packages/kbn-es-query/jest.config.js new file mode 100644 index 0000000000000..306e12f34f698 --- /dev/null +++ b/packages/kbn-es-query/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-es-query'], +}; diff --git a/packages/kbn-es-query/package.json b/packages/kbn-es-query/package.json new file mode 100644 index 0000000000000..335ef61b8b360 --- /dev/null +++ b/packages/kbn-es-query/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/es-query", + "browser": "./target_web/index.js", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true +} \ No newline at end of file diff --git a/src/plugins/data/common/es_query/__fixtures__/index_pattern_response.ts b/packages/kbn-es-query/src/__fixtures__/index_pattern_response.ts similarity index 100% rename from src/plugins/data/common/es_query/__fixtures__/index_pattern_response.ts rename to packages/kbn-es-query/src/__fixtures__/index_pattern_response.ts diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts b/packages/kbn-es-query/src/es_query/build_es_query.test.ts similarity index 95% rename from src/plugins/data/common/es_query/es_query/build_es_query.test.ts rename to packages/kbn-es-query/src/es_query/build_es_query.test.ts index fa9a2c85aaef5..b31269c4f8160 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts +++ b/packages/kbn-es-query/src/es_query/build_es_query.test.ts @@ -10,15 +10,16 @@ import { buildEsQuery } from './build_es_query'; import { fromKueryExpression, toElasticsearchQuery } from '../kuery'; import { luceneStringToDsl } from './lucene_string_to_dsl'; import { decorateQuery } from './decorate_query'; -import { IIndexPattern } from '../../index_patterns'; -import { MatchAllFilter } from '../filters'; -import { fields } from '../../index_patterns/mocks'; -import { Query } from '../../query/types'; +import { MatchAllFilter, Query } from '../filters'; +import { fields } from '../filters/stubs'; +import { IndexPatternBase } from './types'; + +jest.mock('../kuery/grammar'); describe('build query', () => { - const indexPattern: IIndexPattern = ({ + const indexPattern: IndexPatternBase = { fields, - } as unknown) as IIndexPattern; + }; describe('buildEsQuery', () => { it('should return the parameters of an Elasticsearch bool query', () => { diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.ts b/packages/kbn-es-query/src/es_query/build_es_query.ts similarity index 97% rename from src/plugins/data/common/es_query/es_query/build_es_query.ts rename to packages/kbn-es-query/src/es_query/build_es_query.ts index d7b3c630d1a6e..e8a494ec1b8e4 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.ts +++ b/packages/kbn-es-query/src/es_query/build_es_query.ts @@ -10,8 +10,7 @@ import { groupBy, has, isEqual } from 'lodash'; import { buildQueryFromKuery } from './from_kuery'; import { buildQueryFromFilters } from './from_filters'; import { buildQueryFromLucene } from './from_lucene'; -import { Filter } from '../filters'; -import { Query } from '../../query/types'; +import { Filter, Query } from '../filters'; import { IndexPatternBase } from './types'; export interface EsQueryConfig { diff --git a/src/plugins/data/common/es_query/es_query/decorate_query.test.ts b/packages/kbn-es-query/src/es_query/decorate_query.test.ts similarity index 100% rename from src/plugins/data/common/es_query/es_query/decorate_query.test.ts rename to packages/kbn-es-query/src/es_query/decorate_query.test.ts diff --git a/src/plugins/data/common/es_query/es_query/decorate_query.ts b/packages/kbn-es-query/src/es_query/decorate_query.ts similarity index 100% rename from src/plugins/data/common/es_query/es_query/decorate_query.ts rename to packages/kbn-es-query/src/es_query/decorate_query.ts diff --git a/src/plugins/data/common/es_query/es_query/es_query_dsl.ts b/packages/kbn-es-query/src/es_query/es_query_dsl.ts similarity index 100% rename from src/plugins/data/common/es_query/es_query/es_query_dsl.ts rename to packages/kbn-es-query/src/es_query/es_query_dsl.ts diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts b/packages/kbn-es-query/src/es_query/filter_matches_index.test.ts similarity index 92% rename from src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts rename to packages/kbn-es-query/src/es_query/filter_matches_index.test.ts index ad4d7ff8d78e2..bf4e1291ca438 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts +++ b/packages/kbn-es-query/src/es_query/filter_matches_index.test.ts @@ -8,12 +8,12 @@ import { Filter } from '../filters'; import { filterMatchesIndex } from './filter_matches_index'; -import { IIndexPattern } from '../../index_patterns'; +import { IndexPatternBase } from './types'; describe('filterMatchesIndex', () => { it('should return true if the filter has no meta', () => { const filter = {} as Filter; - const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; + const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IndexPatternBase; expect(filterMatchesIndex(filter, indexPattern)).toBe(true); }); @@ -26,35 +26,35 @@ describe('filterMatchesIndex', () => { it('should return true if the filter key matches a field name', () => { const filter = { meta: { index: 'foo', key: 'bar' } } as Filter; - const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; + const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IndexPatternBase; expect(filterMatchesIndex(filter, indexPattern)).toBe(true); }); it('should return true if custom filter for the same index is passed', () => { const filter = { meta: { index: 'foo', key: 'bar', type: 'custom' } } as Filter; - const indexPattern = { id: 'foo', fields: [{ name: 'bara' }] } as IIndexPattern; + const indexPattern = { id: 'foo', fields: [{ name: 'bara' }] } as IndexPatternBase; expect(filterMatchesIndex(filter, indexPattern)).toBe(true); }); it('should return false if custom filter for a different index is passed', () => { const filter = { meta: { index: 'foo', key: 'bar', type: 'custom' } } as Filter; - const indexPattern = { id: 'food', fields: [{ name: 'bara' }] } as IIndexPattern; + const indexPattern = { id: 'food', fields: [{ name: 'bara' }] } as IndexPatternBase; expect(filterMatchesIndex(filter, indexPattern)).toBe(false); }); it('should return false if the filter key does not match a field name', () => { const filter = { meta: { index: 'foo', key: 'baz' } } as Filter; - const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; + const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IndexPatternBase; expect(filterMatchesIndex(filter, indexPattern)).toBe(false); }); it('should return true if the filter has meta without a key', () => { const filter = { meta: { index: 'foo' } } as Filter; - const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; + const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IndexPatternBase; expect(filterMatchesIndex(filter, indexPattern)).toBe(true); }); diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/packages/kbn-es-query/src/es_query/filter_matches_index.ts similarity index 100% rename from src/plugins/data/common/es_query/es_query/filter_matches_index.ts rename to packages/kbn-es-query/src/es_query/filter_matches_index.ts diff --git a/src/plugins/data/common/es_query/es_query/from_filters.test.ts b/packages/kbn-es-query/src/es_query/from_filters.test.ts similarity index 96% rename from src/plugins/data/common/es_query/es_query/from_filters.test.ts rename to packages/kbn-es-query/src/es_query/from_filters.test.ts index 4a60db48c41b6..e3a56b5a9d63d 100644 --- a/src/plugins/data/common/es_query/es_query/from_filters.test.ts +++ b/packages/kbn-es-query/src/es_query/from_filters.test.ts @@ -7,14 +7,14 @@ */ import { buildQueryFromFilters } from './from_filters'; -import { IIndexPattern } from '../../index_patterns'; import { ExistsFilter, Filter, MatchAllFilter } from '../filters'; -import { fields } from '../../index_patterns/mocks'; +import { fields } from '../filters/stubs'; +import { IndexPatternBase } from './types'; describe('build query', () => { - const indexPattern: IIndexPattern = ({ + const indexPattern: IndexPatternBase = { fields, - } as unknown) as IIndexPattern; + }; describe('buildQueryFromFilters', () => { test('should return the parameters of an Elasticsearch bool query', () => { diff --git a/src/plugins/data/common/es_query/es_query/from_filters.ts b/packages/kbn-es-query/src/es_query/from_filters.ts similarity index 100% rename from src/plugins/data/common/es_query/es_query/from_filters.ts rename to packages/kbn-es-query/src/es_query/from_filters.ts diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.test.ts b/packages/kbn-es-query/src/es_query/from_kuery.test.ts similarity index 91% rename from src/plugins/data/common/es_query/es_query/from_kuery.test.ts rename to packages/kbn-es-query/src/es_query/from_kuery.test.ts index 920102566f8b3..2458013854393 100644 --- a/src/plugins/data/common/es_query/es_query/from_kuery.test.ts +++ b/packages/kbn-es-query/src/es_query/from_kuery.test.ts @@ -8,14 +8,16 @@ import { buildQueryFromKuery } from './from_kuery'; import { fromKueryExpression, toElasticsearchQuery } from '../kuery'; -import { IIndexPattern } from '../../index_patterns'; -import { fields } from '../../index_patterns/mocks'; -import { Query } from '../../query/types'; +import { fields } from '../filters/stubs'; +import { IndexPatternBase } from './types'; +import { Query } from '..'; + +jest.mock('../kuery/grammar'); describe('build query', () => { - const indexPattern: IIndexPattern = ({ + const indexPattern: IndexPatternBase = { fields, - } as unknown) as IIndexPattern; + }; describe('buildQueryFromKuery', () => { test('should return the parameters of an Elasticsearch bool query', () => { diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.ts b/packages/kbn-es-query/src/es_query/from_kuery.ts similarity index 96% rename from src/plugins/data/common/es_query/es_query/from_kuery.ts rename to packages/kbn-es-query/src/es_query/from_kuery.ts index 3eccfd8776113..efe8b26a81412 100644 --- a/src/plugins/data/common/es_query/es_query/from_kuery.ts +++ b/packages/kbn-es-query/src/es_query/from_kuery.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ +import { Query } from '../filters'; import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '../kuery'; import { IndexPatternBase } from './types'; -import { Query } from '../../query/types'; export function buildQueryFromKuery( indexPattern: IndexPatternBase | undefined, diff --git a/src/plugins/data/common/es_query/es_query/from_lucene.test.ts b/packages/kbn-es-query/src/es_query/from_lucene.test.ts similarity index 98% rename from src/plugins/data/common/es_query/es_query/from_lucene.test.ts rename to packages/kbn-es-query/src/es_query/from_lucene.test.ts index 2438a4b40e256..e4ca435ae8862 100644 --- a/src/plugins/data/common/es_query/es_query/from_lucene.test.ts +++ b/packages/kbn-es-query/src/es_query/from_lucene.test.ts @@ -9,7 +9,7 @@ import { buildQueryFromLucene } from './from_lucene'; import { decorateQuery } from './decorate_query'; import { luceneStringToDsl } from './lucene_string_to_dsl'; -import { Query } from '../../query/types'; +import { Query } from '..'; describe('build query', () => { describe('buildQueryFromLucene', () => { diff --git a/src/plugins/data/common/es_query/es_query/from_lucene.ts b/packages/kbn-es-query/src/es_query/from_lucene.ts similarity index 95% rename from src/plugins/data/common/es_query/es_query/from_lucene.ts rename to packages/kbn-es-query/src/es_query/from_lucene.ts index 6485281cc0fb3..cba789513c983 100644 --- a/src/plugins/data/common/es_query/es_query/from_lucene.ts +++ b/packages/kbn-es-query/src/es_query/from_lucene.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ +import { Query } from '..'; import { decorateQuery } from './decorate_query'; import { luceneStringToDsl } from './lucene_string_to_dsl'; -import { Query } from '../../query/types'; export function buildQueryFromLucene( queries: Query[], diff --git a/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts b/packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts similarity index 98% rename from src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts rename to packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts index 24852ebf33bda..9c7b6070c7ec0 100644 --- a/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts +++ b/packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts @@ -7,7 +7,7 @@ */ import { handleNestedFilter } from './handle_nested_filter'; -import { fields } from '../../index_patterns/mocks'; +import { fields } from '../filters/stubs'; import { buildPhraseFilter, buildQueryFilter } from '../filters'; import { IndexPatternBase } from './types'; diff --git a/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts b/packages/kbn-es-query/src/es_query/handle_nested_filter.ts similarity index 100% rename from src/plugins/data/common/es_query/es_query/handle_nested_filter.ts rename to packages/kbn-es-query/src/es_query/handle_nested_filter.ts diff --git a/src/plugins/data/common/es_query/es_query/index.ts b/packages/kbn-es-query/src/es_query/index.ts similarity index 92% rename from src/plugins/data/common/es_query/es_query/index.ts rename to packages/kbn-es-query/src/es_query/index.ts index ecc7c8ba5a9f5..beba50f50dd81 100644 --- a/src/plugins/data/common/es_query/es_query/index.ts +++ b/packages/kbn-es-query/src/es_query/index.ts @@ -10,5 +10,4 @@ export { buildEsQuery, EsQueryConfig } from './build_es_query'; export { buildQueryFromFilters } from './from_filters'; export { luceneStringToDsl } from './lucene_string_to_dsl'; export { decorateQuery } from './decorate_query'; -export { getEsQueryConfig } from './get_es_query_config'; export { IndexPatternBase, IndexPatternFieldBase, IFieldSubType } from './types'; diff --git a/src/plugins/data/common/es_query/es_query/lucene_string_to_dsl.test.ts b/packages/kbn-es-query/src/es_query/lucene_string_to_dsl.test.ts similarity index 100% rename from src/plugins/data/common/es_query/es_query/lucene_string_to_dsl.test.ts rename to packages/kbn-es-query/src/es_query/lucene_string_to_dsl.test.ts diff --git a/src/plugins/data/common/es_query/es_query/lucene_string_to_dsl.ts b/packages/kbn-es-query/src/es_query/lucene_string_to_dsl.ts similarity index 100% rename from src/plugins/data/common/es_query/es_query/lucene_string_to_dsl.ts rename to packages/kbn-es-query/src/es_query/lucene_string_to_dsl.ts diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts b/packages/kbn-es-query/src/es_query/migrate_filter.test.ts similarity index 100% rename from src/plugins/data/common/es_query/es_query/migrate_filter.test.ts rename to packages/kbn-es-query/src/es_query/migrate_filter.test.ts diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.ts b/packages/kbn-es-query/src/es_query/migrate_filter.ts similarity index 100% rename from src/plugins/data/common/es_query/es_query/migrate_filter.ts rename to packages/kbn-es-query/src/es_query/migrate_filter.ts diff --git a/src/plugins/data/common/es_query/es_query/types.ts b/packages/kbn-es-query/src/es_query/types.ts similarity index 98% rename from src/plugins/data/common/es_query/es_query/types.ts rename to packages/kbn-es-query/src/es_query/types.ts index 9282072cd444d..ca6a542779053 100644 --- a/src/plugins/data/common/es_query/es_query/types.ts +++ b/packages/kbn-es-query/src/es_query/types.ts @@ -34,4 +34,5 @@ export interface IndexPatternFieldBase { export interface IndexPatternBase { fields: IndexPatternFieldBase[]; id?: string; + title?: string; } diff --git a/src/plugins/data/common/es_query/filters/build_filter.test.ts b/packages/kbn-es-query/src/filters/build_filter.test.ts similarity index 94% rename from src/plugins/data/common/es_query/filters/build_filter.test.ts rename to packages/kbn-es-query/src/filters/build_filter.test.ts index 33221e3838d9e..d7cf7938b3737 100644 --- a/src/plugins/data/common/es_query/filters/build_filter.test.ts +++ b/packages/kbn-es-query/src/filters/build_filter.test.ts @@ -7,9 +7,15 @@ */ import { buildFilter, FilterStateStore, FILTERS } from '.'; -import { stubIndexPattern, stubFields } from '../../../common/stubs'; +import { IndexPatternBase } from '..'; +import { fields as stubFields } from './stubs'; describe('buildFilter', () => { + const stubIndexPattern: IndexPatternBase = { + id: 'logstash-*', + fields: stubFields, + }; + it('should build phrase filters', () => { const params = 'foo'; const alias = 'bar'; diff --git a/src/plugins/data/common/es_query/filters/build_filters.ts b/packages/kbn-es-query/src/filters/build_filters.ts similarity index 94% rename from src/plugins/data/common/es_query/filters/build_filters.ts rename to packages/kbn-es-query/src/filters/build_filters.ts index 1d8d67b6e937f..ae27e64f3a41d 100644 --- a/src/plugins/data/common/es_query/filters/build_filters.ts +++ b/packages/kbn-es-query/src/filters/build_filters.ts @@ -6,18 +6,16 @@ * Side Public License, v 1. */ -import { IndexPatternFieldBase, IndexPatternBase } from '../..'; - import { Filter, FILTERS, - FilterStateStore, - FilterMeta, buildPhraseFilter, buildPhrasesFilter, buildRangeFilter, buildExistsFilter, } from '.'; +import { IndexPatternFieldBase, IndexPatternBase } from '..'; +import { FilterMeta, FilterStateStore } from './types'; export function buildFilter( indexPattern: IndexPatternBase, diff --git a/src/plugins/data/common/es_query/filters/custom_filter.ts b/packages/kbn-es-query/src/filters/custom_filter.ts similarity index 91% rename from src/plugins/data/common/es_query/filters/custom_filter.ts rename to packages/kbn-es-query/src/filters/custom_filter.ts index bab226157ddd1..aa9e798a5b1ac 100644 --- a/src/plugins/data/common/es_query/filters/custom_filter.ts +++ b/packages/kbn-es-query/src/filters/custom_filter.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Filter } from './meta_filter'; +import { Filter } from './types'; export type CustomFilter = Filter & { query: any; diff --git a/src/plugins/data/common/es_query/filters/exists_filter.test.ts b/packages/kbn-es-query/src/filters/exists_filter.test.ts similarity index 81% rename from src/plugins/data/common/es_query/filters/exists_filter.test.ts rename to packages/kbn-es-query/src/filters/exists_filter.test.ts index 848fcead5f5d9..83976d45f8e04 100644 --- a/src/plugins/data/common/es_query/filters/exists_filter.test.ts +++ b/packages/kbn-es-query/src/filters/exists_filter.test.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ +import { IndexPatternBase } from '..'; import { buildExistsFilter, getExistsFilterField } from './exists_filter'; -import { IIndexPattern } from '../../index_patterns'; -import { fields } from '../../index_patterns/fields/fields.mocks'; +import { fields } from './stubs/fields.mocks'; describe('exists filter', function () { - const indexPattern: IIndexPattern = ({ + const indexPattern: IndexPatternBase = { fields, - } as unknown) as IIndexPattern; + }; describe('getExistsFilterField', function () { it('should return the name of the field an exists query is targeting', () => { diff --git a/src/plugins/data/common/es_query/filters/exists_filter.ts b/packages/kbn-es-query/src/filters/exists_filter.ts similarity index 95% rename from src/plugins/data/common/es_query/filters/exists_filter.ts rename to packages/kbn-es-query/src/filters/exists_filter.ts index 7a09adb7d9ed6..7785a62261fde 100644 --- a/src/plugins/data/common/es_query/filters/exists_filter.ts +++ b/packages/kbn-es-query/src/filters/exists_filter.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { Filter, FilterMeta } from './meta_filter'; import { IndexPatternFieldBase, IndexPatternBase } from '..'; +import { Filter, FilterMeta } from './types'; export type ExistsFilterMeta = FilterMeta; diff --git a/src/plugins/data/common/es_query/filters/geo_bounding_box_filter.test.ts b/packages/kbn-es-query/src/filters/geo_bounding_box_filter.test.ts similarity index 100% rename from src/plugins/data/common/es_query/filters/geo_bounding_box_filter.test.ts rename to packages/kbn-es-query/src/filters/geo_bounding_box_filter.test.ts diff --git a/src/plugins/data/common/es_query/filters/geo_bounding_box_filter.ts b/packages/kbn-es-query/src/filters/geo_bounding_box_filter.ts similarity index 93% rename from src/plugins/data/common/es_query/filters/geo_bounding_box_filter.ts rename to packages/kbn-es-query/src/filters/geo_bounding_box_filter.ts index 987055405886c..b309515109d48 100644 --- a/src/plugins/data/common/es_query/filters/geo_bounding_box_filter.ts +++ b/packages/kbn-es-query/src/filters/geo_bounding_box_filter.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Filter, FilterMeta, LatLon } from './meta_filter'; +import { Filter, FilterMeta, LatLon } from './types'; export type GeoBoundingBoxFilterMeta = FilterMeta & { params: { diff --git a/src/plugins/data/common/es_query/filters/geo_polygon_filter.test.ts b/packages/kbn-es-query/src/filters/geo_polygon_filter.test.ts similarity index 100% rename from src/plugins/data/common/es_query/filters/geo_polygon_filter.test.ts rename to packages/kbn-es-query/src/filters/geo_polygon_filter.test.ts diff --git a/src/plugins/data/common/es_query/filters/geo_polygon_filter.ts b/packages/kbn-es-query/src/filters/geo_polygon_filter.ts similarity index 93% rename from src/plugins/data/common/es_query/filters/geo_polygon_filter.ts rename to packages/kbn-es-query/src/filters/geo_polygon_filter.ts index 5b284f1b6e3a1..42e417f2c88a4 100644 --- a/src/plugins/data/common/es_query/filters/geo_polygon_filter.ts +++ b/packages/kbn-es-query/src/filters/geo_polygon_filter.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Filter, FilterMeta, LatLon } from './meta_filter'; +import { Filter, FilterMeta, LatLon } from './types'; export type GeoPolygonFilterMeta = FilterMeta & { params: { diff --git a/src/plugins/data/common/es_query/filters/get_filter_field.test.ts b/packages/kbn-es-query/src/filters/get_filter_field.test.ts similarity index 87% rename from src/plugins/data/common/es_query/filters/get_filter_field.test.ts rename to packages/kbn-es-query/src/filters/get_filter_field.test.ts index b9ae8f3abaa0c..0425aa6cc8c72 100644 --- a/src/plugins/data/common/es_query/filters/get_filter_field.test.ts +++ b/packages/kbn-es-query/src/filters/get_filter_field.test.ts @@ -9,14 +9,14 @@ import { buildPhraseFilter } from './phrase_filter'; import { buildQueryFilter } from './query_string_filter'; import { getFilterField } from './get_filter_field'; -import { IIndexPattern } from '../../index_patterns'; -import { fields } from '../../index_patterns/fields/fields.mocks'; +import { IndexPatternBase } from '..'; +import { fields } from './stubs/fields.mocks'; describe('getFilterField', function () { - const indexPattern: IIndexPattern = ({ + const indexPattern: IndexPatternBase = { id: 'logstash-*', fields, - } as unknown) as IIndexPattern; + }; it('should return the field name from known filter types that target a specific field', () => { const field = indexPattern.fields.find((patternField) => patternField.name === 'extension'); diff --git a/src/plugins/data/common/es_query/filters/get_filter_field.ts b/packages/kbn-es-query/src/filters/get_filter_field.ts similarity index 97% rename from src/plugins/data/common/es_query/filters/get_filter_field.ts rename to packages/kbn-es-query/src/filters/get_filter_field.ts index 0782e46bac784..4b540beb03754 100644 --- a/src/plugins/data/common/es_query/filters/get_filter_field.ts +++ b/packages/kbn-es-query/src/filters/get_filter_field.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Filter } from './meta_filter'; +import { Filter } from './types'; import { getExistsFilterField, isExistsFilter } from './exists_filter'; import { getGeoBoundingBoxFilterField, isGeoBoundingBoxFilter } from './geo_bounding_box_filter'; import { getGeoPolygonFilterField, isGeoPolygonFilter } from './geo_polygon_filter'; diff --git a/src/plugins/data/common/es_query/filters/get_filter_params.test.ts b/packages/kbn-es-query/src/filters/get_filter_params.test.ts similarity index 100% rename from src/plugins/data/common/es_query/filters/get_filter_params.test.ts rename to packages/kbn-es-query/src/filters/get_filter_params.test.ts diff --git a/src/plugins/data/common/es_query/filters/get_filter_params.ts b/packages/kbn-es-query/src/filters/get_filter_params.ts similarity index 100% rename from src/plugins/data/common/es_query/filters/get_filter_params.ts rename to packages/kbn-es-query/src/filters/get_filter_params.ts diff --git a/src/plugins/data/common/es_query/filters/index.ts b/packages/kbn-es-query/src/filters/index.ts similarity index 89% rename from src/plugins/data/common/es_query/filters/index.ts rename to packages/kbn-es-query/src/filters/index.ts index fe7cdadabaee3..90c1675e8a3cf 100644 --- a/src/plugins/data/common/es_query/filters/index.ts +++ b/packages/kbn-es-query/src/filters/index.ts @@ -7,7 +7,7 @@ */ import { omit, get } from 'lodash'; -import { Filter } from './meta_filter'; +import { Filter } from './types'; export * from './build_filters'; export * from './custom_filter'; @@ -24,7 +24,7 @@ export * from './phrases_filter'; export * from './query_string_filter'; export * from './range_filter'; -export * from './types'; +export { Query, Filter, FILTERS, LatLon, FilterStateStore, FieldFilter, FilterMeta } from './types'; /** * Clean out any invalid attributes from the filters diff --git a/src/plugins/data/common/es_query/filters/match_all_filter.ts b/packages/kbn-es-query/src/filters/match_all_filter.ts similarity index 92% rename from src/plugins/data/common/es_query/filters/match_all_filter.ts rename to packages/kbn-es-query/src/filters/match_all_filter.ts index 36eb5ee1fce18..a3fdd740986d4 100644 --- a/src/plugins/data/common/es_query/filters/match_all_filter.ts +++ b/packages/kbn-es-query/src/filters/match_all_filter.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Filter, FilterMeta } from './meta_filter'; +import { Filter, FilterMeta } from './types'; export interface MatchAllFilterMeta extends FilterMeta { field: any; diff --git a/src/plugins/data/common/es_query/filters/meta_filter.ts b/packages/kbn-es-query/src/filters/meta_filter.ts similarity index 69% rename from src/plugins/data/common/es_query/filters/meta_filter.ts rename to packages/kbn-es-query/src/filters/meta_filter.ts index 87455cf1cb763..25fd26c410826 100644 --- a/src/plugins/data/common/es_query/filters/meta_filter.ts +++ b/packages/kbn-es-query/src/filters/meta_filter.ts @@ -6,49 +6,7 @@ * Side Public License, v 1. */ -export enum FilterStateStore { - APP_STATE = 'appState', - GLOBAL_STATE = 'globalState', -} - -// eslint-disable-next-line -export type FilterState = { - store: FilterStateStore; -}; - -type FilterFormatterFunction = (value: any) => string; -export interface FilterValueFormatter { - convert: FilterFormatterFunction; - getConverterFor: (type: string) => FilterFormatterFunction; -} - -// eslint-disable-next-line -export type FilterMeta = { - alias: string | null; - disabled: boolean; - negate: boolean; - // controlledBy is there to identify who owns the filter - controlledBy?: string; - // index and type are optional only because when you create a new filter, there are no defaults - index?: string; - isMultiIndex?: boolean; - type?: string; - key?: string; - params?: any; - value?: string; -}; - -// eslint-disable-next-line -export type Filter = { - $state?: FilterState; - meta: FilterMeta; - query?: any; -}; - -export interface LatLon { - lat: number; - lon: number; -} +import { Filter, FilterMeta, FilterState, FilterStateStore } from './types'; export const buildEmptyFilter = (isPinned: boolean, index?: string): Filter => { const meta: FilterMeta = { diff --git a/src/plugins/data/common/es_query/filters/missing_filter.test.ts b/packages/kbn-es-query/src/filters/missing_filter.test.ts similarity index 100% rename from src/plugins/data/common/es_query/filters/missing_filter.test.ts rename to packages/kbn-es-query/src/filters/missing_filter.test.ts diff --git a/src/plugins/data/common/es_query/filters/missing_filter.ts b/packages/kbn-es-query/src/filters/missing_filter.ts similarity index 93% rename from src/plugins/data/common/es_query/filters/missing_filter.ts rename to packages/kbn-es-query/src/filters/missing_filter.ts index d0d337283ca60..0e10cb18d3c21 100644 --- a/src/plugins/data/common/es_query/filters/missing_filter.ts +++ b/packages/kbn-es-query/src/filters/missing_filter.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Filter, FilterMeta } from './meta_filter'; +import { Filter, FilterMeta } from './types'; export type MissingFilterMeta = FilterMeta; diff --git a/src/plugins/data/common/es_query/filters/phrase_filter.test.ts b/packages/kbn-es-query/src/filters/phrase_filter.test.ts similarity index 85% rename from src/plugins/data/common/es_query/filters/phrase_filter.test.ts rename to packages/kbn-es-query/src/filters/phrase_filter.test.ts index 513f0e29b5b24..7e6ccadc800d4 100644 --- a/src/plugins/data/common/es_query/filters/phrase_filter.test.ts +++ b/packages/kbn-es-query/src/filters/phrase_filter.test.ts @@ -11,16 +11,17 @@ import { buildPhraseFilter, getPhraseFilterField, } from './phrase_filter'; -import { fields, getField } from '../../index_patterns/mocks'; -import { IIndexPattern } from '../../index_patterns'; +import { fields, getField } from '../filters/stubs'; +import { IndexPatternBase } from '../es_query'; describe('Phrase filter builder', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { indexPattern = { id: 'id', - } as IIndexPattern; + fields, + }; }); it('should be a function', () => { @@ -30,7 +31,7 @@ describe('Phrase filter builder', () => { it('should return a match query filter when passed a standard string field', () => { const field = getField('extension'); - expect(buildPhraseFilter(field, 'jpg', indexPattern)).toEqual({ + expect(buildPhraseFilter(field!, 'jpg', indexPattern)).toEqual({ meta: { index: 'id', }, @@ -45,7 +46,7 @@ describe('Phrase filter builder', () => { it('should return a match query filter when passed a standard numeric field', () => { const field = getField('bytes'); - expect(buildPhraseFilter(field, '5', indexPattern)).toEqual({ + expect(buildPhraseFilter(field!, '5', indexPattern)).toEqual({ meta: { index: 'id', }, @@ -60,7 +61,7 @@ describe('Phrase filter builder', () => { it('should return a match query filter when passed a standard bool field', () => { const field = getField('ssl'); - expect(buildPhraseFilter(field, 'true', indexPattern)).toEqual({ + expect(buildPhraseFilter(field!, 'true', indexPattern)).toEqual({ meta: { index: 'id', }, @@ -75,7 +76,7 @@ describe('Phrase filter builder', () => { it('should return a script filter when passed a scripted field', () => { const field = getField('script number'); - expect(buildPhraseFilter(field, 5, indexPattern)).toEqual({ + expect(buildPhraseFilter(field!, 5, indexPattern)).toEqual({ meta: { index: 'id', field: 'script number', @@ -95,7 +96,7 @@ describe('Phrase filter builder', () => { it('should return a script filter when passed a scripted field with numeric conversion', () => { const field = getField('script number'); - expect(buildPhraseFilter(field, '5', indexPattern)).toEqual({ + expect(buildPhraseFilter(field!, '5', indexPattern)).toEqual({ meta: { index: 'id', field: 'script number', @@ -140,9 +141,9 @@ describe('buildInlineScriptForPhraseFilter', () => { }); describe('getPhraseFilterField', function () { - const indexPattern: IIndexPattern = ({ + const indexPattern: IndexPatternBase = { fields, - } as unknown) as IIndexPattern; + }; it('should return the name of the field a phrase query is targeting', () => { const field = indexPattern.fields.find((patternField) => patternField.name === 'extension'); diff --git a/src/plugins/data/common/es_query/filters/phrase_filter.ts b/packages/kbn-es-query/src/filters/phrase_filter.ts similarity index 98% rename from src/plugins/data/common/es_query/filters/phrase_filter.ts rename to packages/kbn-es-query/src/filters/phrase_filter.ts index 68ad16cb31d42..fd5ded010bf07 100644 --- a/src/plugins/data/common/es_query/filters/phrase_filter.ts +++ b/packages/kbn-es-query/src/filters/phrase_filter.ts @@ -7,7 +7,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; import { get, isPlainObject } from 'lodash'; -import { Filter, FilterMeta } from './meta_filter'; +import { Filter, FilterMeta } from './types'; import { IndexPatternFieldBase, IndexPatternBase } from '..'; export type PhraseFilterMeta = FilterMeta & { diff --git a/src/plugins/data/common/es_query/filters/phrases_filter.test.ts b/packages/kbn-es-query/src/filters/phrases_filter.test.ts similarity index 82% rename from src/plugins/data/common/es_query/filters/phrases_filter.test.ts rename to packages/kbn-es-query/src/filters/phrases_filter.test.ts index 68ef69ba76eeb..5e742187cdf05 100644 --- a/src/plugins/data/common/es_query/filters/phrases_filter.test.ts +++ b/packages/kbn-es-query/src/filters/phrases_filter.test.ts @@ -7,13 +7,13 @@ */ import { buildPhrasesFilter, getPhrasesFilterField } from './phrases_filter'; -import { IIndexPattern } from '../../index_patterns'; -import { fields } from '../../index_patterns/fields/fields.mocks'; +import { IndexPatternBase } from '..'; +import { fields } from './stubs/fields.mocks'; describe('phrases filter', function () { - const indexPattern: IIndexPattern = ({ + const indexPattern: IndexPatternBase = { fields, - } as unknown) as IIndexPattern; + }; describe('getPhrasesFilterField', function () { it('should return the name of the field a phrases query is targeting', () => { diff --git a/src/plugins/data/common/es_query/filters/phrases_filter.ts b/packages/kbn-es-query/src/filters/phrases_filter.ts similarity index 97% rename from src/plugins/data/common/es_query/filters/phrases_filter.ts rename to packages/kbn-es-query/src/filters/phrases_filter.ts index 7f7831e1c7978..451a0a46789fb 100644 --- a/src/plugins/data/common/es_query/filters/phrases_filter.ts +++ b/packages/kbn-es-query/src/filters/phrases_filter.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Filter, FilterMeta } from './meta_filter'; +import { Filter, FilterMeta } from './types'; import { getPhraseScript } from './phrase_filter'; import { FILTERS } from './index'; import { IndexPatternFieldBase, IndexPatternBase } from '../es_query'; diff --git a/src/plugins/data/common/es_query/filters/query_string_filter.test.ts b/packages/kbn-es-query/src/filters/query_string_filter.test.ts similarity index 100% rename from src/plugins/data/common/es_query/filters/query_string_filter.test.ts rename to packages/kbn-es-query/src/filters/query_string_filter.test.ts diff --git a/src/plugins/data/common/es_query/filters/query_string_filter.ts b/packages/kbn-es-query/src/filters/query_string_filter.ts similarity index 94% rename from src/plugins/data/common/es_query/filters/query_string_filter.ts rename to packages/kbn-es-query/src/filters/query_string_filter.ts index bf960f0ac04b3..b4774d5e65a67 100644 --- a/src/plugins/data/common/es_query/filters/query_string_filter.ts +++ b/packages/kbn-es-query/src/filters/query_string_filter.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Filter, FilterMeta } from './meta_filter'; +import { Filter, FilterMeta } from './types'; export type QueryStringFilterMeta = FilterMeta; diff --git a/src/plugins/data/common/es_query/filters/range_filter.test.ts b/packages/kbn-es-query/src/filters/range_filter.test.ts similarity index 88% rename from src/plugins/data/common/es_query/filters/range_filter.test.ts rename to packages/kbn-es-query/src/filters/range_filter.test.ts index 30e52b21d52b7..634471b1d4d9e 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.test.ts +++ b/packages/kbn-es-query/src/filters/range_filter.test.ts @@ -8,7 +8,7 @@ import { each } from 'lodash'; import { buildRangeFilter, getRangeFilterField, RangeFilter } from './range_filter'; -import { fields, getField } from '../../index_patterns/mocks'; +import { fields, getField } from '../filters/stubs'; import { IndexPatternBase, IndexPatternFieldBase } from '../es_query'; describe('Range filter builder', () => { @@ -27,7 +27,7 @@ describe('Range filter builder', () => { it('should return a range filter when passed a standard field', () => { const field = getField('bytes'); - expect(buildRangeFilter(field, { gte: 1, lte: 3 }, indexPattern)).toEqual({ + expect(buildRangeFilter(field!, { gte: 1, lte: 3 }, indexPattern)).toEqual({ meta: { index: 'id', params: {}, @@ -44,7 +44,7 @@ describe('Range filter builder', () => { it('should return a script filter when passed a scripted field', () => { const field = getField('script number'); - expect(buildRangeFilter(field, { gte: 1, lte: 3 }, indexPattern)).toEqual({ + expect(buildRangeFilter(field!, { gte: 1, lte: 3 }, indexPattern)).toEqual({ meta: { field: 'script number', index: 'id', @@ -67,7 +67,7 @@ describe('Range filter builder', () => { it('should convert strings to numbers if the field is scripted and type number', () => { const field = getField('script number'); - expect(buildRangeFilter(field, { gte: '1', lte: '3' }, indexPattern)).toEqual({ + expect(buildRangeFilter(field!, { gte: '1', lte: '3' }, indexPattern)).toEqual({ meta: { field: 'script number', index: 'id', @@ -95,7 +95,7 @@ describe('Range filter builder', () => { `gte(() -> { ${field!.script} }, params.gte) && ` + `lte(() -> { ${field!.script} }, params.lte)`; - const rangeFilter = buildRangeFilter(field, { gte: 1, lte: 3 }, indexPattern); + const rangeFilter = buildRangeFilter(field!, { gte: 1, lte: 3 }, indexPattern); expect(rangeFilter.script!.script.source).toBe(expected); }); @@ -104,11 +104,11 @@ describe('Range filter builder', () => { const field = getField('script number'); expect(() => { - buildRangeFilter(field, { gte: 1, gt: 3 }, indexPattern); + buildRangeFilter(field!, { gte: 1, gt: 3 }, indexPattern); }).toThrowError(); expect(() => { - buildRangeFilter(field, { lte: 1, lt: 3 }, indexPattern); + buildRangeFilter(field!, { lte: 1, lt: 3 }, indexPattern); }).toThrowError(); }); @@ -120,7 +120,7 @@ describe('Range filter builder', () => { [key]: 5, }; - const filter = buildRangeFilter(field, params, indexPattern); + const filter = buildRangeFilter(field!, params, indexPattern); const script = filter.script!.script; expect(script.source).toBe('(' + field!.script + ')' + operator + key); @@ -134,7 +134,7 @@ describe('Range filter builder', () => { let filter: RangeFilter; beforeEach(() => { - field = getField('script number'); + field = getField('script number')!; filter = buildRangeFilter(field, { gte: 0, lt: Infinity }, indexPattern); }); @@ -164,7 +164,7 @@ describe('Range filter builder', () => { let filter: RangeFilter; beforeEach(() => { - field = getField('script number'); + field = getField('script number')!; filter = buildRangeFilter(field, { gte: -Infinity, lt: Infinity }, indexPattern); }); diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/packages/kbn-es-query/src/filters/range_filter.ts similarity index 99% rename from src/plugins/data/common/es_query/filters/range_filter.ts rename to packages/kbn-es-query/src/filters/range_filter.ts index e44e23f64936e..2df313eb7dd76 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.ts +++ b/packages/kbn-es-query/src/filters/range_filter.ts @@ -7,7 +7,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; import { map, reduce, mapValues, get, keys, pickBy } from 'lodash'; -import { Filter, FilterMeta } from './meta_filter'; +import { Filter, FilterMeta } from './types'; import { IndexPatternBase, IndexPatternFieldBase } from '..'; const OPERANDS_IN_RANGE = 2; diff --git a/src/plugins/data/common/es_query/filters/stubs/exists_filter.ts b/packages/kbn-es-query/src/filters/stubs/exists_filter.ts similarity index 100% rename from src/plugins/data/common/es_query/filters/stubs/exists_filter.ts rename to packages/kbn-es-query/src/filters/stubs/exists_filter.ts diff --git a/packages/kbn-es-query/src/filters/stubs/fields.mocks.ts b/packages/kbn-es-query/src/filters/stubs/fields.mocks.ts new file mode 100644 index 0000000000000..2507293b87477 --- /dev/null +++ b/packages/kbn-es-query/src/filters/stubs/fields.mocks.ts @@ -0,0 +1,80 @@ +/* + * Copyright 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 { IndexPatternFieldBase } from '../..'; + +/** + * Base index pattern fields for testing + */ +export const fields: IndexPatternFieldBase[] = [ + { + name: 'bytes', + type: 'number', + scripted: false, + }, + { + name: 'ssl', + type: 'boolean', + scripted: false, + }, + { + name: '@timestamp', + type: 'date', + scripted: false, + }, + { + name: 'extension', + type: 'string', + scripted: false, + }, + { + name: 'machine.os', + type: 'string', + scripted: false, + }, + { + name: 'machine.os.raw', + type: 'string', + scripted: false, + }, + { + name: 'script number', + type: 'number', + scripted: true, + script: '1234', + lang: 'expression', + }, + { + name: 'script date', + type: 'date', + scripted: true, + script: '1234', + lang: 'painless', + }, + { + name: 'script string', + type: 'string', + scripted: true, + script: '1234', + lang: 'painless', + }, + { + name: 'nestedField.child', + type: 'string', + scripted: false, + subType: { nested: { path: 'nestedField' } }, + }, + { + name: 'nestedField.nestedChild.doublyNestedChild', + type: 'string', + scripted: false, + subType: { nested: { path: 'nestedField.nestedChild' } }, + }, +]; + +export const getField = (name: string) => fields.find((field) => field.name === name); diff --git a/packages/kbn-es-query/src/filters/stubs/index.ts b/packages/kbn-es-query/src/filters/stubs/index.ts new file mode 100644 index 0000000000000..e4d99b5082257 --- /dev/null +++ b/packages/kbn-es-query/src/filters/stubs/index.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 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 './exists_filter'; +export * from './phrase_filter'; +export * from './phrases_filter'; +export * from './range_filter'; +export * from './fields.mocks'; diff --git a/src/plugins/data/common/es_query/filters/stubs/phrase_filter.ts b/packages/kbn-es-query/src/filters/stubs/phrase_filter.ts similarity index 100% rename from src/plugins/data/common/es_query/filters/stubs/phrase_filter.ts rename to packages/kbn-es-query/src/filters/stubs/phrase_filter.ts diff --git a/src/plugins/data/common/es_query/filters/stubs/phrases_filter.ts b/packages/kbn-es-query/src/filters/stubs/phrases_filter.ts similarity index 100% rename from src/plugins/data/common/es_query/filters/stubs/phrases_filter.ts rename to packages/kbn-es-query/src/filters/stubs/phrases_filter.ts diff --git a/src/plugins/data/common/es_query/filters/stubs/range_filter.ts b/packages/kbn-es-query/src/filters/stubs/range_filter.ts similarity index 100% rename from src/plugins/data/common/es_query/filters/stubs/range_filter.ts rename to packages/kbn-es-query/src/filters/stubs/range_filter.ts diff --git a/src/plugins/data/common/es_query/filters/types.ts b/packages/kbn-es-query/src/filters/types.ts similarity index 59% rename from src/plugins/data/common/es_query/filters/types.ts rename to packages/kbn-es-query/src/filters/types.ts index a007189d81a03..13e4a941b9166 100644 --- a/src/plugins/data/common/es_query/filters/types.ts +++ b/packages/kbn-es-query/src/filters/types.ts @@ -40,3 +40,47 @@ export enum FILTERS { GEO_POLYGON = 'geo_polygon', SPATIAL_FILTER = 'spatial_filter', } + +export enum FilterStateStore { + APP_STATE = 'appState', + GLOBAL_STATE = 'globalState', +} + +// eslint-disable-next-line +export type FilterState = { + store: FilterStateStore; +}; + +// eslint-disable-next-line +export type FilterMeta = { + alias: string | null; + disabled: boolean; + negate: boolean; + // controlledBy is there to identify who owns the filter + controlledBy?: string; + // index and type are optional only because when you create a new filter, there are no defaults + index?: string; + isMultiIndex?: boolean; + type?: string; + key?: string; + params?: any; + value?: string; +}; + +// eslint-disable-next-line +export type Filter = { + $state?: FilterState; + meta: FilterMeta; + query?: any; // TODO: can we use the Query type her? +}; + +// eslint-disable-next-line +export type Query = { + query: string | { [key: string]: any }; + language: string; +}; + +export interface LatLon { + lat: number; + lon: number; +} diff --git a/packages/kbn-es-query/src/index.ts b/packages/kbn-es-query/src/index.ts new file mode 100644 index 0000000000000..bbba52871d4c8 --- /dev/null +++ b/packages/kbn-es-query/src/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './es_query'; +export * from './filters'; +export * from './kuery'; diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.test.ts b/packages/kbn-es-query/src/kuery/ast/ast.test.ts similarity index 98% rename from src/plugins/data/common/es_query/kuery/ast/ast.test.ts rename to packages/kbn-es-query/src/kuery/ast/ast.test.ts index f8d7dc02d38fc..459ace026796c 100644 --- a/src/plugins/data/common/es_query/kuery/ast/ast.test.ts +++ b/packages/kbn-es-query/src/kuery/ast/ast.test.ts @@ -8,17 +8,19 @@ import { fromKueryExpression, fromLiteralExpression, toElasticsearchQuery } from './ast'; import { nodeTypes } from '../node_types'; -import { fields } from '../../../index_patterns/mocks'; -import { IIndexPattern } from '../../../index_patterns'; +import { IndexPatternBase } from '../..'; import { KueryNode } from '../types'; +import { fields } from '../../filters/stubs'; + +jest.mock('../grammar'); describe('kuery AST API', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { - indexPattern = ({ + indexPattern = { fields, - } as unknown) as IIndexPattern; + }; }); describe('fromKueryExpression', () => { diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.ts b/packages/kbn-es-query/src/kuery/ast/ast.ts similarity index 96% rename from src/plugins/data/common/es_query/kuery/ast/ast.ts rename to packages/kbn-es-query/src/kuery/ast/ast.ts index 3e7b25897cab7..6f43098a752de 100644 --- a/src/plugins/data/common/es_query/kuery/ast/ast.ts +++ b/packages/kbn-es-query/src/kuery/ast/ast.ts @@ -11,8 +11,7 @@ import { nodeTypes } from '../node_types/index'; import { KQLSyntaxError } from '../kuery_syntax_error'; import { KueryNode, DslQuery, KueryParseOptions } from '../types'; -// @ts-ignore -import { parse as parseKuery } from './_generated_/kuery'; +import { parse as parseKuery } from '../grammar'; import { IndexPatternBase } from '../..'; const fromExpression = ( diff --git a/src/plugins/data/common/es_query/kuery/ast/index.ts b/packages/kbn-es-query/src/kuery/ast/index.ts similarity index 100% rename from src/plugins/data/common/es_query/kuery/ast/index.ts rename to packages/kbn-es-query/src/kuery/ast/index.ts diff --git a/src/plugins/data/common/es_query/kuery/functions/and.test.ts b/packages/kbn-es-query/src/kuery/functions/and.test.ts similarity index 88% rename from src/plugins/data/common/es_query/kuery/functions/and.test.ts rename to packages/kbn-es-query/src/kuery/functions/and.test.ts index fae7888d75536..1e6797485c964 100644 --- a/src/plugins/data/common/es_query/kuery/functions/and.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/and.test.ts @@ -7,24 +7,24 @@ */ import { nodeTypes } from '../node_types'; -import { fields } from '../../../index_patterns/mocks'; -import { IIndexPattern } from '../../../index_patterns'; +import { fields } from '../../filters/stubs'; import * as ast from '../ast'; - -// @ts-ignore import * as and from './and'; +import { IndexPatternBase } from '../../es_query'; + +jest.mock('../grammar'); const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); describe('kuery functions', () => { describe('and', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { - indexPattern = ({ + indexPattern = { fields, - } as unknown) as IIndexPattern; + }; }); describe('buildNodeParams', () => { diff --git a/src/plugins/data/common/es_query/kuery/functions/and.ts b/packages/kbn-es-query/src/kuery/functions/and.ts similarity index 93% rename from src/plugins/data/common/es_query/kuery/functions/and.ts rename to packages/kbn-es-query/src/kuery/functions/and.ts index ba7d5d1f6645b..239dd67e73d10 100644 --- a/src/plugins/data/common/es_query/kuery/functions/and.ts +++ b/packages/kbn-es-query/src/kuery/functions/and.ts @@ -7,7 +7,7 @@ */ import * as ast from '../ast'; -import { IndexPatternBase, KueryNode } from '../../..'; +import { IndexPatternBase, KueryNode } from '../..'; export function buildNodeParams(children: KueryNode[]) { return { diff --git a/src/plugins/data/common/es_query/kuery/functions/exists.test.ts b/packages/kbn-es-query/src/kuery/functions/exists.test.ts similarity index 92% rename from src/plugins/data/common/es_query/kuery/functions/exists.test.ts rename to packages/kbn-es-query/src/kuery/functions/exists.test.ts index 4036e7c4089f0..0941e478e816b 100644 --- a/src/plugins/data/common/es_query/kuery/functions/exists.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/exists.test.ts @@ -7,20 +7,22 @@ */ import { nodeTypes } from '../node_types'; -import { fields } from '../../../index_patterns/mocks'; -import { IIndexPattern } from '../../../index_patterns'; +import { fields } from '../../filters/stubs'; +import { IndexPatternBase } from '../..'; + +jest.mock('../grammar'); // @ts-ignore import * as exists from './exists'; describe('kuery functions', () => { describe('exists', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { - indexPattern = ({ + indexPattern = { fields, - } as unknown) as IIndexPattern; + }; }); describe('buildNodeParams', () => { diff --git a/src/plugins/data/common/es_query/kuery/functions/exists.ts b/packages/kbn-es-query/src/kuery/functions/exists.ts similarity index 93% rename from src/plugins/data/common/es_query/kuery/functions/exists.ts rename to packages/kbn-es-query/src/kuery/functions/exists.ts index 4df566d874d8b..0e05ade5181db 100644 --- a/src/plugins/data/common/es_query/kuery/functions/exists.ts +++ b/packages/kbn-es-query/src/kuery/functions/exists.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ +import { IndexPatternFieldBase, IndexPatternBase, KueryNode } from '../..'; import * as literal from '../node_types/literal'; -import { KueryNode, IndexPatternFieldBase, IndexPatternBase } from '../../..'; export function buildNodeParams(fieldName: string) { return { diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts b/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.test.ts similarity index 94% rename from src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts rename to packages/kbn-es-query/src/kuery/functions/geo_bounding_box.test.ts index 54c2383be785a..028c5e39bf5da 100644 --- a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.test.ts @@ -8,12 +8,13 @@ import { get } from 'lodash'; import { nodeTypes } from '../node_types'; -import { fields } from '../../../index_patterns/mocks'; -import { IIndexPattern } from '../../../index_patterns'; +import { fields } from '../../filters/stubs'; +import { IndexPatternBase } from '../..'; -// @ts-ignore import * as geoBoundingBox from './geo_bounding_box'; +jest.mock('../grammar'); + const params = { bottomRight: { lat: 50.73, @@ -27,12 +28,12 @@ const params = { describe('kuery functions', () => { describe('geoBoundingBox', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { - indexPattern = ({ + indexPattern = { fields, - } as unknown) as IIndexPattern; + }; }); describe('buildNodeParams', () => { diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts b/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.ts similarity index 96% rename from src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts rename to packages/kbn-es-query/src/kuery/functions/geo_bounding_box.ts index 79bef10b14f71..b1fd8680af604 100644 --- a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts +++ b/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.ts @@ -9,7 +9,7 @@ import _ from 'lodash'; import { nodeTypes } from '../node_types'; import * as ast from '../ast'; -import { IndexPatternBase, KueryNode, LatLon } from '../../..'; +import { IndexPatternBase, KueryNode, LatLon } from '../..'; export function buildNodeParams(fieldName: string, params: any) { params = _.pick(params, 'topLeft', 'bottomRight'); diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts b/packages/kbn-es-query/src/kuery/functions/geo_polygon.test.ts similarity index 94% rename from src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts rename to packages/kbn-es-query/src/kuery/functions/geo_polygon.test.ts index a106754a5f56f..f16ca378866f0 100644 --- a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/geo_polygon.test.ts @@ -7,12 +7,13 @@ */ import { nodeTypes } from '../node_types'; -import { fields } from '../../../index_patterns/mocks'; -import { IIndexPattern } from '../../../index_patterns'; +import { fields } from '../../filters/stubs'; +import { IndexPatternBase } from '../..'; -// @ts-ignore import * as geoPolygon from './geo_polygon'; +jest.mock('../grammar'); + const points = [ { lat: 69.77, @@ -30,12 +31,12 @@ const points = [ describe('kuery functions', () => { describe('geoPolygon', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { - indexPattern = ({ + indexPattern = { fields, - } as unknown) as IIndexPattern; + }; }); describe('buildNodeParams', () => { diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts b/packages/kbn-es-query/src/kuery/functions/geo_polygon.ts similarity index 96% rename from src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts rename to packages/kbn-es-query/src/kuery/functions/geo_polygon.ts index 2e3280138502a..d02ba84237c7f 100644 --- a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts +++ b/packages/kbn-es-query/src/kuery/functions/geo_polygon.ts @@ -8,7 +8,7 @@ import { nodeTypes } from '../node_types'; import * as ast from '../ast'; -import { IndexPatternBase, KueryNode, LatLon } from '../../..'; +import { IndexPatternBase, KueryNode, LatLon } from '../..'; import { LiteralTypeBuildNode } from '../node_types/types'; export function buildNodeParams(fieldName: string, points: LatLon[]) { diff --git a/src/plugins/data/common/es_query/kuery/functions/index.ts b/packages/kbn-es-query/src/kuery/functions/index.ts similarity index 100% rename from src/plugins/data/common/es_query/kuery/functions/index.ts rename to packages/kbn-es-query/src/kuery/functions/index.ts diff --git a/src/plugins/data/common/es_query/kuery/functions/is.test.ts b/packages/kbn-es-query/src/kuery/functions/is.test.ts similarity index 97% rename from src/plugins/data/common/es_query/kuery/functions/is.test.ts rename to packages/kbn-es-query/src/kuery/functions/is.test.ts index 55aac8189c1d8..292159e727a92 100644 --- a/src/plugins/data/common/es_query/kuery/functions/is.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/is.test.ts @@ -7,20 +7,21 @@ */ import { nodeTypes } from '../node_types'; -import { fields } from '../../../index_patterns/mocks'; +import { fields } from '../../filters/stubs'; -// @ts-ignore import * as is from './is'; -import { IIndexPattern } from '../../../index_patterns'; +import { IndexPatternBase } from '../..'; + +jest.mock('../grammar'); describe('kuery functions', () => { describe('is', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { - indexPattern = ({ + indexPattern = { fields, - } as unknown) as IIndexPattern; + }; }); describe('buildNodeParams', () => { diff --git a/src/plugins/data/common/es_query/kuery/functions/is.ts b/packages/kbn-es-query/src/kuery/functions/is.ts similarity index 99% rename from src/plugins/data/common/es_query/kuery/functions/is.ts rename to packages/kbn-es-query/src/kuery/functions/is.ts index 381913670c26a..c8d33921b084a 100644 --- a/src/plugins/data/common/es_query/kuery/functions/is.ts +++ b/packages/kbn-es-query/src/kuery/functions/is.ts @@ -11,7 +11,7 @@ import { getPhraseScript } from '../../filters'; import { getFields } from './utils/get_fields'; import { getTimeZoneFromSettings } from '../../utils'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; -import { IndexPatternBase, KueryNode, IndexPatternFieldBase } from '../../..'; +import { IndexPatternBase, KueryNode, IndexPatternFieldBase } from '../..'; import * as ast from '../ast'; diff --git a/src/plugins/data/common/es_query/kuery/functions/nested.test.ts b/packages/kbn-es-query/src/kuery/functions/nested.test.ts similarity index 90% rename from src/plugins/data/common/es_query/kuery/functions/nested.test.ts rename to packages/kbn-es-query/src/kuery/functions/nested.test.ts index 50460500c0877..47e515b9bd47f 100644 --- a/src/plugins/data/common/es_query/kuery/functions/nested.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/nested.test.ts @@ -7,24 +7,25 @@ */ import { nodeTypes } from '../node_types'; -import { fields } from '../../../index_patterns/mocks'; -import { IIndexPattern } from '../../../index_patterns'; +import { fields } from '../../filters/stubs'; +import { IndexPatternBase } from '../..'; import * as ast from '../ast'; -// @ts-ignore import * as nested from './nested'; +jest.mock('../grammar'); + const childNode = nodeTypes.function.buildNode('is', 'child', 'foo'); describe('kuery functions', () => { describe('nested', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { - indexPattern = ({ + indexPattern = { fields, - } as unknown) as IIndexPattern; + }; }); describe('buildNodeParams', () => { diff --git a/src/plugins/data/common/es_query/kuery/functions/nested.ts b/packages/kbn-es-query/src/kuery/functions/nested.ts similarity index 95% rename from src/plugins/data/common/es_query/kuery/functions/nested.ts rename to packages/kbn-es-query/src/kuery/functions/nested.ts index 46ceeaf3e5de6..248de1c40d62a 100644 --- a/src/plugins/data/common/es_query/kuery/functions/nested.ts +++ b/packages/kbn-es-query/src/kuery/functions/nested.ts @@ -8,7 +8,7 @@ import * as ast from '../ast'; import * as literal from '../node_types/literal'; -import { IndexPatternBase, KueryNode } from '../../..'; +import { IndexPatternBase, KueryNode } from '../..'; export function buildNodeParams(path: any, child: any) { const pathNode = diff --git a/src/plugins/data/common/es_query/kuery/functions/not.test.ts b/packages/kbn-es-query/src/kuery/functions/not.test.ts similarity index 87% rename from src/plugins/data/common/es_query/kuery/functions/not.test.ts rename to packages/kbn-es-query/src/kuery/functions/not.test.ts index ab83df581edd7..a44f3e9c5dda8 100644 --- a/src/plugins/data/common/es_query/kuery/functions/not.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/not.test.ts @@ -7,24 +7,24 @@ */ import { nodeTypes } from '../node_types'; -import { fields } from '../../../index_patterns/mocks'; -import { IIndexPattern } from '../../../index_patterns'; +import { fields } from '../../filters/stubs'; +import { IndexPatternBase } from '../..'; import * as ast from '../ast'; - -// @ts-ignore import * as not from './not'; +jest.mock('../grammar'); + const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg'); describe('kuery functions', () => { describe('not', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { - indexPattern = ({ + indexPattern = { fields, - } as unknown) as IIndexPattern; + }; }); describe('buildNodeParams', () => { diff --git a/src/plugins/data/common/es_query/kuery/functions/not.ts b/packages/kbn-es-query/src/kuery/functions/not.ts similarity index 93% rename from src/plugins/data/common/es_query/kuery/functions/not.ts rename to packages/kbn-es-query/src/kuery/functions/not.ts index f837cd261c814..96404ee800010 100644 --- a/src/plugins/data/common/es_query/kuery/functions/not.ts +++ b/packages/kbn-es-query/src/kuery/functions/not.ts @@ -7,7 +7,7 @@ */ import * as ast from '../ast'; -import { IndexPatternBase, KueryNode } from '../../..'; +import { IndexPatternBase, KueryNode } from '../..'; export function buildNodeParams(child: KueryNode) { return { diff --git a/src/plugins/data/common/es_query/kuery/functions/or.test.ts b/packages/kbn-es-query/src/kuery/functions/or.test.ts similarity index 90% rename from src/plugins/data/common/es_query/kuery/functions/or.test.ts rename to packages/kbn-es-query/src/kuery/functions/or.test.ts index 5e151098a5ef9..15faa7e1753d3 100644 --- a/src/plugins/data/common/es_query/kuery/functions/or.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/or.test.ts @@ -7,25 +7,25 @@ */ import { nodeTypes } from '../node_types'; -import { fields } from '../../../index_patterns/mocks'; -import { IIndexPattern } from '../../../index_patterns'; +import { fields } from '../../filters/stubs'; +import { IndexPatternBase } from '../..'; import * as ast from '../ast'; -// @ts-ignore import * as or from './or'; +jest.mock('../grammar'); const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); describe('kuery functions', () => { describe('or', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { - indexPattern = ({ + indexPattern = { fields, - } as unknown) as IIndexPattern; + }; }); describe('buildNodeParams', () => { diff --git a/src/plugins/data/common/es_query/kuery/functions/or.ts b/packages/kbn-es-query/src/kuery/functions/or.ts similarity index 94% rename from src/plugins/data/common/es_query/kuery/functions/or.ts rename to packages/kbn-es-query/src/kuery/functions/or.ts index 7365cc39595e6..b94814e1f7c05 100644 --- a/src/plugins/data/common/es_query/kuery/functions/or.ts +++ b/packages/kbn-es-query/src/kuery/functions/or.ts @@ -7,7 +7,7 @@ */ import * as ast from '../ast'; -import { IndexPatternBase, KueryNode } from '../../..'; +import { IndexPatternBase, KueryNode } from '../..'; export function buildNodeParams(children: KueryNode[]) { return { diff --git a/src/plugins/data/common/es_query/kuery/functions/range.test.ts b/packages/kbn-es-query/src/kuery/functions/range.test.ts similarity index 96% rename from src/plugins/data/common/es_query/kuery/functions/range.test.ts rename to packages/kbn-es-query/src/kuery/functions/range.test.ts index c4bc9c1372b27..fa1805e64887c 100644 --- a/src/plugins/data/common/es_query/kuery/functions/range.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/range.test.ts @@ -8,21 +8,21 @@ import { get } from 'lodash'; import { nodeTypes } from '../node_types'; -import { fields } from '../../../index_patterns/mocks'; -import { IIndexPattern } from '../../../index_patterns'; +import { fields } from '../../filters/stubs'; +import { IndexPatternBase } from '../..'; import { RangeFilterParams } from '../../filters'; -// @ts-ignore import * as range from './range'; +jest.mock('../grammar'); describe('kuery functions', () => { describe('range', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { - indexPattern = ({ + indexPattern = { fields, - } as unknown) as IIndexPattern; + }; }); describe('buildNodeParams', () => { diff --git a/src/plugins/data/common/es_query/kuery/functions/range.ts b/packages/kbn-es-query/src/kuery/functions/range.ts similarity index 98% rename from src/plugins/data/common/es_query/kuery/functions/range.ts rename to packages/kbn-es-query/src/kuery/functions/range.ts index b134434dc182b..e80a365441c43 100644 --- a/src/plugins/data/common/es_query/kuery/functions/range.ts +++ b/packages/kbn-es-query/src/kuery/functions/range.ts @@ -13,7 +13,7 @@ import { getRangeScript, RangeFilterParams } from '../../filters'; import { getFields } from './utils/get_fields'; import { getTimeZoneFromSettings } from '../../utils'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; -import { IndexPatternBase, KueryNode } from '../../..'; +import { IndexPatternBase, KueryNode } from '../..'; export function buildNodeParams(fieldName: string, params: RangeFilterParams) { const paramsToMap = _.pick(params, 'gt', 'lt', 'gte', 'lte', 'format'); diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts b/packages/kbn-es-query/src/kuery/functions/utils/get_fields.test.ts similarity index 97% rename from src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts rename to packages/kbn-es-query/src/kuery/functions/utils/get_fields.test.ts index 949f94d043553..4125b0a572566 100644 --- a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/utils/get_fields.test.ts @@ -7,13 +7,13 @@ */ import { IndexPatternBase } from '../../..'; -import { fields } from '../../../../index_patterns/mocks'; +import { fields } from '../../../filters/stubs'; import { nodeTypes } from '../../index'; - -// @ts-ignore import { getFields } from './get_fields'; +jest.mock('../../grammar'); + describe('getFields', () => { let indexPattern: IndexPatternBase; diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts b/packages/kbn-es-query/src/kuery/functions/utils/get_fields.ts similarity index 94% rename from src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts rename to packages/kbn-es-query/src/kuery/functions/utils/get_fields.ts index 7dac1262d5062..db3826d4ef8c4 100644 --- a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts +++ b/packages/kbn-es-query/src/kuery/functions/utils/get_fields.ts @@ -8,7 +8,7 @@ import * as literal from '../../node_types/literal'; import * as wildcard from '../../node_types/wildcard'; -import { KueryNode, IndexPatternBase } from '../../../..'; +import { KueryNode, IndexPatternBase } from '../../..'; import { LiteralTypeBuildNode } from '../../node_types/types'; export function getFields(node: KueryNode, indexPattern?: IndexPatternBase) { diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts b/packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.test.ts similarity index 92% rename from src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts rename to packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.test.ts index 639f5584ef592..dccfc5d1c463a 100644 --- a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.test.ts @@ -7,19 +7,19 @@ */ import { nodeTypes } from '../../node_types'; -import { fields } from '../../../../index_patterns/mocks'; -import { IIndexPattern } from '../../../../index_patterns'; - -// @ts-ignore +import { fields } from '../../../filters/stubs'; +import { IndexPatternBase } from '../../..'; import { getFullFieldNameNode } from './get_full_field_name_node'; +jest.mock('../../grammar'); + describe('getFullFieldNameNode', function () { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { - indexPattern = ({ + indexPattern = { fields, - } as unknown) as IIndexPattern; + }; }); test('should return unchanged name node if no nested path is passed in', () => { diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts b/packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.ts similarity index 99% rename from src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts rename to packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.ts index 2a31ebeee2fab..6b575fbdea8fb 100644 --- a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts +++ b/packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.ts @@ -7,7 +7,7 @@ */ import { getFields } from './get_fields'; -import { IndexPatternBase, IndexPatternFieldBase, KueryNode } from '../../../..'; +import { IndexPatternBase, IndexPatternFieldBase, KueryNode } from '../../..'; export function getFullFieldNameNode( rootNameNode: any, diff --git a/packages/kbn-es-query/src/kuery/grammar/__mocks__/grammar.js b/packages/kbn-es-query/src/kuery/grammar/__mocks__/grammar.js new file mode 100644 index 0000000000000..89c13bb2b05c2 --- /dev/null +++ b/packages/kbn-es-query/src/kuery/grammar/__mocks__/grammar.js @@ -0,0 +1,2219 @@ +/* + * Copyright 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. + */ + +// Generated by Peggy 1.2.0. +// +// https://peggyjs.org/ +/* eslint-disable */ + +"use strict"; + +function peg$subclass(child, parent) { + function C() { this.constructor = child; } + C.prototype = parent.prototype; + child.prototype = new C(); +} + +function peg$SyntaxError(message, expected, found, location) { + var self = Error.call(this, message); + if (Object.setPrototypeOf) { + Object.setPrototypeOf(self, peg$SyntaxError.prototype); + } + self.expected = expected; + self.found = found; + self.location = location; + self.name = "SyntaxError"; + return self; +} + +peg$subclass(peg$SyntaxError, Error); + +function peg$padEnd(str, targetLength, padString) { + padString = padString || " "; + if (str.length > targetLength) { return str; } + targetLength -= str.length; + padString += padString.repeat(targetLength); + return str + padString.slice(0, targetLength); +} + +peg$SyntaxError.prototype.format = function(sources) { + var str = "Error: " + this.message; + if (this.location) { + var src = null; + var k; + for (k = 0; k < sources.length; k++) { + if (sources[k].source === this.location.source) { + src = sources[k].text.split(/\r\n|\n|\r/g); + break; + } + } + var s = this.location.start; + var loc = this.location.source + ":" + s.line + ":" + s.column; + if (src) { + var e = this.location.end; + var filler = peg$padEnd("", s.line.toString().length); + var line = src[s.line - 1]; + var last = s.line === e.line ? e.column : line.length + 1; + str += "\n --> " + loc + "\n" + + filler + " |\n" + + s.line + " | " + line + "\n" + + filler + " | " + peg$padEnd("", s.column - 1) + + peg$padEnd("", last - s.column, "^"); + } else { + str += "\n at " + loc; + } + } + return str; +}; + +peg$SyntaxError.buildMessage = function(expected, found) { + var DESCRIBE_EXPECTATION_FNS = { + literal: function(expectation) { + return "\"" + literalEscape(expectation.text) + "\""; + }, + + class: function(expectation) { + var escapedParts = expectation.parts.map(function(part) { + return Array.isArray(part) + ? classEscape(part[0]) + "-" + classEscape(part[1]) + : classEscape(part); + }); + + return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]"; + }, + + any: function() { + return "any character"; + }, + + end: function() { + return "end of input"; + }, + + other: function(expectation) { + return expectation.description; + } + }; + + function hex(ch) { + return ch.charCodeAt(0).toString(16).toUpperCase(); + } + + function literalEscape(s) { + return s + .replace(/\\/g, "\\\\") + .replace(/"/g, "\\\"") + .replace(/\0/g, "\\0") + .replace(/\t/g, "\\t") + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); }) + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); }); + } + + function classEscape(s) { + return s + .replace(/\\/g, "\\\\") + .replace(/\]/g, "\\]") + .replace(/\^/g, "\\^") + .replace(/-/g, "\\-") + .replace(/\0/g, "\\0") + .replace(/\t/g, "\\t") + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); }) + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); }); + } + + function describeExpectation(expectation) { + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); + } + + function describeExpected(expected) { + var descriptions = expected.map(describeExpectation); + var i, j; + + descriptions.sort(); + + if (descriptions.length > 0) { + for (i = 1, j = 1; i < descriptions.length; i++) { + if (descriptions[i - 1] !== descriptions[i]) { + descriptions[j] = descriptions[i]; + j++; + } + } + descriptions.length = j; + } + + switch (descriptions.length) { + case 1: + return descriptions[0]; + + case 2: + return descriptions[0] + " or " + descriptions[1]; + + default: + return descriptions.slice(0, -1).join(", ") + + ", or " + + descriptions[descriptions.length - 1]; + } + } + + function describeFound(found) { + return found ? "\"" + literalEscape(found) + "\"" : "end of input"; + } + + return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; +}; + +function peg$parse(input, options) { + options = options !== undefined ? options : {}; + + var peg$FAILED = {}; + var peg$source = options.grammarSource; + + var peg$startRuleFunctions = { start: peg$parsestart, Literal: peg$parseLiteral }; + var peg$startRuleFunction = peg$parsestart; + + var peg$c0 = "("; + var peg$c1 = ")"; + var peg$c2 = ":"; + var peg$c3 = "{"; + var peg$c4 = "}"; + var peg$c5 = "or"; + var peg$c6 = "and"; + var peg$c7 = "not"; + var peg$c8 = "\""; + var peg$c9 = "\\"; + var peg$c10 = "*"; + var peg$c11 = "\\t"; + var peg$c12 = "\\r"; + var peg$c13 = "\\n"; + var peg$c14 = "u"; + var peg$c15 = "<="; + var peg$c16 = ">="; + var peg$c17 = "<"; + var peg$c18 = ">"; + var peg$c19 = "@kuery-cursor@"; + + var peg$r0 = /^[\\"]/; + var peg$r1 = /^[^"]/; + var peg$r2 = /^[\\():<>"*{}]/; + var peg$r3 = /^[0-9a-f]/i; + var peg$r4 = /^[ \t\r\n\xA0]/; + + var peg$e0 = peg$literalExpectation("(", false); + var peg$e1 = peg$literalExpectation(")", false); + var peg$e2 = peg$literalExpectation(":", false); + var peg$e3 = peg$literalExpectation("{", false); + var peg$e4 = peg$literalExpectation("}", false); + var peg$e5 = peg$otherExpectation("fieldName"); + var peg$e6 = peg$otherExpectation("value"); + var peg$e7 = peg$otherExpectation("OR"); + var peg$e8 = peg$literalExpectation("or", true); + var peg$e9 = peg$otherExpectation("AND"); + var peg$e10 = peg$literalExpectation("and", true); + var peg$e11 = peg$otherExpectation("NOT"); + var peg$e12 = peg$literalExpectation("not", true); + var peg$e13 = peg$otherExpectation("literal"); + var peg$e14 = peg$literalExpectation("\"", false); + var peg$e15 = peg$literalExpectation("\\", false); + var peg$e16 = peg$classExpectation(["\\", "\""], false, false); + var peg$e17 = peg$classExpectation(["\""], true, false); + var peg$e18 = peg$anyExpectation(); + var peg$e19 = peg$literalExpectation("*", false); + var peg$e20 = peg$literalExpectation("\\t", false); + var peg$e21 = peg$literalExpectation("\\r", false); + var peg$e22 = peg$literalExpectation("\\n", false); + var peg$e23 = peg$classExpectation(["\\", "(", ")", ":", "<", ">", "\"", "*", "{", "}"], false, false); + var peg$e24 = peg$literalExpectation("u", false); + var peg$e25 = peg$classExpectation([["0", "9"], ["a", "f"]], false, true); + var peg$e26 = peg$literalExpectation("<=", false); + var peg$e27 = peg$literalExpectation(">=", false); + var peg$e28 = peg$literalExpectation("<", false); + var peg$e29 = peg$literalExpectation(">", false); + var peg$e30 = peg$otherExpectation("whitespace"); + var peg$e31 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); + var peg$e32 = peg$literalExpectation("@kuery-cursor@", false); + + var peg$f0 = function(query, trailing) { + if (trailing.type === 'cursor') { + return { + ...trailing, + suggestionTypes: ['conjunction'] + }; + } + if (query !== null) return query; + return nodeTypes.function.buildNode('is', '*', '*'); + }; + var peg$f1 = function(head, query) { return query; }; + var peg$f2 = function(head, tail) { + const nodes = [head, ...tail]; + const cursor = parseCursor && nodes.find(node => node.type === 'cursor'); + if (cursor) return cursor; + return buildFunctionNode('or', nodes); + }; + var peg$f3 = function(head, tail) { + const nodes = [head, ...tail]; + const cursor = parseCursor && nodes.find(node => node.type === 'cursor'); + if (cursor) return cursor; + return buildFunctionNode('and', nodes); + }; + var peg$f4 = function(query) { + if (query.type === 'cursor') return query; + return buildFunctionNode('not', [query]); + }; + var peg$f5 = function(query, trailing) { + if (trailing.type === 'cursor') { + return { + ...trailing, + suggestionTypes: ['conjunction'] + }; + } + return query; + }; + var peg$f6 = function(field, query, trailing) { + if (query.type === 'cursor') { + return { + ...query, + nestedPath: query.nestedPath ? `${field.value}.${query.nestedPath}` : field.value, + } + }; + + if (trailing.type === 'cursor') { + return { + ...trailing, + suggestionTypes: ['conjunction'] + }; + } + return buildFunctionNode('nested', [field, query]); + }; + var peg$f7 = function(field, operator, value) { + if (value.type === 'cursor') { + return { + ...value, + suggestionTypes: ['conjunction'] + }; + } + const range = buildNamedArgNode(operator, value); + return buildFunctionNode('range', [field, range]); + }; + var peg$f8 = function(field, partial) { + if (partial.type === 'cursor') { + return { + ...partial, + fieldName: field.value, + suggestionTypes: ['value', 'conjunction'] + }; + } + return partial(field); + }; + var peg$f9 = function(partial) { + if (partial.type === 'cursor') { + const fieldName = `${partial.prefix}${partial.suffix}`.trim(); + return { + ...partial, + fieldName, + suggestionTypes: ['field', 'operator', 'conjunction'] + }; + } + const field = buildLiteralNode(null); + return partial(field); + }; + var peg$f10 = function(partial, trailing) { + if (trailing.type === 'cursor') { + return { + ...trailing, + suggestionTypes: ['conjunction'] + }; + } + return partial; + }; + var peg$f11 = function(head, partial) { return partial; }; + var peg$f12 = function(head, tail) { + const nodes = [head, ...tail]; + const cursor = parseCursor && nodes.find(node => node.type === 'cursor'); + if (cursor) { + return { + ...cursor, + suggestionTypes: ['value'] + }; + } + return (field) => buildFunctionNode('or', nodes.map(partial => partial(field))); + }; + var peg$f13 = function(head, tail) { + const nodes = [head, ...tail]; + const cursor = parseCursor && nodes.find(node => node.type === 'cursor'); + if (cursor) { + return { + ...cursor, + suggestionTypes: ['value'] + }; + } + return (field) => buildFunctionNode('and', nodes.map(partial => partial(field))); + }; + var peg$f14 = function(partial) { + if (partial.type === 'cursor') { + return { + ...list, + suggestionTypes: ['value'] + }; + } + return (field) => buildFunctionNode('not', [partial(field)]); + }; + var peg$f15 = function(value) { + if (value.type === 'cursor') return value; + const isPhrase = buildLiteralNode(true); + return (field) => buildFunctionNode('is', [field, value, isPhrase]); + }; + var peg$f16 = function(value) { + if (value.type === 'cursor') return value; + + if (!allowLeadingWildcards && value.type === 'wildcard' && nodeTypes.wildcard.hasLeadingWildcard(value)) { + error('Leading wildcards are disabled. See query:allowLeadingWildcards in Advanced Settings.'); + } + + const isPhrase = buildLiteralNode(false); + return (field) => buildFunctionNode('is', [field, value, isPhrase]); + }; + var peg$f17 = function() { return parseCursor; }; + var peg$f18 = function(prefix, cursor, suffix) { + const { start, end } = location(); + return { + type: 'cursor', + start: start.offset, + end: end.offset - cursor.length, + prefix: prefix.join(''), + suffix: suffix.join(''), + text: text().replace(cursor, '') + }; + }; + var peg$f19 = function(chars) { + return buildLiteralNode(chars.join('')); + }; + var peg$f20 = function(char) { return char; }; + var peg$f21 = function(chars) { + const sequence = chars.join('').trim(); + if (sequence === 'null') return buildLiteralNode(null); + if (sequence === 'true') return buildLiteralNode(true); + if (sequence === 'false') return buildLiteralNode(false); + if (chars.includes(wildcardSymbol)) return buildWildcardNode(sequence); + return buildLiteralNode(sequence); + }; + var peg$f22 = function() { return wildcardSymbol; }; + var peg$f23 = function() { return '\t'; }; + var peg$f24 = function() { return '\r'; }; + var peg$f25 = function() { return '\n'; }; + var peg$f26 = function(keyword) { return keyword; }; + var peg$f27 = function(sequence) { return sequence; }; + var peg$f28 = function(digits) { + return String.fromCharCode(parseInt(digits, 16)); + }; + var peg$f29 = function() { return 'lte'; }; + var peg$f30 = function() { return 'gte'; }; + var peg$f31 = function() { return 'lt'; }; + var peg$f32 = function() { return 'gt'; }; + var peg$f33 = function() { return cursorSymbol; }; + + var peg$currPos = 0; + var peg$savedPos = 0; + var peg$posDetailsCache = [{ line: 1, column: 1 }]; + var peg$maxFailPos = 0; + var peg$maxFailExpected = []; + var peg$silentFails = 0; + + var peg$result; + + if ("startRule" in options) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + + function text() { + return input.substring(peg$savedPos, peg$currPos); + } + + function offset() { + return peg$savedPos; + } + + function range() { + return { + source: peg$source, + start: peg$savedPos, + end: peg$currPos + }; + } + + function location() { + return peg$computeLocation(peg$savedPos, peg$currPos); + } + + function expected(description, location) { + location = location !== undefined + ? location + : peg$computeLocation(peg$savedPos, peg$currPos); + + throw peg$buildStructuredError( + [peg$otherExpectation(description)], + input.substring(peg$savedPos, peg$currPos), + location + ); + } + + function error(message, location) { + location = location !== undefined + ? location + : peg$computeLocation(peg$savedPos, peg$currPos); + + throw peg$buildSimpleError(message, location); + } + + function peg$literalExpectation(text, ignoreCase) { + return { type: "literal", text: text, ignoreCase: ignoreCase }; + } + + function peg$classExpectation(parts, inverted, ignoreCase) { + return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase }; + } + + function peg$anyExpectation() { + return { type: "any" }; + } + + function peg$endExpectation() { + return { type: "end" }; + } + + function peg$otherExpectation(description) { + return { type: "other", description: description }; + } + + function peg$computePosDetails(pos) { + var details = peg$posDetailsCache[pos]; + var p; + + if (details) { + return details; + } else { + p = pos - 1; + while (!peg$posDetailsCache[p]) { + p--; + } + + details = peg$posDetailsCache[p]; + details = { + line: details.line, + column: details.column + }; + + while (p < pos) { + if (input.charCodeAt(p) === 10) { + details.line++; + details.column = 1; + } else { + details.column++; + } + + p++; + } + + peg$posDetailsCache[pos] = details; + + return details; + } + } + + function peg$computeLocation(startPos, endPos) { + var startPosDetails = peg$computePosDetails(startPos); + var endPosDetails = peg$computePosDetails(endPos); + + return { + source: peg$source, + start: { + offset: startPos, + line: startPosDetails.line, + column: startPosDetails.column + }, + end: { + offset: endPos, + line: endPosDetails.line, + column: endPosDetails.column + } + }; + } + + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { return; } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$buildSimpleError(message, location) { + return new peg$SyntaxError(message, null, null, location); + } + + function peg$buildStructuredError(expected, found, location) { + return new peg$SyntaxError( + peg$SyntaxError.buildMessage(expected, found), + expected, + found, + location + ); + } + + function peg$parsestart() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = []; + s2 = peg$parseSpace(); + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$parseSpace(); + } + s2 = peg$parseOrQuery(); + if (s2 === peg$FAILED) { + s2 = null; + } + s3 = peg$parseOptionalSpace(); + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f0(s2, s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseOrQuery() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseAndQuery(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parseOr(); + if (s4 !== peg$FAILED) { + s5 = peg$parseAndQuery(); + if (s5 !== peg$FAILED) { + peg$savedPos = s3; + s3 = peg$f1(s1, s5); + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parseOr(); + if (s4 !== peg$FAILED) { + s5 = peg$parseAndQuery(); + if (s5 !== peg$FAILED) { + peg$savedPos = s3; + s3 = peg$f1(s1, s5); + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f2(s1, s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$parseAndQuery(); + } + + return s0; + } + + function peg$parseAndQuery() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseNotQuery(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parseAnd(); + if (s4 !== peg$FAILED) { + s5 = peg$parseNotQuery(); + if (s5 !== peg$FAILED) { + peg$savedPos = s3; + s3 = peg$f1(s1, s5); + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parseAnd(); + if (s4 !== peg$FAILED) { + s5 = peg$parseNotQuery(); + if (s5 !== peg$FAILED) { + peg$savedPos = s3; + s3 = peg$f1(s1, s5); + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f3(s1, s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$parseNotQuery(); + } + + return s0; + } + + function peg$parseNotQuery() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parseNot(); + if (s1 !== peg$FAILED) { + s2 = peg$parseSubQuery(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f4(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$parseSubQuery(); + } + + return s0; + } + + function peg$parseSubQuery() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c0; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseSpace(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseSpace(); + } + s3 = peg$parseOrQuery(); + if (s3 !== peg$FAILED) { + s4 = peg$parseOptionalSpace(); + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 41) { + s5 = peg$c1; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f5(s3, s4); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$parseNestedQuery(); + } + + return s0; + } + + function peg$parseNestedQuery() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9; + + s0 = peg$currPos; + s1 = peg$parseField(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseSpace(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseSpace(); + } + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c2; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e2); } + } + if (s3 !== peg$FAILED) { + s4 = []; + s5 = peg$parseSpace(); + while (s5 !== peg$FAILED) { + s4.push(s5); + s5 = peg$parseSpace(); + } + if (input.charCodeAt(peg$currPos) === 123) { + s5 = peg$c3; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e3); } + } + if (s5 !== peg$FAILED) { + s6 = []; + s7 = peg$parseSpace(); + while (s7 !== peg$FAILED) { + s6.push(s7); + s7 = peg$parseSpace(); + } + s7 = peg$parseOrQuery(); + if (s7 !== peg$FAILED) { + s8 = peg$parseOptionalSpace(); + if (s8 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s9 = peg$c4; + peg$currPos++; + } else { + s9 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } + if (s9 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f6(s1, s7, s8); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$parseExpression(); + } + + return s0; + } + + function peg$parseExpression() { + var s0; + + s0 = peg$parseFieldRangeExpression(); + if (s0 === peg$FAILED) { + s0 = peg$parseFieldValueExpression(); + if (s0 === peg$FAILED) { + s0 = peg$parseValueExpression(); + } + } + + return s0; + } + + function peg$parseField() { + var s0, s1; + + peg$silentFails++; + s0 = peg$parseLiteral(); + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } + + return s0; + } + + function peg$parseFieldRangeExpression() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseField(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseSpace(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseSpace(); + } + s3 = peg$parseRangeOperator(); + if (s3 !== peg$FAILED) { + s4 = []; + s5 = peg$parseSpace(); + while (s5 !== peg$FAILED) { + s4.push(s5); + s5 = peg$parseSpace(); + } + s5 = peg$parseLiteral(); + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f7(s1, s3, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseFieldValueExpression() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseField(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseSpace(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseSpace(); + } + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c2; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e2); } + } + if (s3 !== peg$FAILED) { + s4 = []; + s5 = peg$parseSpace(); + while (s5 !== peg$FAILED) { + s4.push(s5); + s5 = peg$parseSpace(); + } + s5 = peg$parseListOfValues(); + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f8(s1, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseValueExpression() { + var s0, s1; + + s0 = peg$currPos; + s1 = peg$parseValue(); + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f9(s1); + } + s0 = s1; + + return s0; + } + + function peg$parseListOfValues() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c0; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseSpace(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseSpace(); + } + s3 = peg$parseOrListOfValues(); + if (s3 !== peg$FAILED) { + s4 = peg$parseOptionalSpace(); + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 41) { + s5 = peg$c1; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f10(s3, s4); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$parseValue(); + } + + return s0; + } + + function peg$parseOrListOfValues() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseAndListOfValues(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parseOr(); + if (s4 !== peg$FAILED) { + s5 = peg$parseAndListOfValues(); + if (s5 !== peg$FAILED) { + peg$savedPos = s3; + s3 = peg$f11(s1, s5); + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parseOr(); + if (s4 !== peg$FAILED) { + s5 = peg$parseAndListOfValues(); + if (s5 !== peg$FAILED) { + peg$savedPos = s3; + s3 = peg$f11(s1, s5); + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f12(s1, s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$parseAndListOfValues(); + } + + return s0; + } + + function peg$parseAndListOfValues() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseNotListOfValues(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parseAnd(); + if (s4 !== peg$FAILED) { + s5 = peg$parseNotListOfValues(); + if (s5 !== peg$FAILED) { + peg$savedPos = s3; + s3 = peg$f11(s1, s5); + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parseAnd(); + if (s4 !== peg$FAILED) { + s5 = peg$parseNotListOfValues(); + if (s5 !== peg$FAILED) { + peg$savedPos = s3; + s3 = peg$f11(s1, s5); + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f13(s1, s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$parseNotListOfValues(); + } + + return s0; + } + + function peg$parseNotListOfValues() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parseNot(); + if (s1 !== peg$FAILED) { + s2 = peg$parseListOfValues(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f14(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$parseListOfValues(); + } + + return s0; + } + + function peg$parseValue() { + var s0, s1; + + peg$silentFails++; + s0 = peg$currPos; + s1 = peg$parseQuotedString(); + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f15(s1); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parseUnquotedLiteral(); + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f16(s1); + } + s0 = s1; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } + + return s0; + } + + function peg$parseOr() { + var s0, s1, s2, s3, s4; + + peg$silentFails++; + s0 = peg$currPos; + s1 = []; + s2 = peg$parseSpace(); + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$parseSpace(); + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + if (input.substr(peg$currPos, 2).toLowerCase() === peg$c5) { + s2 = input.substr(peg$currPos, 2); + peg$currPos += 2; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$parseSpace(); + if (s4 !== peg$FAILED) { + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$parseSpace(); + } + } else { + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s1 = [s1, s2, s3]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e7); } + } + + return s0; + } + + function peg$parseAnd() { + var s0, s1, s2, s3, s4; + + peg$silentFails++; + s0 = peg$currPos; + s1 = []; + s2 = peg$parseSpace(); + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$parseSpace(); + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + if (input.substr(peg$currPos, 3).toLowerCase() === peg$c6) { + s2 = input.substr(peg$currPos, 3); + peg$currPos += 3; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e10); } + } + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$parseSpace(); + if (s4 !== peg$FAILED) { + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$parseSpace(); + } + } else { + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s1 = [s1, s2, s3]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e9); } + } + + return s0; + } + + function peg$parseNot() { + var s0, s1, s2, s3; + + peg$silentFails++; + s0 = peg$currPos; + if (input.substr(peg$currPos, 3).toLowerCase() === peg$c7) { + s1 = input.substr(peg$currPos, 3); + peg$currPos += 3; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseSpace(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseSpace(); + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e11); } + } + + return s0; + } + + function peg$parseLiteral() { + var s0, s1; + + peg$silentFails++; + s0 = peg$parseQuotedString(); + if (s0 === peg$FAILED) { + s0 = peg$parseUnquotedLiteral(); + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e13); } + } + + return s0; + } + + function peg$parseQuotedString() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + peg$savedPos = peg$currPos; + s1 = peg$f17(); + if (s1) { + s1 = undefined; + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 34) { + s2 = peg$c8; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e14); } + } + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$parseQuotedCharacter(); + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$parseQuotedCharacter(); + } + s4 = peg$parseCursor(); + if (s4 !== peg$FAILED) { + s5 = []; + s6 = peg$parseQuotedCharacter(); + while (s6 !== peg$FAILED) { + s5.push(s6); + s6 = peg$parseQuotedCharacter(); + } + if (input.charCodeAt(peg$currPos) === 34) { + s6 = peg$c8; + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e14); } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f18(s3, s4, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 34) { + s1 = peg$c8; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e14); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseQuotedCharacter(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseQuotedCharacter(); + } + if (input.charCodeAt(peg$currPos) === 34) { + s3 = peg$c8; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e14); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f19(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + + return s0; + } + + function peg$parseQuotedCharacter() { + var s0, s1, s2; + + s0 = peg$parseEscapedWhitespace(); + if (s0 === peg$FAILED) { + s0 = peg$parseEscapedUnicodeSequence(); + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c9; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } + if (s1 !== peg$FAILED) { + if (peg$r0.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f20(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$currPos; + peg$silentFails++; + s2 = peg$parseCursor(); + peg$silentFails--; + if (s2 === peg$FAILED) { + s1 = undefined; + } else { + peg$currPos = s1; + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + if (peg$r1.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f20(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + } + } + + return s0; + } + + function peg$parseUnquotedLiteral() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + peg$savedPos = peg$currPos; + s1 = peg$f17(); + if (s1) { + s1 = undefined; + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseUnquotedCharacter(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseUnquotedCharacter(); + } + s3 = peg$parseCursor(); + if (s3 !== peg$FAILED) { + s4 = []; + s5 = peg$parseUnquotedCharacter(); + while (s5 !== peg$FAILED) { + s4.push(s5); + s5 = peg$parseUnquotedCharacter(); + } + peg$savedPos = s0; + s0 = peg$f18(s2, s3, s4); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = []; + s2 = peg$parseUnquotedCharacter(); + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$parseUnquotedCharacter(); + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f21(s1); + } + s0 = s1; + } + + return s0; + } + + function peg$parseUnquotedCharacter() { + var s0, s1, s2, s3, s4; + + s0 = peg$parseEscapedWhitespace(); + if (s0 === peg$FAILED) { + s0 = peg$parseEscapedSpecialCharacter(); + if (s0 === peg$FAILED) { + s0 = peg$parseEscapedUnicodeSequence(); + if (s0 === peg$FAILED) { + s0 = peg$parseEscapedKeyword(); + if (s0 === peg$FAILED) { + s0 = peg$parseWildcard(); + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$currPos; + peg$silentFails++; + s2 = peg$parseSpecialCharacter(); + peg$silentFails--; + if (s2 === peg$FAILED) { + s1 = undefined; + } else { + peg$currPos = s1; + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + peg$silentFails++; + s3 = peg$parseKeyword(); + peg$silentFails--; + if (s3 === peg$FAILED) { + s2 = undefined; + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s3 = peg$currPos; + peg$silentFails++; + s4 = peg$parseCursor(); + peg$silentFails--; + if (s4 === peg$FAILED) { + s3 = undefined; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + if (input.length > peg$currPos) { + s4 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e18); } + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f20(s4); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + } + } + } + } + + return s0; + } + + function peg$parseWildcard() { + var s0, s1; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 42) { + s1 = peg$c10; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e19); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f22(); + } + s0 = s1; + + return s0; + } + + function peg$parseOptionalSpace() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + peg$savedPos = peg$currPos; + s1 = peg$f17(); + if (s1) { + s1 = undefined; + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseSpace(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseSpace(); + } + s3 = peg$parseCursor(); + if (s3 !== peg$FAILED) { + s4 = []; + s5 = peg$parseSpace(); + while (s5 !== peg$FAILED) { + s4.push(s5); + s5 = peg$parseSpace(); + } + peg$savedPos = s0; + s0 = peg$f18(s2, s3, s4); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = []; + s1 = peg$parseSpace(); + while (s1 !== peg$FAILED) { + s0.push(s1); + s1 = peg$parseSpace(); + } + } + + return s0; + } + + function peg$parseEscapedWhitespace() { + var s0, s1; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c11) { + s1 = peg$c11; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e20); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f23(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c12) { + s1 = peg$c12; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e21); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f24(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c13) { + s1 = peg$c13; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e22); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f25(); + } + s0 = s1; + } + } + + return s0; + } + + function peg$parseEscapedSpecialCharacter() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c9; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseSpecialCharacter(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f20(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseEscapedKeyword() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c9; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } + if (s1 !== peg$FAILED) { + if (input.substr(peg$currPos, 2).toLowerCase() === peg$c5) { + s2 = input.substr(peg$currPos, 2); + peg$currPos += 2; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } + if (s2 === peg$FAILED) { + if (input.substr(peg$currPos, 3).toLowerCase() === peg$c6) { + s2 = input.substr(peg$currPos, 3); + peg$currPos += 3; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e10); } + } + if (s2 === peg$FAILED) { + if (input.substr(peg$currPos, 3).toLowerCase() === peg$c7) { + s2 = input.substr(peg$currPos, 3); + peg$currPos += 3; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } + } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f26(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseKeyword() { + var s0; + + s0 = peg$parseOr(); + if (s0 === peg$FAILED) { + s0 = peg$parseAnd(); + if (s0 === peg$FAILED) { + s0 = peg$parseNot(); + } + } + + return s0; + } + + function peg$parseSpecialCharacter() { + var s0; + + if (peg$r2.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e23); } + } + + return s0; + } + + function peg$parseEscapedUnicodeSequence() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c9; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseUnicodeSequence(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f27(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseUnicodeSequence() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 117) { + s1 = peg$c14; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e24); } + } + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + s3 = peg$currPos; + s4 = peg$parseHexDigit(); + if (s4 !== peg$FAILED) { + s5 = peg$parseHexDigit(); + if (s5 !== peg$FAILED) { + s6 = peg$parseHexDigit(); + if (s6 !== peg$FAILED) { + s7 = peg$parseHexDigit(); + if (s7 !== peg$FAILED) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s2 = input.substring(s2, peg$currPos); + } else { + s2 = s3; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f28(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseHexDigit() { + var s0; + + if (peg$r3.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e25); } + } + + return s0; + } + + function peg$parseRangeOperator() { + var s0, s1; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c15) { + s1 = peg$c15; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e26); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f29(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c16) { + s1 = peg$c16; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e27); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f30(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 60) { + s1 = peg$c17; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f31(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 62) { + s1 = peg$c18; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e29); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f32(); + } + s0 = s1; + } + } + } + + return s0; + } + + function peg$parseSpace() { + var s0, s1; + + peg$silentFails++; + if (peg$r4.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e31); } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e30); } + } + + return s0; + } + + function peg$parseCursor() { + var s0, s1, s2; + + s0 = peg$currPos; + peg$savedPos = peg$currPos; + s1 = peg$f17(); + if (s1) { + s1 = undefined; + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + if (input.substr(peg$currPos, 14) === peg$c19) { + s2 = peg$c19; + peg$currPos += 14; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e32); } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f33(); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + + const { parseCursor, cursorSymbol, allowLeadingWildcards = true, helpers: { nodeTypes } } = options; + const buildFunctionNode = nodeTypes.function.buildNodeWithArgumentNodes; + const buildLiteralNode = nodeTypes.literal.buildNode; + const buildWildcardNode = nodeTypes.wildcard.buildNode; + const buildNamedArgNode = nodeTypes.namedArg.buildNode; + const { wildcardSymbol } = nodeTypes.wildcard; + + + peg$result = peg$startRuleFunction(); + + if (peg$result !== peg$FAILED && peg$currPos === input.length) { + return peg$result; + } else { + if (peg$result !== peg$FAILED && peg$currPos < input.length) { + peg$fail(peg$endExpectation()); + } + + throw peg$buildStructuredError( + peg$maxFailExpected, + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, + peg$maxFailPos < input.length + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } +} + +module.exports = { + SyntaxError: peg$SyntaxError, + parse: peg$parse +}; diff --git a/packages/kbn-es-query/src/kuery/grammar/__mocks__/index.ts b/packages/kbn-es-query/src/kuery/grammar/__mocks__/index.ts new file mode 100644 index 0000000000000..9103c852c4845 --- /dev/null +++ b/packages/kbn-es-query/src/kuery/grammar/__mocks__/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. + */ +// @ts-expect-error +export { parse } from './grammar'; diff --git a/packages/kbn-es-query/src/kuery/grammar/index.ts b/packages/kbn-es-query/src/kuery/grammar/index.ts new file mode 100644 index 0000000000000..811fa723da3b8 --- /dev/null +++ b/packages/kbn-es-query/src/kuery/grammar/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +// @ts-expect-error +export { parse } from '../../../grammar'; diff --git a/src/plugins/data/common/es_query/kuery/index.ts b/packages/kbn-es-query/src/kuery/index.ts similarity index 80% rename from src/plugins/data/common/es_query/kuery/index.ts rename to packages/kbn-es-query/src/kuery/index.ts index 72eecc09756b4..7796785f85394 100644 --- a/src/plugins/data/common/es_query/kuery/index.ts +++ b/packages/kbn-es-query/src/kuery/index.ts @@ -8,6 +8,5 @@ export { KQLSyntaxError } from './kuery_syntax_error'; export { nodeTypes, nodeBuilder } from './node_types'; -export * from './ast'; - -export * from './types'; +export { fromKueryExpression, toElasticsearchQuery } from './ast'; +export { DslQuery, KueryNode } from './types'; diff --git a/src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts b/packages/kbn-es-query/src/kuery/kuery_syntax_error.test.ts similarity index 89% rename from src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts rename to packages/kbn-es-query/src/kuery/kuery_syntax_error.test.ts index 6875bc3e5f74f..e04b1abdc8494 100644 --- a/src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts +++ b/packages/kbn-es-query/src/kuery/kuery_syntax_error.test.ts @@ -8,6 +8,8 @@ import { fromKueryExpression } from './ast'; +jest.mock('./grammar'); + describe('kql syntax errors', () => { it('should throw an error for a field query missing a value', () => { expect(() => { @@ -47,7 +49,7 @@ describe('kql syntax errors', () => { ); }); - it('should throw an error for a NOT list missing a sub-query', () => { + it('should throw an error for a "missing a sub-query', () => { expect(() => { fromKueryExpression('response:(200 and not )'); }).toThrow( @@ -66,13 +68,17 @@ describe('kql syntax errors', () => { it('should throw an error for unescaped quotes in a quoted string', () => { expect(() => { fromKueryExpression('foo:"ba "r"'); - }).toThrow('Expected AND, OR, end of input but "r" found.\n' + 'foo:"ba "r"\n' + '---------^'); + }).toThrow( + 'Expected AND, OR, end of input, whitespace but "r" found.\n' + 'foo:"ba "r"\n' + '---------^' + ); }); it('should throw an error for unescaped special characters in literals', () => { expect(() => { fromKueryExpression('foo:ba:r'); - }).toThrow('Expected AND, OR, end of input but ":" found.\n' + 'foo:ba:r\n' + '------^'); + }).toThrow( + 'Expected AND, OR, end of input, whitespace but ":" found.\n' + 'foo:ba:r\n' + '------^' + ); }); it('should throw an error for range queries missing a value', () => { diff --git a/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts b/packages/kbn-es-query/src/kuery/kuery_syntax_error.ts similarity index 59% rename from src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts rename to packages/kbn-es-query/src/kuery/kuery_syntax_error.ts index a9adbad4781b7..aa4440579eb49 100644 --- a/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts +++ b/packages/kbn-es-query/src/kuery/kuery_syntax_error.ts @@ -6,28 +6,40 @@ * Side Public License, v 1. */ -import { repeat } from 'lodash'; +import { repeat, uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; -const endOfInputText = i18n.translate('data.common.kql.errors.endOfInputText', { +const endOfInputText = i18n.translate('esQuery.kql.errors.endOfInputText', { defaultMessage: 'end of input', }); const grammarRuleTranslations: Record = { - fieldName: i18n.translate('data.common.kql.errors.fieldNameText', { + fieldName: i18n.translate('esQuery.kql.errors.fieldNameText', { defaultMessage: 'field name', }), - value: i18n.translate('data.common.kql.errors.valueText', { + value: i18n.translate('esQuery.kql.errors.valueText', { defaultMessage: 'value', }), - literal: i18n.translate('data.common.kql.errors.literalText', { + literal: i18n.translate('esQuery.kql.errors.literalText', { defaultMessage: 'literal', }), - whitespace: i18n.translate('data.common.kql.errors.whitespaceText', { + whitespace: i18n.translate('esQuery.kql.errors.whitespaceText', { defaultMessage: 'whitespace', }), }; +const getItemText = (item: KQLSyntaxErrorExpected): string => { + if (item.type === 'other') { + return item.description!; + } else if (item.type === 'literal') { + return `"${item.text!}"`; + } else if (item.type === 'end') { + return 'end of input'; + } else { + return item.text || item.description || ''; + } +}; + interface KQLSyntaxErrorData extends Error { found: string; expected: KQLSyntaxErrorExpected[] | null; @@ -35,7 +47,9 @@ interface KQLSyntaxErrorData extends Error { } interface KQLSyntaxErrorExpected { - description: string; + description?: string; + text?: string; + type: string; } export class KQLSyntaxError extends Error { @@ -45,12 +59,16 @@ export class KQLSyntaxError extends Error { let message = error.message; if (error.expected) { const translatedExpectations = error.expected.map((expected) => { - return grammarRuleTranslations[expected.description] || expected.description; + const key = getItemText(expected); + return grammarRuleTranslations[key] || key; }); - const translatedExpectationText = translatedExpectations.join(', '); + const translatedExpectationText = uniq(translatedExpectations) + .filter((item) => item !== undefined) + .sort() + .join(', '); - message = i18n.translate('data.common.kql.errors.syntaxError', { + message = i18n.translate('esQuery.kql.errors.syntaxError', { defaultMessage: 'Expected {expectedList} but {foundInput} found.', values: { expectedList: translatedExpectationText, diff --git a/src/plugins/data/common/es_query/kuery/node_types/function.test.ts b/packages/kbn-es-query/src/kuery/node_types/function.test.ts similarity index 91% rename from src/plugins/data/common/es_query/kuery/node_types/function.test.ts rename to packages/kbn-es-query/src/kuery/node_types/function.test.ts index 42c06d7fdb603..5df6ba1916046 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/function.test.ts +++ b/packages/kbn-es-query/src/kuery/node_types/function.test.ts @@ -6,22 +6,23 @@ * Side Public License, v 1. */ -import { fields } from '../../../index_patterns/mocks'; - import { nodeTypes } from './index'; -import { IIndexPattern } from '../../../index_patterns'; import { buildNode, buildNodeWithArgumentNodes, toElasticsearchQuery } from './function'; import { toElasticsearchQuery as isFunctionToElasticsearchQuery } from '../functions/is'; +import { IndexPatternBase } from '../../es_query'; +import { fields } from '../../filters/stubs/fields.mocks'; + +jest.mock('../grammar'); describe('kuery node types', () => { describe('function', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPatternBase; beforeEach(() => { - indexPattern = ({ + indexPattern = { fields, - } as unknown) as IIndexPattern; + }; }); describe('buildNode', () => { diff --git a/src/plugins/data/common/es_query/kuery/node_types/function.ts b/packages/kbn-es-query/src/kuery/node_types/function.ts similarity index 96% rename from src/plugins/data/common/es_query/kuery/node_types/function.ts rename to packages/kbn-es-query/src/kuery/node_types/function.ts index 642089a101f31..e72f8a6b1e77a 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/function.ts +++ b/packages/kbn-es-query/src/kuery/node_types/function.ts @@ -9,7 +9,7 @@ import _ from 'lodash'; import { functions } from '../functions'; -import { IndexPatternBase, KueryNode } from '../../..'; +import { IndexPatternBase, KueryNode } from '../..'; import { FunctionName, FunctionTypeBuildNode } from './types'; export function buildNode(functionName: FunctionName, ...args: any[]) { diff --git a/src/plugins/data/common/es_query/kuery/node_types/index.ts b/packages/kbn-es-query/src/kuery/node_types/index.ts similarity index 100% rename from src/plugins/data/common/es_query/kuery/node_types/index.ts rename to packages/kbn-es-query/src/kuery/node_types/index.ts diff --git a/src/plugins/data/common/es_query/kuery/node_types/literal.test.ts b/packages/kbn-es-query/src/kuery/node_types/literal.test.ts similarity index 97% rename from src/plugins/data/common/es_query/kuery/node_types/literal.test.ts rename to packages/kbn-es-query/src/kuery/node_types/literal.test.ts index c370292de38bc..7a36be704a609 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/literal.test.ts +++ b/packages/kbn-es-query/src/kuery/node_types/literal.test.ts @@ -9,6 +9,8 @@ // @ts-ignore import { buildNode, toElasticsearchQuery } from './literal'; +jest.mock('../grammar'); + describe('kuery node types', () => { describe('literal', () => { describe('buildNode', () => { diff --git a/src/plugins/data/common/es_query/kuery/node_types/literal.ts b/packages/kbn-es-query/src/kuery/node_types/literal.ts similarity index 100% rename from src/plugins/data/common/es_query/kuery/node_types/literal.ts rename to packages/kbn-es-query/src/kuery/node_types/literal.ts diff --git a/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts b/packages/kbn-es-query/src/kuery/node_types/named_arg.test.ts similarity index 98% rename from src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts rename to packages/kbn-es-query/src/kuery/node_types/named_arg.test.ts index 2c3fb43ee0f59..fa944660288d5 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts +++ b/packages/kbn-es-query/src/kuery/node_types/named_arg.test.ts @@ -7,10 +7,10 @@ */ import { nodeTypes } from './index'; - -// @ts-ignore import { buildNode, toElasticsearchQuery } from './named_arg'; +jest.mock('../grammar'); + describe('kuery node types', () => { describe('named arg', () => { describe('buildNode', () => { diff --git a/src/plugins/data/common/es_query/kuery/node_types/named_arg.ts b/packages/kbn-es-query/src/kuery/node_types/named_arg.ts similarity index 100% rename from src/plugins/data/common/es_query/kuery/node_types/named_arg.ts rename to packages/kbn-es-query/src/kuery/node_types/named_arg.ts diff --git a/src/plugins/data/common/es_query/kuery/node_types/node_builder.test.ts b/packages/kbn-es-query/src/kuery/node_types/node_builder.test.ts similarity index 99% rename from src/plugins/data/common/es_query/kuery/node_types/node_builder.test.ts rename to packages/kbn-es-query/src/kuery/node_types/node_builder.test.ts index d6439f8e7cc87..aefd40c6db3fb 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/node_builder.test.ts +++ b/packages/kbn-es-query/src/kuery/node_types/node_builder.test.ts @@ -9,6 +9,8 @@ import { nodeBuilder } from './node_builder'; import { toElasticsearchQuery } from '../index'; +jest.mock('../grammar'); + describe('nodeBuilder', () => { describe('is method', () => { test('string value', () => { diff --git a/src/plugins/data/common/es_query/kuery/node_types/node_builder.ts b/packages/kbn-es-query/src/kuery/node_types/node_builder.ts similarity index 100% rename from src/plugins/data/common/es_query/kuery/node_types/node_builder.ts rename to packages/kbn-es-query/src/kuery/node_types/node_builder.ts diff --git a/src/plugins/data/common/es_query/kuery/node_types/types.ts b/packages/kbn-es-query/src/kuery/node_types/types.ts similarity index 100% rename from src/plugins/data/common/es_query/kuery/node_types/types.ts rename to packages/kbn-es-query/src/kuery/node_types/types.ts diff --git a/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts b/packages/kbn-es-query/src/kuery/node_types/wildcard.test.ts similarity index 99% rename from src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts rename to packages/kbn-es-query/src/kuery/node_types/wildcard.test.ts index 8c20851cfe3f9..9f444eec82b69 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts +++ b/packages/kbn-es-query/src/kuery/node_types/wildcard.test.ts @@ -16,6 +16,8 @@ import { // @ts-ignore } from './wildcard'; +jest.mock('../grammar'); + describe('kuery node types', () => { describe('wildcard', () => { describe('buildNode', () => { diff --git a/src/plugins/data/common/es_query/kuery/node_types/wildcard.ts b/packages/kbn-es-query/src/kuery/node_types/wildcard.ts similarity index 100% rename from src/plugins/data/common/es_query/kuery/node_types/wildcard.ts rename to packages/kbn-es-query/src/kuery/node_types/wildcard.ts diff --git a/src/plugins/data/common/es_query/kuery/types.ts b/packages/kbn-es-query/src/kuery/types.ts similarity index 100% rename from src/plugins/data/common/es_query/kuery/types.ts rename to packages/kbn-es-query/src/kuery/types.ts diff --git a/src/plugins/data/common/es_query/utils.ts b/packages/kbn-es-query/src/utils.ts similarity index 100% rename from src/plugins/data/common/es_query/utils.ts rename to packages/kbn-es-query/src/utils.ts diff --git a/packages/kbn-es-query/tsconfig.browser.json b/packages/kbn-es-query/tsconfig.browser.json new file mode 100644 index 0000000000000..0a1c21cc8e05b --- /dev/null +++ b/packages/kbn-es-query/tsconfig.browser.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.browser.json", + "compilerOptions": { + "incremental": true, + "outDir": "./target_web", + "declaration": false, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-es-query/src", + "types": [ + "jest", + "node" + ], + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "**/__fixtures__/**/*", + "**/__mocks__/**/*" + ] +} diff --git a/packages/kbn-es-query/tsconfig.json b/packages/kbn-es-query/tsconfig.json new file mode 100644 index 0000000000000..b48d90373e2cb --- /dev/null +++ b/packages/kbn-es-query/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": true, + "declarationDir": "./target_types", + "outDir": "./target_node", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-es-query/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "**/__fixtures__/**/*", + "**/__mocks__/**/grammar.js", + ] +} diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 49d4bdbc2de64..0bf15d236bc9c 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -113,4 +113,6 @@ pageLoadAssetSize: expressionRevealImage: 25675 cases: 144442 expressionError: 22127 + expressionRepeatImage: 22341 + expressionShape: 30033 userSetup: 18532 diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index 3cabb307b1654..54a6fa26664e3 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -31,6 +31,7 @@ const ALERT_STATUS = `${ALERT_NAMESPACE}.status` as const; const SPACE_IDS = 'kibana.space_ids' as const; const ALERT_EVALUATION_THRESHOLD = `${ALERT_NAMESPACE}.evaluation.threshold` as const; const ALERT_EVALUATION_VALUE = `${ALERT_NAMESPACE}.evaluation.value` as const; +const ALERT_REASON = `${ALERT_NAMESPACE}.reason` as const; const fields = { TIMESTAMP, @@ -53,6 +54,7 @@ const fields = { ALERT_STATUS, ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, + ALERT_REASON, SPACE_IDS, }; @@ -77,6 +79,7 @@ export { ALERT_STATUS, ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, + ALERT_REASON, SPACE_IDS, }; diff --git a/packages/kbn-securitysolution-autocomplete/BUILD.bazel b/packages/kbn-securitysolution-autocomplete/BUILD.bazel new file mode 100644 index 0000000000000..8e403a215d81d --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/BUILD.bazel @@ -0,0 +1,125 @@ +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-autocomplete" + +PKG_REQUIRE_NAME = "@kbn/securitysolution-autocomplete" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx" + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*", + "**/*.mocks.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "react/package.json", + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-babel-preset", + "//packages/kbn-dev-utils", + "//packages/kbn-i18n", + "//packages/kbn-securitysolution-io-ts-list-types", + "//packages/kbn-securitysolution-list-hooks", + "@npm//@babel/core", + "@npm//babel-loader", + "@npm//@elastic/eui", + "@npm//react", + "@npm//resize-observer-polyfill", + "@npm//rxjs", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//typescript", + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/react", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_config( + name = "tsconfig_browser", + src = "tsconfig.browser.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.browser.json", + ], +) + +ts_project( + name = "tsc", + args = ["--pretty"], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = True, + declaration_dir = "target_types", + declaration_map = True, + incremental = True, + out_dir = "target_node", + root_dir = "src", + source_map = True, + tsconfig = ":tsconfig", +) + +ts_project( + name = "tsc_browser", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = False, + incremental = True, + out_dir = "target_web", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig_browser", +) + +js_library( + name = PKG_BASE_NAME, + package_name = PKG_REQUIRE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + visibility = ["//visibility:public"], + deps = [":tsc", ":tsc_browser"] + DEPS, +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/README.md b/packages/kbn-securitysolution-autocomplete/README.md similarity index 100% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/README.md rename to packages/kbn-securitysolution-autocomplete/README.md diff --git a/packages/kbn-securitysolution-autocomplete/babel.config.js b/packages/kbn-securitysolution-autocomplete/babel.config.js new file mode 100644 index 0000000000000..b4a118df51af5 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/babel.config.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + env: { + web: { + presets: ['@kbn/babel-preset/webpack_preset'], + }, + node: { + presets: ['@kbn/babel-preset/node_preset'], + }, + }, + ignore: ['**/*.test.ts', '**/*.test.tsx'], +}; diff --git a/packages/kbn-securitysolution-autocomplete/jest.config.js b/packages/kbn-securitysolution-autocomplete/jest.config.js new file mode 100644 index 0000000000000..9b14447c98366 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/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-autocomplete'], +}; diff --git a/packages/kbn-securitysolution-autocomplete/package.json b/packages/kbn-securitysolution-autocomplete/package.json new file mode 100644 index 0000000000000..5cfd18b63256a --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/securitysolution-autocomplete", + "version": "1.0.0", + "description": "Security Solution auto complete", + "license": "SSPL-1.0 OR Elastic License 2.0", + "browser": "./target_web/index.js", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-autocomplete/react/package.json b/packages/kbn-securitysolution-autocomplete/react/package.json new file mode 100644 index 0000000000000..c5f222b5843ac --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/react/package.json @@ -0,0 +1,5 @@ +{ + "browser": "../target_web/react", + "main": "../target_node/react", + "types": "../target_types/react/index.d.ts" +} diff --git a/packages/kbn-securitysolution-autocomplete/src/autocomplete/index.mock.ts b/packages/kbn-securitysolution-autocomplete/src/autocomplete/index.mock.ts new file mode 100644 index 0000000000000..444a033b4887b --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/autocomplete/index.mock.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 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/plugins/data/public/mocks.ts" but without any type information +// TODO: Remove this in favor of the data/public/mocks if/when they become available, https://github.com/elastic/kibana/issues/100715 +export const autocompleteStartMock = { + getQuerySuggestions: jest.fn(), + getValueSuggestions: jest.fn(), + hasQuerySuggestions: jest.fn(), +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts new file mode 100644 index 0000000000000..c36184e5c5ba1 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { checkEmptyValue } from '.'; +import { getField } from '../fields/index.mock'; +import * as i18n from '../translations'; + +describe('check_empty_value', () => { + test('returns no errors if no field has been selected', () => { + const isValid = checkEmptyValue('', undefined, true, false); + + expect(isValid).toBeUndefined(); + }); + + test('returns error string if user has touched a required input and left empty', () => { + const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, true); + + expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); + }); + + test('returns no errors if required input is empty but user has not yet touched it', () => { + const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, false); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if user has touched an input that is not required and left empty', () => { + const isValid = checkEmptyValue(undefined, getField('@timestamp'), false, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if user has touched an input that is not required and left empty string', () => { + const isValid = checkEmptyValue('', getField('@timestamp'), false, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns null if input value is not empty string or undefined', () => { + const isValid = checkEmptyValue('hellooo', getField('@timestamp'), false, true); + + expect(isValid).toBeNull(); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts new file mode 100644 index 0000000000000..894f233f73a5a --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/check_empty_value/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as i18n from '../translations'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +/** + * Determines if empty value is ok + */ +export const checkEmptyValue = ( + param: string | undefined, + field: IFieldType | undefined, + isRequired: boolean, + touched: boolean +): string | undefined | null => { + if (isRequired && touched && (param == null || param.trim() === '')) { + return i18n.FIELD_REQUIRED_ERR; + } + + if ( + field == null || + (isRequired && !touched) || + (!isRequired && (param == null || param === '')) + ) { + return undefined; + } + + return null; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx similarity index 92% rename from x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx index 79e6fe5506b84..08f55cef89b66 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field/index.test.tsx @@ -1,35 +1,32 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import { mount } from 'enzyme'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { FieldComponent } from '.'; +import { fields, getField } from '../fields/index.mock'; -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { FieldComponent } from './field'; - -describe('FieldComponent', () => { +describe('field', () => { test('it renders disabled if "isDisabled" is true', () => { const wrapper = mount( ); @@ -41,17 +38,17 @@ describe('FieldComponent', () => { test('it renders loading if "isLoading" is true', () => { const wrapper = mount( ); wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] button`).at(0).simulate('click'); @@ -65,17 +62,17 @@ describe('FieldComponent', () => { test('it allows user to clear values if "isClearable" is true', () => { const wrapper = mount( ); @@ -89,17 +86,17 @@ describe('FieldComponent', () => { test('it correctly displays selected field', () => { const wrapper = mount( ); @@ -112,17 +109,17 @@ describe('FieldComponent', () => { const mockOnChange = jest.fn(); const wrapper = mount( ); diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx b/packages/kbn-securitysolution-autocomplete/src/field/index.tsx similarity index 87% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx rename to packages/kbn-securitysolution-autocomplete/src/field/index.tsx index 47527914e71ff..43342079ef92b 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field/index.tsx @@ -1,17 +1,23 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React, { useCallback, useMemo, useState } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; +type IIndexPattern = any; -import { getGenericComboBoxProps } from './helpers'; -import { GetGenericComboBoxPropsReturn } from './types'; +import { + getGenericComboBoxProps, + GetGenericComboBoxPropsReturn, +} from '../get_generic_combo_box_props'; const AS_PLAIN_TEXT = { asPlainText: true }; @@ -28,13 +34,6 @@ interface OperatorProps { selectedField: IFieldType | undefined; } -/** - * There is a copy within: - * x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * NOTE: This has deviated from the copy and will have to be reconciled. - */ export const FieldComponent: React.FC = ({ fieldInputWidth, fieldTypeFilter = [], diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.test.tsx similarity index 70% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.test.tsx index b6300581f12dd..c4c07aff909e4 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.test.tsx @@ -1,14 +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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import { mount } from 'enzyme'; -import { AutocompleteFieldExistsComponent } from './field_value_exists'; +import { AutocompleteFieldExistsComponent } from '.'; describe('AutocompleteFieldExistsComponent', () => { test('it renders field disabled', () => { diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.tsx similarity index 83% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.tsx index ff70204e53483..37a16406e65a3 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_exists.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_exists/index.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx similarity index 88% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx index a338ce6a27d6c..6fcf8ddf74b03 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -11,15 +12,20 @@ import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { waitFor } from '@testing-library/react'; import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock'; -import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; -import { DATE_NOW, IMMUTABLE, VERSION } from '../../../../../lists/common/constants.mock'; - -import { AutocompleteFieldListsComponent } from './field_value_lists'; - -const mockKibanaHttpService = coreMock.createStart().http; +import { getField } from '../fields/index.mock'; +import { AutocompleteFieldListsComponent } from '.'; +import { + getListResponseMock, + getFoundListSchemaMock, + DATE_NOW, + IMMUTABLE, + VERSION, +} from '../list_schema/index.mock'; + +// TODO: Once these mocks are available, use them instead of hand mocking, https://github.com/elastic/kibana/issues/100715 +// const mockKibanaHttpService = coreMock.createStart().http; +// import { coreMock } from '../../../../../../../src/core/public/mocks'; +const mockKibanaHttpService = jest.fn(); const mockStart = jest.fn(); const mockKeywordList: ListSchema = { @@ -35,7 +41,6 @@ jest.mock('@kbn/securitysolution-list-hooks', () => { return { ...originalModule, - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type useFindLists: () => ({ error: undefined, loading: false, diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx similarity index 80% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx index 047f8ef33c8c0..4064ff11962bd 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx @@ -1,20 +1,28 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; -import { HttpStart } from 'kibana/public'; import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; import { useFindLists } from '@kbn/securitysolution-list-hooks'; -import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { filterFieldToList } from '../filter_field_to_list'; +import { getGenericComboBoxProps } from '../get_generic_combo_box_props'; -import { filterFieldToList, getGenericComboBoxProps } from './helpers'; -import * as i18n from './translations'; +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 +// import { HttpStart } from 'kibana/public'; +type HttpStart = any; + +import * as i18n from '../translations'; const SINGLE_SELECTION = { asPlainText: true }; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx similarity index 95% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx index c1ffb008e8563..d695088245622 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.test.tsx @@ -1,27 +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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import { ReactWrapper, mount } from 'enzyme'; import { EuiComboBox, EuiComboBoxOptionOption, EuiSuperSelect } from '@elastic/eui'; import { act } from '@testing-library/react'; +import { AutocompleteFieldMatchComponent } from '.'; +import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; +import { fields, getField } from '../fields/index.mock'; +import { autocompleteStartMock } from '../autocomplete/index.mock'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; - -import { AutocompleteFieldMatchComponent } from './field_value_match'; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; - -jest.mock('./hooks/use_field_value_autocomplete'); - -const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); +jest.mock('../hooks/use_field_value_autocomplete'); describe('AutocompleteFieldMatchComponent', () => { let wrapper: ReactWrapper; @@ -299,7 +293,6 @@ describe('AutocompleteFieldMatchComponent', () => { selectedValue="" /> ); - expect( wrapper.find('[data-test-subj="valuesAutocompleteMatchBoolean"]').exists() ).toBeTruthy(); @@ -431,7 +424,6 @@ describe('AutocompleteFieldMatchComponent', () => { selectedValue="" /> ); - wrapper .find('[data-test-subj="valueAutocompleteFieldMatchNumber"] input') .at(0) diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx similarity index 85% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx index 8dbe8f223ae5b..8199967489515 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx @@ -1,28 +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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; import { - EuiComboBox, - EuiComboBoxOptionOption, - EuiFieldNumber, - EuiFormRow, EuiSuperSelect, + EuiFormRow, + EuiFieldNumber, + EuiComboBoxOptionOption, + EuiComboBox, } from '@elastic/eui'; import { uniq } from 'lodash'; + import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 +// import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; +type AutocompleteStart = any; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; -import { getGenericComboBoxProps, paramIsValid } from './helpers'; -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; +type IIndexPattern = any; + +import * as i18n from '../translations'; +import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; +import { + getGenericComboBoxProps, + GetGenericComboBoxPropsReturn, +} from '../get_generic_combo_box_props'; +import { paramIsValid } from '../param_is_valid'; const BOOLEAN_OPTIONS = [ { inputDisplay: 'true', value: 'true' }, @@ -47,11 +58,6 @@ interface AutocompleteFieldMatchProps { onError?: (arg: boolean) => void; } -/** - * There is a copy of this within: - * x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - */ export const AutocompleteFieldMatchComponent: React.FC = ({ placeholder, rowLabel, @@ -189,11 +195,6 @@ export const AutocompleteFieldMatchComponent: React.FC (fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}), - [fieldInputWidth] - ); - useEffect((): void => { setError(undefined); if (onError != null) { @@ -225,7 +226,7 @@ export const AutocompleteFieldMatchComponent: React.FC @@ -234,7 +235,7 @@ export const AutocompleteFieldMatchComponent: React.FC @@ -289,7 +290,7 @@ export const AutocompleteFieldMatchComponent: React.FC diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.test.tsx similarity index 91% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.test.tsx index 8aa1f18b695a0..a3ca97874908e 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -10,18 +11,18 @@ import { ReactWrapper, mount } from 'enzyme'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { act } from '@testing-library/react'; -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { AutocompleteFieldMatchAnyComponent } from '.'; +import { getField, fields } from '../fields/index.mock'; +import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; +import { autocompleteStartMock } from '../autocomplete/index.mock'; -import { AutocompleteFieldMatchAnyComponent } from './field_value_match_any'; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; - -const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); - -jest.mock('./hooks/use_field_value_autocomplete'); +jest.mock('../hooks/use_field_value_autocomplete', () => { + const actual = jest.requireActual('../hooks/use_field_value_autocomplete'); + return { + ...actual, + useFieldValueAutocomplete: jest.fn(), + }; +}); describe('AutocompleteFieldMatchAnyComponent', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx similarity index 86% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx rename to packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx index e5a5e76f8cc5d..338c4baa8bc6f 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React, { useCallback, useMemo, useState } from 'react'; @@ -10,13 +11,22 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { uniq } from 'lodash'; import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; - -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; -import { getGenericComboBoxProps, paramIsValid } from './helpers'; -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 +// import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; +type AutocompleteStart = any; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; +type IIndexPattern = any; + +import * as i18n from '../translations'; +import { + getGenericComboBoxProps, + GetGenericComboBoxPropsReturn, +} from '../get_generic_combo_box_props'; +import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; +import { paramIsValid } from '../param_is_valid'; interface AutocompleteFieldMatchAnyProps { placeholder: string; diff --git a/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts b/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts new file mode 100644 index 0000000000000..5938ed34547a1 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts @@ -0,0 +1,313 @@ +/* + * Copyright 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/plugins/data/common/index_patterns/fields/fields.mocks.ts" +// but without types. +// TODO: This should move out once those mocks are directly useable or in their own package, https://github.com/elastic/kibana/issues/100715 + +export const fields = [ + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'ssl', + type: 'boolean', + esTypes: ['boolean'], + count: 20, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: '@timestamp', + type: 'date', + esTypes: ['date'], + count: 30, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'time', + type: 'date', + esTypes: ['date'], + count: 30, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: '@tags', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'utc_time', + type: 'date', + esTypes: ['date'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'phpmemory', + type: 'number', + esTypes: ['integer'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'ip', + type: 'ip', + esTypes: ['ip'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'request_body', + type: 'attachment', + esTypes: ['attachment'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'point', + type: 'geo_point', + esTypes: ['geo_point'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'area', + type: 'geo_shape', + esTypes: ['geo_shape'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'hashed', + type: 'murmur3', + esTypes: ['murmur3'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + name: 'geo.coordinates', + type: 'geo_point', + esTypes: ['geo_point'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'extension', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'machine.os', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'machine.os.raw', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { multi: { parent: 'machine.os' } }, + }, + { + name: 'geo.src', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: '_id', + type: 'string', + esTypes: ['_id'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: '_type', + type: 'string', + esTypes: ['_type'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: '_source', + type: '_source', + esTypes: ['_source'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'non-filterable', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'non-sortable', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + name: 'custom_user_field', + type: 'conflict', + esTypes: ['long', 'text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'script string', + type: 'string', + count: 0, + scripted: true, + script: "'i am a string'", + lang: 'expression', + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'script number', + type: 'number', + count: 0, + scripted: true, + script: '1234', + lang: 'expression', + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'script date', + type: 'date', + count: 0, + scripted: true, + script: '1234', + lang: 'painless', + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'script murmur3', + type: 'murmur3', + count: 0, + scripted: true, + script: '1234', + lang: 'expression', + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + name: 'nestedField.child', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'nestedField' } }, + }, + { + name: 'nestedField.nestedChild.doublyNestedChild', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'nestedField.nestedChild' } }, + }, +]; + +export const getField = (name: string) => fields.find((field) => field.name === name); diff --git a/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.test.ts new file mode 100644 index 0000000000000..1022849ffda36 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { filterFieldToList } from '.'; + +import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { getListResponseMock } from '../list_schema/index.mock'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +describe('#filterFieldToList', () => { + test('it returns empty array if given a undefined for field', () => { + const filter = filterFieldToList([], undefined); + expect(filter).toEqual([]); + }); + + test('it returns empty array if filed does not contain esTypes', () => { + const field: IFieldType = { name: 'some-name', type: 'some-type' }; + const filter = filterFieldToList([], field); + expect(filter).toEqual([]); + }); + + test('it returns single filtered list of ip_range -> ip', () => { + const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns single filtered list of ip -> ip', () => { + const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'ip' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns single filtered list of keyword -> keyword', () => { + const field: IFieldType = { esTypes: ['keyword'], name: 'some-name', type: 'keyword' }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'keyword' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns single filtered list of text -> text', () => { + const field: IFieldType = { esTypes: ['text'], name: 'some-name', type: 'text' }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'text' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns 2 filtered lists of ip_range -> ip', () => { + const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; + const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const listItem2: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const filter = filterFieldToList([listItem1, listItem2], field); + const expected: ListSchema[] = [listItem1, listItem2]; + expect(filter).toEqual(expected); + }); + + test('it returns 1 filtered lists of ip_range -> ip if the 2nd is not compatible type', () => { + const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; + const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const listItem2: ListSchema = { ...getListResponseMock(), type: 'text' }; + const filter = filterFieldToList([listItem1, listItem2], field); + const expected: ListSchema[] = [listItem1]; + expect(filter).toEqual(expected); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.ts b/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.ts new file mode 100644 index 0000000000000..b2e48c25f9b51 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/filter_field_to_list/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { typeMatch } from '../type_match'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +/** + * Given an array of lists and optionally a field this will return all + * the lists that match against the field based on the types from the field + * @param lists The lists to match against the field + * @param field The field to check against the list to see if they are compatible + */ +export const filterFieldToList = (lists: ListSchema[], field?: IFieldType): ListSchema[] => { + if (field != null) { + const { esTypes = [] } = field; + return lists.filter(({ type }) => esTypes.some((esType: string) => typeMatch(type, esType))); + } else { + return []; + } +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.test.tsx b/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.test.tsx new file mode 100644 index 0000000000000..63a94be1271a7 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.test.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { getGenericComboBoxProps } from '.'; + +describe('get_generic_combo_box_props', () => { + test('it returns empty arrays if "options" is empty array', () => { + const result = getGenericComboBoxProps({ + options: [], + selectedOptions: ['option1'], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ comboOptions: [], labels: [], selectedComboOptions: [] }); + }); + + test('it returns formatted props if "options" array is not empty', () => { + const result = getGenericComboBoxProps({ + options: ['option1', 'option2', 'option3'], + selectedOptions: [], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ + comboOptions: [ + { + label: 'option1', + }, + { + label: 'option2', + }, + { + label: 'option3', + }, + ], + labels: ['option1', 'option2', 'option3'], + selectedComboOptions: [], + }); + }); + + test('it does not return "selectedOptions" items that do not appear in "options"', () => { + const result = getGenericComboBoxProps({ + options: ['option1', 'option2', 'option3'], + selectedOptions: ['option4'], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ + comboOptions: [ + { + label: 'option1', + }, + { + label: 'option2', + }, + { + label: 'option3', + }, + ], + labels: ['option1', 'option2', 'option3'], + selectedComboOptions: [], + }); + }); + + test('it return "selectedOptions" items that do appear in "options"', () => { + const result = getGenericComboBoxProps({ + options: ['option1', 'option2', 'option3'], + selectedOptions: ['option2'], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ + comboOptions: [ + { + label: 'option1', + }, + { + label: 'option2', + }, + { + label: 'option3', + }, + ], + labels: ['option1', 'option2', 'option3'], + selectedComboOptions: [ + { + label: 'option2', + }, + ], + }); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.ts b/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.ts new file mode 100644 index 0000000000000..0fba3c39344b8 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/get_generic_combo_box_props/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { EuiComboBoxOptionOption } from '@elastic/eui'; + +export interface GetGenericComboBoxPropsReturn { + comboOptions: EuiComboBoxOptionOption[]; + labels: string[]; + selectedComboOptions: EuiComboBoxOptionOption[]; +} + +/** + * Determines the options, selected values and option labels for EUI combo box + * @param options options user can select from + * @param selectedOptions user selection if any + * @param getLabel helper function to know which property to use for labels + */ +export const getGenericComboBoxProps = ({ + getLabel, + options, + selectedOptions, +}: { + getLabel: (value: T) => string; + options: T[]; + selectedOptions: T[]; +}): GetGenericComboBoxPropsReturn => { + const newLabels = options.map(getLabel); + const newComboOptions: EuiComboBoxOptionOption[] = newLabels.map((label) => ({ label })); + const newSelectedComboOptions = selectedOptions + .map(getLabel) + .filter((option) => { + return newLabels.indexOf(option) !== -1; + }) + .map((option) => { + return newComboOptions[newLabels.indexOf(option)]; + }); + + return { + comboOptions: newComboOptions, + labels: newLabels, + selectedComboOptions: newSelectedComboOptions, + }; +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts new file mode 100644 index 0000000000000..e473df104fa6a --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + doesNotExistOperator, + EXCEPTION_OPERATORS, + existsOperator, + isNotOperator, + isOperator, +} from '@kbn/securitysolution-list-utils'; +import { getOperators } from '.'; +import { getField } from '../fields/index.mock'; + +describe('#getOperators', () => { + test('it returns "isOperator" if passed in field is "undefined"', () => { + const operator = getOperators(undefined); + + expect(operator).toEqual([isOperator]); + }); + + test('it returns expected operators when field type is "boolean"', () => { + const operator = getOperators(getField('ssl')); + + expect(operator).toEqual([isOperator, isNotOperator, existsOperator, doesNotExistOperator]); + }); + + test('it returns "isOperator" when field type is "nested"', () => { + const operator = getOperators({ + aggregatable: false, + count: 0, + esTypes: ['text'], + name: 'nestedField', + readFromDocValues: false, + scripted: false, + searchable: true, + subType: { nested: { path: 'nestedField' } }, + type: 'nested', + }); + + expect(operator).toEqual([isOperator]); + }); + + test('it returns all operator types when field type is not null, boolean, or nested', () => { + const operator = getOperators(getField('machine.os.raw')); + + expect(operator).toEqual(EXCEPTION_OPERATORS); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts new file mode 100644 index 0000000000000..39d2779e2dc44 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +import { + EXCEPTION_OPERATORS, + OperatorOption, + doesNotExistOperator, + existsOperator, + isNotOperator, + isOperator, +} from '@kbn/securitysolution-list-utils'; + +/** + * Returns the appropriate operators given a field type + * + * @param field IFieldType selected field + * + */ +export const getOperators = (field: IFieldType | undefined): OperatorOption[] => { + if (field == null) { + return [isOperator]; + } else if (field.type === 'boolean') { + return [isOperator, isNotOperator, existsOperator, doesNotExistOperator]; + } else if (field.type === 'nested') { + return [isOperator]; + } else { + return EXCEPTION_OPERATORS; + } +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/hooks/index.ts b/packages/kbn-securitysolution-autocomplete/src/hooks/index.ts new file mode 100644 index 0000000000000..cc5a37bfc46f0 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/hooks/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 './use_field_value_autocomplete'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts similarity index 92% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts rename to packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts index 0335ffa55d2a2..534daa021cf4a 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.test.ts @@ -1,28 +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; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { act, renderHook } from '@testing-library/react-hooks'; import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub'; -import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; - import { UseFieldValueAutocompleteProps, UseFieldValueAutocompleteReturn, useFieldValueAutocomplete, -} from './use_field_value_autocomplete'; - -const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); - -jest.mock('../../../../../../../../src/plugins/kibana_react/public'); - -describe('useFieldValueAutocomplete', () => { +} from '.'; +import { getField } from '../../fields/index.mock'; +import { autocompleteStartMock } from '../../autocomplete/index.mock'; + +// Copied from "src/plugins/data/common/index_patterns/index_pattern.stub.ts" +// TODO: Remove this in favor of the above if/when it is ported, https://github.com/elastic/kibana/issues/100715 +export const stubIndexPatternWithFields = { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], +}; + +describe('use_field_value_autocomplete', () => { const onErrorMock = jest.fn(); const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts similarity index 81% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts rename to packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts index 63d3925d6d64d..b4dec1615e3ed 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts @@ -1,16 +1,23 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { useEffect, useRef, useState } from 'react'; import { debounce } from 'lodash'; import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { AutocompleteStart } from '../../../../../../../../src/plugins/data/public'; -import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 +// import { AutocompleteStart } from '../../../../../../../../src/plugins/data/public'; +type AutocompleteStart = any; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +type IFieldType = any; +type IIndexPattern = any; interface FuncArgs { fieldSelected: IFieldType | undefined; @@ -33,10 +40,6 @@ export interface UseFieldValueAutocompleteProps { } /** * Hook for using the field value autocomplete service - * There is a copy within: - * x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 */ export const useFieldValueAutocomplete = ({ selectedField, diff --git a/packages/kbn-securitysolution-autocomplete/src/index.ts b/packages/kbn-securitysolution-autocomplete/src/index.ts new file mode 100644 index 0000000000000..5fcb3f954189a --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export * from './check_empty_value'; +export * from './field'; +export * from './field_value_exists'; +export * from './field_value_lists'; +export * from './field_value_match'; +export * from './field_value_match_any'; +export * from './filter_field_to_list'; +export * from './get_generic_combo_box_props'; +export * from './get_operators'; +export * from './hooks'; +export * from './operator'; +export * from './param_is_valid'; diff --git a/packages/kbn-securitysolution-autocomplete/src/list_schema/index.mock.ts b/packages/kbn-securitysolution-autocomplete/src/list_schema/index.mock.ts new file mode 100644 index 0000000000000..fb629ad2f946e --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/list_schema/index.mock.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FoundListSchema, ListSchema } from '@kbn/securitysolution-io-ts-list-types'; + +// TODO: Once this mock is available within packages, use it instead, https://github.com/elastic/kibana/issues/100715 +// import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock'; +export const getFoundListSchemaMock = (): FoundListSchema => ({ + cursor: '123', + data: [getListResponseMock()], + page: 1, + per_page: 1, + total: 1, +}); + +// TODO: Once these mocks are available from packages use it instead, https://github.com/elastic/kibana/issues/100715 +export const DATE_NOW = '2020-04-20T15:25:31.830Z'; +export const USER = 'some user'; +export const IMMUTABLE = false; +export const VERSION = 1; +export const DESCRIPTION = 'some description'; +export const TIE_BREAKER = '6a76b69d-80df-4ab2-8c3e-85f466b06a0e'; +export const LIST_ID = 'some-list-id'; +export const META = {}; +export const TYPE = 'ip'; +export const NAME = 'some name'; + +// TODO: Once this mock is available within packages, use it instead, https://github.com/elastic/kibana/issues/100715 +// import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; +export const getListResponseMock = (): ListSchema => ({ + _version: undefined, + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + deserializer: undefined, + id: LIST_ID, + immutable: IMMUTABLE, + meta: META, + name: NAME, + serializer: undefined, + tie_breaker_id: TIE_BREAKER, + type: TYPE, + updated_at: DATE_NOW, + updated_by: USER, + version: VERSION, +}); diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.test.tsx b/packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx similarity index 95% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx index dadde8800b67f..fed7007b49636 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -10,11 +11,10 @@ import { mount } from 'enzyme'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { isNotOperator, isOperator } from '@kbn/securitysolution-list-utils'; -import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; +import { OperatorComponent } from '.'; +import { getField } from '../fields/index.mock'; -import { OperatorComponent } from './operator'; - -describe('OperatorComponent', () => { +describe('operator', () => { test('it renders disabled if "isDisabled" is true', () => { const wrapper = mount( { + beforeEach(() => { + // Disable momentJS deprecation warning and it looks like it is not typed either so + // we have to disable the type as well and cannot extend it easily. + ((moment as unknown) as { + suppressDeprecationWarnings: boolean; + }).suppressDeprecationWarnings = true; + }); + + afterEach(() => { + // Re-enable momentJS deprecation warning and it looks like it is not typed either so + // we have to disable the type as well and cannot extend it easily. + ((moment as unknown) as { + suppressDeprecationWarnings: boolean; + }).suppressDeprecationWarnings = false; + }); + + test('returns no errors if no field has been selected', () => { + const isValid = paramIsValid('', undefined, true, false); + + expect(isValid).toBeUndefined(); + }); + + test('returns error string if user has touched a required input and left empty', () => { + const isValid = paramIsValid(undefined, getField('@timestamp'), true, true); + + expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); + }); + + test('returns no errors if required input is empty but user has not yet touched it', () => { + const isValid = paramIsValid(undefined, getField('@timestamp'), true, false); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if user has touched an input that is not required and left empty', () => { + const isValid = paramIsValid(undefined, getField('@timestamp'), false, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if user has touched an input that is not required and left empty string', () => { + const isValid = paramIsValid('', getField('@timestamp'), false, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if field is of type date and value is valid', () => { + const isValid = paramIsValid('1994-11-05T08:15:30-05:00', getField('@timestamp'), false, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns errors if filed is of type date and value is not valid', () => { + const isValid = paramIsValid('1593478826', getField('@timestamp'), false, true); + + expect(isValid).toEqual(i18n.DATE_ERR); + }); + + test('returns no errors if field is of type number and value is an integer', () => { + const isValid = paramIsValid('4', getField('bytes'), true, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if field is of type number and value is a float', () => { + const isValid = paramIsValid('4.3', getField('bytes'), true, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns no errors if field is of type number and value is a long', () => { + const isValid = paramIsValid('-9223372036854775808', getField('bytes'), true, true); + + expect(isValid).toBeUndefined(); + }); + + test('returns errors if field is of type number and value is "hello"', () => { + const isValid = paramIsValid('hello', getField('bytes'), true, true); + + expect(isValid).toEqual(i18n.NUMBER_ERR); + }); + + test('returns errors if field is of type number and value is "123abc"', () => { + const isValid = paramIsValid('123abc', getField('bytes'), true, true); + + expect(isValid).toEqual(i18n.NUMBER_ERR); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.ts b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/index.ts new file mode 100644 index 0000000000000..5b596b4b62408 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/param_is_valid/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 dateMath from '@elastic/datemath'; +import { checkEmptyValue } from '../check_empty_value'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/105731 +// import { IFieldType } from '../../../../../../../src/plugins/data/common'; +type IFieldType = any; + +import * as i18n from '../translations'; + +/** + * Very basic validation for values + * @param param the value being checked + * @param field the selected field + * @param isRequired whether or not an empty value is allowed + * @param touched has field been touched by user + * @returns undefined if valid, string with error message if invalid + */ +export const paramIsValid = ( + param: string | undefined, + field: IFieldType | undefined, + isRequired: boolean, + touched: boolean +): string | undefined => { + if (field == null) { + return undefined; + } + + const emptyValueError = checkEmptyValue(param, field, isRequired, touched); + if (emptyValueError !== null) { + return emptyValueError; + } + + switch (field.type) { + case 'date': + const moment = dateMath.parse(param ?? ''); + const isDate = Boolean(moment && moment.isValid()); + return isDate ? undefined : i18n.DATE_ERR; + case 'number': + const isNum = param != null && param.trim() !== '' && !isNaN(+param); + return isNum ? undefined : i18n.NUMBER_ERR; + default: + return undefined; + } +}; diff --git a/packages/kbn-securitysolution-autocomplete/src/translations/index.ts b/packages/kbn-securitysolution-autocomplete/src/translations/index.ts new file mode 100644 index 0000000000000..35d6531be51bd --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/translations/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const LOADING = i18n.translate('autocomplete.loadingDescription', { + defaultMessage: 'Loading...', +}); + +export const SELECT_FIELD_FIRST = i18n.translate('autocomplete.selectField', { + defaultMessage: 'Please select a field first...', +}); + +export const FIELD_REQUIRED_ERR = i18n.translate('autocomplete.fieldRequiredError', { + defaultMessage: 'Value cannot be empty', +}); + +export const NUMBER_ERR = i18n.translate('autocomplete.invalidNumberError', { + defaultMessage: 'Not a valid number', +}); + +export const DATE_ERR = i18n.translate('autocomplete.invalidDateError', { + defaultMessage: 'Not a valid date', +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/type_match/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/type_match/index.test.ts new file mode 100644 index 0000000000000..4694313720c79 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/type_match/index.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { typeMatch } from '.'; + +describe('type_match', () => { + test('ip -> ip is true', () => { + expect(typeMatch('ip', 'ip')).toEqual(true); + }); + + test('keyword -> keyword is true', () => { + expect(typeMatch('keyword', 'keyword')).toEqual(true); + }); + + test('text -> text is true', () => { + expect(typeMatch('text', 'text')).toEqual(true); + }); + + test('ip_range -> ip is true', () => { + expect(typeMatch('ip_range', 'ip')).toEqual(true); + }); + + test('date_range -> date is true', () => { + expect(typeMatch('date_range', 'date')).toEqual(true); + }); + + test('double_range -> double is true', () => { + expect(typeMatch('double_range', 'double')).toEqual(true); + }); + + test('float_range -> float is true', () => { + expect(typeMatch('float_range', 'float')).toEqual(true); + }); + + test('integer_range -> integer is true', () => { + expect(typeMatch('integer_range', 'integer')).toEqual(true); + }); + + test('long_range -> long is true', () => { + expect(typeMatch('long_range', 'long')).toEqual(true); + }); + + test('ip -> date is false', () => { + expect(typeMatch('ip', 'date')).toEqual(false); + }); + + test('long -> float is false', () => { + expect(typeMatch('long', 'float')).toEqual(false); + }); + + test('integer -> long is false', () => { + expect(typeMatch('integer', 'long')).toEqual(false); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/type_match/index.ts b/packages/kbn-securitysolution-autocomplete/src/type_match/index.ts new file mode 100644 index 0000000000000..d5476f3b32b49 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/type_match/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { Type } from '@kbn/securitysolution-io-ts-list-types'; + +/** + * Given an input list type and a string based ES type this will match + * if they're exact or if they are compatible with a range + * @param type The type to match against the esType + * @param esType The ES type to match with + */ +export const typeMatch = (type: Type, esType: string): boolean => { + return ( + type === esType || + (type === 'ip_range' && esType === 'ip') || + (type === 'date_range' && esType === 'date') || + (type === 'double_range' && esType === 'double') || + (type === 'float_range' && esType === 'float') || + (type === 'integer_range' && esType === 'integer') || + (type === 'long_range' && esType === 'long') + ); +}; diff --git a/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json b/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json new file mode 100644 index 0000000000000..bab7b18c59cfd --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.browser.json", + "compilerOptions": { + "allowJs": true, + "incremental": true, + "outDir": "./target_web", + "declaration": false, + "isolatedModules": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-securitysolution-autocomplete/src", + "types": [ + "jest", + "node" + ], + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + ], + "exclude": [ + "**/__fixtures__/**/*" + ] +} diff --git a/packages/kbn-securitysolution-autocomplete/tsconfig.json b/packages/kbn-securitysolution-autocomplete/tsconfig.json new file mode 100644 index 0000000000000..bf402e93ffd69 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "incremental": true, + "declarationDir": "./target_types", + "outDir": "target_node", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-autocomplete/src", + "rootDir": "src", + "types": ["jest", "node", "resize-observer-polyfill"] + }, + "include": ["src/**/*"] +} diff --git a/packages/kbn-securitysolution-list-utils/src/build_exception_filter/index.ts b/packages/kbn-securitysolution-list-utils/src/build_exception_filter/index.ts index 080bd0a311d7e..72db4991a49a4 100644 --- a/packages/kbn-securitysolution-list-utils/src/build_exception_filter/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/build_exception_filter/index.ts @@ -19,6 +19,7 @@ import { entriesMatch, entriesMatchAny, entriesNested, + OsTypeArray, } from '@kbn/securitysolution-io-ts-list-types'; import { hasLargeValueList } from '../has_large_value_list'; @@ -69,26 +70,87 @@ export const chunkExceptions = ( return chunk(chunkSize, exceptions); }; -export const buildExceptionItemFilter = ( - exceptionItem: ExceptionItemSansLargeValueLists -): BooleanFilter | NestedFilter => { - const { entries } = exceptionItem; +/** + * Transforms the os_type into a regular filter as if the user had created it + * from the fields for the next state of transforms which will create the elastic filters + * from it. + * + * Note: We use two types of fields, the "host.os.type" and "host.os.name.caseless" + * The endpoint/endgame agent has been using "host.os.name.caseless" as the same value as the ECS + * value of "host.os.type" where the auditbeat, winlogbeat, etc... (other agents) are all using + * "host.os.type". In order to be compatible with both, I create an "OR" between these two data types + * where if either has a match then we will exclude it as part of the match. This should also be + * forwards compatible for endpoints/endgame agents when/if they upgrade to using "host.os.type" + * rather than using "host.os.name.caseless" values. + * + * Also we create another "OR" from the osType names so that if there are multiples such as ['windows', 'linux'] + * this will exclude anything with either 'windows' or with 'linux' + * @param osTypes The os_type array from the REST interface that is an array such as ['windows', 'linux'] + * @param entries The entries to join the OR's with before the elastic filter change out + */ +export const transformOsType = ( + osTypes: OsTypeArray, + entries: NonListEntry[] +): NonListEntry[][] => { + const hostTypeTransformed = osTypes.map((osType) => { + return [ + { field: 'host.os.type', operator: 'included', type: 'match', value: osType }, + ...entries, + ]; + }); + const caseLessTransformed = osTypes.map((osType) => { + return [ + { field: 'host.os.name.caseless', operator: 'included', type: 'match', value: osType }, + ...entries, + ]; + }); + return [...hostTypeTransformed, ...caseLessTransformed]; +}; - if (entries.length === 1) { - return createInnerAndClauses(entries[0]); - } else { +/** + * This builds an exception item filter with the os type + * @param osTypes The os_type array from the REST interface that is an array such as ['windows', 'linux'] + * @param entries The entries to join the OR's with before the elastic filter change out + */ +export const buildExceptionItemFilterWithOsType = ( + osTypes: OsTypeArray, + entries: NonListEntry[] +): BooleanFilter[] => { + const entriesWithOsTypes = transformOsType(osTypes, entries); + return entriesWithOsTypes.map((entryWithOsType) => { return { bool: { - filter: entries.map((entry) => createInnerAndClauses(entry)), + filter: entryWithOsType.map((entry) => createInnerAndClauses(entry)), }, }; + }); +}; + +export const buildExceptionItemFilter = ( + exceptionItem: ExceptionItemSansLargeValueLists +): Array => { + const { entries, os_types: osTypes } = exceptionItem; + if (osTypes != null && osTypes.length > 0) { + return buildExceptionItemFilterWithOsType(osTypes, entries); + } else { + if (entries.length === 1) { + return [createInnerAndClauses(entries[0])]; + } else { + return [ + { + bool: { + filter: entries.map((entry) => createInnerAndClauses(entry)), + }, + }, + ]; + } } }; export const createOrClauses = ( exceptionItems: ExceptionItemSansLargeValueLists[] ): Array => { - return exceptionItems.map((exceptionItem) => buildExceptionItemFilter(exceptionItem)); + return exceptionItems.flatMap((exceptionItem) => buildExceptionItemFilter(exceptionItem)); }; export const buildExceptionFilter = ({ diff --git a/packages/kbn-test/src/jest/utils/get_url.ts b/packages/kbn-test/src/jest/utils/get_url.ts index 734e26c5199d7..e08695b334e1b 100644 --- a/packages/kbn-test/src/jest/utils/get_url.ts +++ b/packages/kbn-test/src/jest/utils/get_url.ts @@ -22,11 +22,6 @@ interface UrlParam { username?: string; } -interface App { - pathname?: string; - hash?: string; -} - /** * Converts a config and a pathname to a url * @param {object} config A url config @@ -46,11 +41,11 @@ interface App { * @return {string} */ -function getUrl(config: UrlParam, app: App) { +function getUrl(config: UrlParam, app: UrlParam) { return url.format(_.assign({}, config, app)); } -getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: App) { +getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: UrlParam) { config = _.pickBy(config, function (val, param) { return param !== 'auth'; }); diff --git a/packages/kbn-ui-shared-deps/BUILD.bazel b/packages/kbn-ui-shared-deps/BUILD.bazel index f92049292f373..426f8d0b7485a 100644 --- a/packages/kbn-ui-shared-deps/BUILD.bazel +++ b/packages/kbn-ui-shared-deps/BUILD.bazel @@ -44,9 +44,7 @@ SRC_DEPS = [ "@npm//abortcontroller-polyfill", "@npm//angular", "@npm//babel-loader", - "@npm//compression-webpack-plugin", "@npm//core-js", - "@npm//css-minimizer-webpack-plugin", "@npm//css-loader", "@npm//fflate", "@npm//jquery", @@ -67,7 +65,6 @@ SRC_DEPS = [ "@npm//rxjs", "@npm//styled-components", "@npm//symbol-observable", - "@npm//terser-webpack-plugin", "@npm//url-loader", "@npm//val-loader", "@npm//whatwg-fetch" diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index 9d18c8033ff67..9692f768cb3b4 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -7,20 +7,23 @@ */ const Path = require('path'); - const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); -const TerserPlugin = require('terser-webpack-plugin'); -const CompressionPlugin = require('compression-webpack-plugin'); const { REPO_ROOT } = require('@kbn/utils'); -const { RawSource } = require('webpack-sources'); const UiSharedDeps = require('./src/index'); const MOMENT_SRC = require.resolve('moment/min/moment-with-locales.js'); +const WEBPACK_SRC = require.resolve('webpack'); module.exports = { + node: { + child_process: 'empty', + fs: 'empty', + }, + externals: { + module: 'module', + }, mode: 'production', entry: { 'kbn-ui-shared-deps': './src/entry.js', @@ -30,8 +33,7 @@ module.exports = { 'kbn-ui-shared-deps.v8.light': ['@elastic/eui/dist/eui_theme_amsterdam_light.css'], }, context: __dirname, - // cheap-source-map should be used if needed - devtool: false, + devtool: 'cheap-source-map', output: { path: UiSharedDeps.distDir, filename: '[name].js', @@ -39,10 +41,11 @@ module.exports = { devtoolModuleFilenameTemplate: (info) => `kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`, library: '__kbnSharedDeps__', + futureEmitAssets: true, }, module: { - noParse: [MOMENT_SRC], + noParse: [MOMENT_SRC, WEBPACK_SRC], rules: [ { include: [require.resolve('./src/entry.js')], @@ -102,35 +105,17 @@ module.exports = { resolve: { alias: { moment: MOMENT_SRC, + // NOTE: Used to include react profiling on bundles + // https://gist.github.com/bvaughn/25e6233aeb1b4f0cdb8d8366e54a3977#webpack-4 + 'react-dom$': 'react-dom/profiling', + 'scheduler/tracing': 'scheduler/tracing-profiling', }, extensions: ['.js', '.ts'], symlinks: false, }, optimization: { - minimizer: [ - new CssMinimizerPlugin({ - parallel: false, - minimizerOptions: { - preset: [ - 'default', - { - discardComments: false, - }, - ], - }, - }), - new TerserPlugin({ - cache: false, - sourceMap: false, - extractComments: false, - parallel: false, - terserOptions: { - compress: true, - mangle: true, - }, - }), - ], + minimize: false, noEmitOnErrors: true, splitChunks: { cacheGroups: { @@ -155,44 +140,5 @@ module.exports = { new MiniCssExtractPlugin({ filename: '[name].css', }), - new CompressionPlugin({ - algorithm: 'brotliCompress', - filename: '[path].br', - test: /\.(js|css)$/, - cache: false, - }), - new CompressionPlugin({ - algorithm: 'gzip', - filename: '[path].gz', - test: /\.(js|css)$/, - cache: false, - }), - new (class MetricsPlugin { - apply(compiler) { - compiler.hooks.emit.tap('MetricsPlugin', (compilation) => { - const metrics = [ - { - group: 'page load bundle size', - id: 'kbnUiSharedDeps-js', - value: compilation.assets['kbn-ui-shared-deps.js'].size(), - }, - { - group: 'page load bundle size', - id: 'kbnUiSharedDeps-css', - value: - compilation.assets['kbn-ui-shared-deps.css'].size() + - compilation.assets['kbn-ui-shared-deps.v7.light.css'].size(), - }, - { - group: 'page load bundle size', - id: 'kbnUiSharedDeps-elastic', - value: compilation.assets['kbn-ui-shared-deps.@elastic.js'].size(), - }, - ]; - - compilation.emitAsset('metrics.json', new RawSource(JSON.stringify(metrics, null, 2))); - }); - } - })(), ], }; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index e8453d009e720..7152c7eb3cb1b 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -242,6 +242,7 @@ export class DocLinksService { anomalyDetectionJobResource: `${ELASTICSEARCH_DOCS}ml-put-job.html#ml-put-job-path-parms`, anomalyDetectionJobResourceAnalysisConfig: `${ELASTICSEARCH_DOCS}ml-put-job.html#put-analysisconfig`, anomalyDetectionJobTips: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html#ml-ad-job-tips`, + alertingRules: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-alerts.html`, anomalyDetectionModelMemoryLimits: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html#ml-ad-model-memory-limits`, calendars: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html#ml-ad-calendars`, classificationEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-classification.html#ml-dfanalytics-classification-evaluation`, diff --git a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap index 16504cf97366e..95f5d1953b761 100644 --- a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap +++ b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap @@ -93,8 +93,8 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiDataGridSchema.booleanSortTextDesc": "True-False", "euiDataGridSchema.currencySortTextAsc": "Low-High", "euiDataGridSchema.currencySortTextDesc": "High-Low", - "euiDataGridSchema.dateSortTextAsc": "New-Old", - "euiDataGridSchema.dateSortTextDesc": "Old-New", + "euiDataGridSchema.dateSortTextAsc": "Old-New", + "euiDataGridSchema.dateSortTextDesc": "New-Old", "euiDataGridSchema.jsonSortTextAsc": "Small-Large", "euiDataGridSchema.jsonSortTextDesc": "Large-Small", "euiDataGridSchema.numberSortTextAsc": "Low-High", @@ -180,11 +180,11 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiSaturation.roleDescription": "HSV color mode saturation and value selection", "euiSaturation.screenReaderAnnouncement": "Use the arrow keys to navigate the square color gradient. The coordinates resulting from each key press will be used to calculate HSV color mode \\"saturation\\" and \\"value\\" numbers, in the range of 0 to 1. Left and right decrease and increase (respectively) the \\"saturation\\" value. Up and down decrease and increase (respectively) the \\"value\\" value.", "euiSelectable.loadingOptions": "Loading options", - "euiSelectable.noAvailableOptions": "There aren't any options available", + "euiSelectable.noAvailableOptions": "No options available", "euiSelectable.noMatchingOptions": [Function], "euiSelectable.placeholderName": "Filter options", "euiSelectableListItem.excludedOption": "Excluded option.", - "euiSelectableListItem.excludedOptionInstructions": "To deselect this option, press enter", + "euiSelectableListItem.excludedOptionInstructions": "To deselect this option, press enter.", "euiSelectableListItem.includedOption": "Included option.", "euiSelectableListItem.includedOptionInstructions": "To exclude this option, press enter.", "euiSelectableTemplateSitewide.loadingResults": "Loading results", diff --git a/src/core/public/i18n/i18n_eui_mapping.test.ts b/src/core/public/i18n/i18n_eui_mapping.test.ts new file mode 100644 index 0000000000000..1b80257266d4c --- /dev/null +++ b/src/core/public/i18n/i18n_eui_mapping.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +jest.mock('@kbn/i18n'); + +import { i18n } from '@kbn/i18n'; + +import i18ntokens from '@elastic/eui/i18ntokens.json'; +import { getEuiContextMapping } from './i18n_eui_mapping'; + +/** Regexp to find {values} usage */ +const VALUES_REGEXP = /\{\w+\}/; + +describe('@elastic/eui i18n tokens', () => { + const i18nTranslateMock = jest + .fn() + .mockImplementation((id, { defaultMessage }) => defaultMessage); + i18n.translate = i18nTranslateMock; + + const euiContextMapping = getEuiContextMapping(); + + test('all tokens are mapped', () => { + // Extract the tokens from the EUI library: We need to uniq them because they might be duplicated + const euiTokensFromLib = [...new Set(i18ntokens.map(({ token }) => token))]; + const euiTokensFromMapping = Object.keys(euiContextMapping); + + expect(euiTokensFromMapping.sort()).toStrictEqual(euiTokensFromLib.sort()); + }); + + test('tokens that include {word} should be mapped to functions', () => { + const euiTokensFromLibWithValues = i18ntokens.filter(({ defString }) => + VALUES_REGEXP.test(defString) + ); + const euiTokensFromLib = [...new Set(euiTokensFromLibWithValues.map(({ token }) => token))]; + const euiTokensFromMapping = Object.entries(euiContextMapping) + .filter(([, value]) => typeof value === 'function') + .map(([key]) => key); + + expect(euiTokensFromMapping.sort()).toStrictEqual(euiTokensFromLib.sort()); + }); + + i18ntokens.forEach(({ token, defString }) => { + describe(`Token "${token}"`, () => { + let i18nTranslateCall: [ + string, + { defaultMessage: string; values?: object; description?: string } + ]; + + beforeAll(() => { + // If it's a function, call it, so we have the mock to register the call. + const entry = euiContextMapping[token as keyof typeof euiContextMapping]; + const translationOutput = typeof entry === 'function' ? entry({}) : entry; + + // If it's a string, it comes from i18n.translate call + if (typeof translationOutput === 'string') { + // find the call in the mocks + i18nTranslateCall = i18nTranslateMock.mock.calls.find( + ([kbnToken]) => kbnToken === `core.${token}` + ); + } else { + // Otherwise, it's a fn returning `FormattedMessage` component => read the props + const { id, defaultMessage, values } = translationOutput.props; + i18nTranslateCall = [id, { defaultMessage, values }]; + } + }); + + test('a translation should be registered as `core.{TOKEN}`', () => { + expect(i18nTranslateCall).not.toBeUndefined(); + }); + + test('defaultMessage is in sync with defString', () => { + // Clean up typical errors from the `@elastic/eui` extraction token tool + const normalizedDefString = defString + // Quoted words should use double-quotes + .replace(/\s'/g, ' "') + .replace(/'\s/g, '" ') + // Should not include break-lines + .replace(/\n/g, '') + // Should trim extra spaces + .replace(/\s{2,}/g, ' ') + .trim(); + + expect(i18nTranslateCall[1].defaultMessage).toBe(normalizedDefString); + }); + + test('values should match', () => { + const valuesFromEuiLib = defString.match(new RegExp(VALUES_REGEXP, 'g')) || []; + const receivedValuesInMock = Object.keys(i18nTranslateCall[1].values ?? {}).map( + (key) => `{${key}}` + ); + expect(receivedValuesInMock.sort()).toStrictEqual(valuesFromEuiLib.sort()); + }); + }); + }); +}); diff --git a/src/core/public/i18n/i18n_eui_mapping.tsx b/src/core/public/i18n/i18n_eui_mapping.tsx index b7fbf8f91cc4e..beed0deced35c 100644 --- a/src/core/public/i18n/i18n_eui_mapping.tsx +++ b/src/core/public/i18n/i18n_eui_mapping.tsx @@ -15,8 +15,8 @@ interface EuiValues { [key: string]: any; } -export const getEuiContextMapping = () => { - const euiContextMapping: EuiTokensObject = { +export const getEuiContextMapping = (): EuiTokensObject => { + return { 'euiAccordion.isLoading': i18n.translate('core.euiAccordion.isLoading', { defaultMessage: 'Loading', }), @@ -40,7 +40,7 @@ export const getEuiContextMapping = () => { page, pageCount, }: EuiValues) => - i18n.translate('core.euiBasicTable.tableDescriptionWithoutPagination', { + i18n.translate('core.euiBasicTable.tableAutoCaptionWithPagination', { defaultMessage: 'This table contains {itemCount} rows out of {totalItemCount} rows; Page {page} of {pageCount}.', values: { itemCount, totalItemCount, page, pageCount }, @@ -219,6 +219,9 @@ export const getEuiContextMapping = () => { description: 'Screen reader text to describe the composite behavior of the color stops component.', }), + 'euiColumnActions.hideColumn': i18n.translate('core.euiColumnActions.hideColumn', { + defaultMessage: 'Hide column', + }), 'euiColumnActions.sort': ({ schemaLabel }: EuiValues) => i18n.translate('core.euiColumnActions.sort', { defaultMessage: 'Sort {schemaLabel}', @@ -230,9 +233,6 @@ export const getEuiContextMapping = () => { 'euiColumnActions.moveRight': i18n.translate('core.euiColumnActions.moveRight', { defaultMessage: 'Move right', }), - 'euiColumnActions.hideColumn': i18n.translate('core.euiColumnActions.hideColumn', { - defaultMessage: 'Hide column', - }), 'euiColumnSelector.hideAll': i18n.translate('core.euiColumnSelector.hideAll', { defaultMessage: 'Hide all', }), @@ -369,12 +369,6 @@ export const getEuiContextMapping = () => { 'euiControlBar.screenReaderHeading': i18n.translate('core.euiControlBar.screenReaderHeading', { defaultMessage: 'Page level controls', }), - 'euiControlBar.customScreenReaderAnnouncement': ({ landmarkHeading }: EuiValues) => - i18n.translate('core.euiControlBar.customScreenReaderAnnouncement', { - defaultMessage: - 'There is a new region landmark called {landmarkHeading} with page level controls at the end of the document.', - values: { landmarkHeading }, - }), 'euiControlBar.screenReaderAnnouncement': i18n.translate( 'core.euiControlBar.screenReaderAnnouncement', { @@ -382,6 +376,12 @@ export const getEuiContextMapping = () => { 'There is a new region landmark with page level controls at the end of the document.', } ), + 'euiControlBar.customScreenReaderAnnouncement': ({ landmarkHeading }: EuiValues) => + i18n.translate('core.euiControlBar.customScreenReaderAnnouncement', { + defaultMessage: + 'There is a new region landmark called {landmarkHeading} with page level controls at the end of the document.', + values: { landmarkHeading }, + }), 'euiDataGrid.screenReaderNotice': i18n.translate('core.euiDataGrid.screenReaderNotice', { defaultMessage: 'Cell contains interactive content.', }), @@ -466,13 +466,13 @@ export const getEuiContextMapping = () => { } ), 'euiDataGridSchema.dateSortTextAsc': i18n.translate('core.euiDataGridSchema.dateSortTextAsc', { - defaultMessage: 'New-Old', + defaultMessage: 'Old-New', description: 'Ascending date label', }), 'euiDataGridSchema.dateSortTextDesc': i18n.translate( 'core.euiDataGridSchema.dateSortTextDesc', { - defaultMessage: 'Old-New', + defaultMessage: 'New-Old', description: 'Descending date label', } ), @@ -519,8 +519,8 @@ export const getEuiContextMapping = () => { }), 'euiFilterButton.filterBadge': ({ count, hasActiveFilters }: EuiValues) => i18n.translate('core.euiFilterButton.filterBadge', { - defaultMessage: '${count} ${filterCountLabel} filters', - values: { count, filterCountLabel: hasActiveFilters ? 'active' : 'available' }, + defaultMessage: '{count} {hasActiveFilters} filters', + values: { count, hasActiveFilters: hasActiveFilters ? 'active' : 'available' }, }), 'euiFlyout.closeAriaLabel': i18n.translate('core.euiFlyout.closeAriaLabel', { defaultMessage: 'Close this dialog', @@ -642,19 +642,19 @@ export const getEuiContextMapping = () => { 'euiModal.closeModal': i18n.translate('core.euiModal.closeModal', { defaultMessage: 'Closes this modal window', }), - 'euiNotificationEventMessages.accordionButtonText': ({ + 'euiNotificationEventMessages.accordionButtonText': ({ messagesLength }: EuiValues) => + i18n.translate('core.euiNotificationEventMessages.accordionButtonText', { + defaultMessage: '+ {messagesLength} more', + values: { messagesLength }, + }), + 'euiNotificationEventMessages.accordionAriaLabelButtonText': ({ messagesLength, eventName, }: EuiValues) => - i18n.translate('core.euiNotificationEventMessages.accordionButtonText', { + i18n.translate('core.euiNotificationEventMessages.accordionAriaLabelButtonText', { defaultMessage: '+ {messagesLength} messages for {eventName}', values: { messagesLength, eventName }, }), - 'euiNotificationEventMessages.accordionAriaLabelButtonText': ({ messagesLength }: EuiValues) => - i18n.translate('core.euiNotificationEventMessages.accordionAriaLabelButtonText', { - defaultMessage: '+ {messagesLength} more', - values: { messagesLength }, - }), 'euiNotificationEventMeta.contextMenuButton': ({ eventName }: EuiValues) => i18n.translate('core.euiNotificationEventMeta.contextMenuButton', { defaultMessage: 'Menu for {eventName}', @@ -682,25 +682,6 @@ export const getEuiContextMapping = () => { defaultMessage: 'Mark as unread', } ), - 'euiNotificationEventReadIcon.readAria': ({ eventName }: EuiValues) => - i18n.translate('core.euiNotificationEventReadIcon.readAria', { - defaultMessage: '{eventName} is read', - values: { eventName }, - }), - 'euiNotificationEventReadIcon.unreadAria': ({ eventName }: EuiValues) => - i18n.translate('core.euiNotificationEventReadIcon.unreadAria', { - defaultMessage: '{eventName} is unread', - values: { eventName }, - }), - 'euiNotificationEventReadIcon.read': i18n.translate('core.euiNotificationEventReadIcon.read', { - defaultMessage: 'Read', - }), - 'euiNotificationEventReadIcon.unread': i18n.translate( - 'core.euiNotificationEventReadIcon.unread', - { - defaultMessage: 'Unread', - } - ), 'euiNotificationEventMessages.accordionHideText': i18n.translate( 'core.euiNotificationEventMessages.accordionHideText', { @@ -712,13 +693,11 @@ export const getEuiContextMapping = () => { defaultMessage: 'Next page, {page}', values: { page }, }), - 'euiPagination.pageOfTotalCompressed': ({ page, total }: EuiValues) => ( - - ), + 'euiPagination.pageOfTotalCompressed': ({ page, total }: EuiValues) => + i18n.translate('core.euiPagination.pageOfTotalCompressed', { + defaultMessage: '{page} of {total}', + values: { page, total }, + }), 'euiPagination.previousPage': ({ page }: EuiValues) => i18n.translate('core.euiPagination.previousPage', { defaultMessage: 'Previous page, {page}', @@ -881,7 +860,7 @@ export const getEuiContextMapping = () => { description: 'Placeholder message while data is asynchronously loaded', }), 'euiSelectable.noAvailableOptions': i18n.translate('core.euiSelectable.noAvailableOptions', { - defaultMessage: "There aren't any options available", + defaultMessage: 'No options available', }), 'euiSelectable.noMatchingOptions': ({ searchValue }: EuiValues) => ( { 'euiSelectableListItem.excludedOptionInstructions': i18n.translate( 'core.euiSelectableListItem.excludedOptionInstructions', { - defaultMessage: 'To deselect this option, press enter', + defaultMessage: 'To deselect this option, press enter.', } ), 'euiSelectableTemplateSitewide.loadingResults': i18n.translate( @@ -1039,7 +1018,7 @@ export const getEuiContextMapping = () => { 'euiSuperSelect.screenReaderAnnouncement': ({ optionsCount }: EuiValues) => i18n.translate('core.euiSuperSelect.screenReaderAnnouncement', { defaultMessage: - 'You are in a form selector of {optionsCount} items and must select a single option. Use the Up and Down keys to navigate or Escape to close.', + 'You are in a form selector of {optionsCount} items and must select a single option. Use the up and down keys to navigate or escape to close.', values: { optionsCount }, }), 'euiSuperSelectControl.selectAnOption': ({ selectedValue }: EuiValues) => @@ -1086,6 +1065,7 @@ export const getEuiContextMapping = () => { i18n.translate('core.euiTableHeaderCell.titleTextWithDesc', { defaultMessage: '{innerText}; {description}', values: { innerText, description }, + description: 'Displayed in a cell in the header of the table to describe the field', }), 'euiTablePagination.rowsPerPage': i18n.translate('core.euiTablePagination.rowsPerPage', { defaultMessage: 'Rows per page', @@ -1111,6 +1091,15 @@ export const getEuiContextMapping = () => { defaultMessage: 'Notification', description: 'ARIA label on an element containing a notification', }), + 'euiTourStep.endTour': i18n.translate('core.euiTourStep.endTour', { + defaultMessage: 'End tour', + }), + 'euiTourStep.skipTour': i18n.translate('core.euiTourStep.skipTour', { + defaultMessage: 'Skip tour', + }), + 'euiTourStep.closeTour': i18n.translate('core.euiTourStep.closeTour', { + defaultMessage: 'Close tour', + }), 'euiTourStepIndicator.isActive': i18n.translate('core.euiTourStepIndicator.isActive', { defaultMessage: 'active', description: 'Text for an active tour step', @@ -1123,15 +1112,6 @@ export const getEuiContextMapping = () => { defaultMessage: 'incomplete', description: 'Text for an incomplete tour step', }), - 'euiTourStep.endTour': i18n.translate('core.euiTourStep.endTour', { - defaultMessage: 'End tour', - }), - 'euiTourStep.skipTour': i18n.translate('core.euiTourStep.skipTour', { - defaultMessage: 'Skip tour', - }), - 'euiTourStep.closeTour': i18n.translate('core.euiTourStep.closeTour', { - defaultMessage: 'Close tour', - }), 'euiTourStepIndicator.ariaLabel': ({ status, number }: EuiValues) => i18n.translate('core.euiTourStepIndicator.ariaLabel', { defaultMessage: 'Step {number} {status}', @@ -1149,7 +1129,24 @@ export const getEuiContextMapping = () => { defaultMessage: 'You can quickly navigate this list using arrow keys.', } ), + 'euiNotificationEventReadIcon.read': i18n.translate('core.euiNotificationEventReadIcon.read', { + defaultMessage: 'Read', + }), + 'euiNotificationEventReadIcon.readAria': ({ eventName }: EuiValues) => + i18n.translate('core.euiNotificationEventReadIcon.readAria', { + defaultMessage: '{eventName} is read', + values: { eventName }, + }), + 'euiNotificationEventReadIcon.unread': i18n.translate( + 'core.euiNotificationEventReadIcon.unread', + { + defaultMessage: 'Unread', + } + ), + 'euiNotificationEventReadIcon.unreadAria': ({ eventName }: EuiValues) => + i18n.translate('core.euiNotificationEventReadIcon.unreadAria', { + defaultMessage: '{eventName} is unread', + values: { eventName }, + }), }; - - return euiContextMapping; }; diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index 2395d6d1c1725..7ecfa37492242 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -122,6 +122,7 @@ describe('CoreUsageDataService', () => { hidden: true, namespaceType: 'agnostic', mappings: expect.anything(), + migrations: expect.anything(), }); }); }); diff --git a/src/core/server/core_usage_data/core_usage_stats.ts b/src/core/server/core_usage_data/core_usage_stats.ts index d02c92353d13e..8cdd83bcad2c9 100644 --- a/src/core/server/core_usage_data/core_usage_stats.ts +++ b/src/core/server/core_usage_data/core_usage_stats.ts @@ -8,6 +8,7 @@ import { SavedObjectsType } from '../saved_objects'; import { CORE_USAGE_STATS_TYPE } from './constants'; +import { migrateTo7141 } from './migrations'; /** @internal */ export const coreUsageStatsType: SavedObjectsType = { @@ -18,4 +19,7 @@ export const coreUsageStatsType: SavedObjectsType = { dynamic: false, // we aren't querying or aggregating over this data, so we don't need to specify any fields properties: {}, }, + migrations: { + '7.14.1': migrateTo7141, + }, }; diff --git a/src/core/server/core_usage_data/core_usage_stats_client.test.ts b/src/core/server/core_usage_data/core_usage_stats_client.test.ts index dc4a81adf5f8e..384e3d7b932c1 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.test.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.test.ts @@ -790,8 +790,14 @@ describe('CoreUsageStatsClient', () => { createNewCopies: true, overwrite: true, } as IncrementSavedObjectsImportOptions); - expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); - expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + await usageStatsClient.incrementSavedObjectsImport({ + request, + createNewCopies: false, + overwrite: true, + } as IncrementSavedObjectsImportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(2); + expect(repositoryMock.incrementCounter).toHaveBeenNthCalledWith( + 1, CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ @@ -799,6 +805,19 @@ describe('CoreUsageStatsClient', () => { `${IMPORT_STATS_PREFIX}.namespace.default.total`, `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, + // excludes 'overwriteEnabled.yes' and 'overwriteEnabled.no' when createNewCopies is true + ], + incrementOptions + ); + expect(repositoryMock.incrementCounter).toHaveBeenNthCalledWith( + 2, + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${IMPORT_STATS_PREFIX}.total`, + `${IMPORT_STATS_PREFIX}.namespace.default.total`, + `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, `${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`, ], incrementOptions diff --git a/src/core/server/core_usage_data/core_usage_stats_client.ts b/src/core/server/core_usage_data/core_usage_stats_client.ts index 3b73b475a30f4..29d6e875c7962 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.ts @@ -150,7 +150,7 @@ export class CoreUsageStatsClient { const { createNewCopies, overwrite } = options; const counterFieldNames = [ `createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`, - `overwriteEnabled.${overwrite ? 'yes' : 'no'}`, + ...(!createNewCopies ? [`overwriteEnabled.${overwrite ? 'yes' : 'no'}`] : []), // the overwrite option is ignored when createNewCopies is true ]; await this.updateUsageStats(counterFieldNames, IMPORT_STATS_PREFIX, options); } diff --git a/src/core/server/core_usage_data/migrations.test.ts b/src/core/server/core_usage_data/migrations.test.ts new file mode 100644 index 0000000000000..27ea745c3fab1 --- /dev/null +++ b/src/core/server/core_usage_data/migrations.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { SavedObjectUnsanitizedDoc } from '../saved_objects'; +import { migrateTo7141 } from './migrations'; +import type { CoreUsageStats } from './types'; + +const type = 'obj-type'; +const id = 'obj-id'; + +describe('#migrateTo7141', () => { + it('Resets targeted counter fields and leaves others unchanged', () => { + const doc = { + type, + id, + attributes: { + foo: 'bar', + 'apiCalls.savedObjectsImport.total': 10, + }, + } as SavedObjectUnsanitizedDoc; + + expect(migrateTo7141(doc)).toEqual({ + type, + id, + attributes: { + foo: 'bar', + 'apiCalls.savedObjectsImport.total': 0, + 'apiCalls.savedObjectsImport.namespace.default.total': 0, + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.yes': 0, + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.no': 0, + 'apiCalls.savedObjectsImport.namespace.custom.total': 0, + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.yes': 0, + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.no': 0, + 'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes': 0, + 'apiCalls.savedObjectsImport.createNewCopiesEnabled.no': 0, + 'apiCalls.savedObjectsImport.overwriteEnabled.yes': 0, + 'apiCalls.savedObjectsImport.overwriteEnabled.no': 0, + }, + }); + }); +}); diff --git a/src/core/server/core_usage_data/migrations.ts b/src/core/server/core_usage_data/migrations.ts new file mode 100644 index 0000000000000..8cbb2a267f0c7 --- /dev/null +++ b/src/core/server/core_usage_data/migrations.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { cloneDeep } from 'lodash'; +import type { SavedObjectUnsanitizedDoc } from '../saved_objects'; +import type { CoreUsageStats } from './types'; + +export const migrateTo7141 = (doc: SavedObjectUnsanitizedDoc) => { + try { + return resetFields(doc, [ + // Prior to this, we were counting the `overwrite` option incorrectly; reset all import API counter fields so we get clean data + 'apiCalls.savedObjectsImport.total', + 'apiCalls.savedObjectsImport.namespace.default.total', + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.yes', + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.no', + 'apiCalls.savedObjectsImport.namespace.custom.total', + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.yes', + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.no', + 'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes', + 'apiCalls.savedObjectsImport.createNewCopiesEnabled.no', + 'apiCalls.savedObjectsImport.overwriteEnabled.yes', + 'apiCalls.savedObjectsImport.overwriteEnabled.no', + ]); + } catch (err) { + // fail-safe + } + return doc; +}; + +function resetFields( + doc: SavedObjectUnsanitizedDoc, + fieldsToReset: Array +) { + const newDoc = cloneDeep(doc); + const { attributes = {} } = newDoc; + for (const field of fieldsToReset) { + attributes[field] = 0; + } + return { ...newDoc, attributes }; +} diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 27228361aef22..474721ff3610a 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -33,7 +33,6 @@ import { errors as EsErrors } from '@elastic/elasticsearch'; const { nodeTypes } = esKuery; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); - // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. diff --git a/src/core/server/status/get_summary_status.test.ts b/src/core/server/status/get_summary_status.test.ts index 0aee718d333cd..33b2e6f7913a1 100644 --- a/src/core/server/status/get_summary_status.test.ts +++ b/src/core/server/status/get_summary_status.test.ts @@ -101,15 +101,7 @@ describe('getSummaryStatus', () => { summary: '[s2]: Lorem ipsum', detail: 'See the status page for more information', meta: { - affectedServices: { - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - meta: { - custom: { data: 'here' }, - }, - }, - }, + affectedServices: ['s2'], }, }); }); @@ -136,17 +128,7 @@ describe('getSummaryStatus', () => { detail: 'Vivamus pulvinar sem ac luctus ultrices.', documentationUrl: 'http://helpmenow.com/problem1', meta: { - affectedServices: { - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - detail: 'Vivamus pulvinar sem ac luctus ultrices.', - documentationUrl: 'http://helpmenow.com/problem1', - meta: { - custom: { data: 'here' }, - }, - }, - }, + affectedServices: ['s2'], }, }); }); @@ -183,26 +165,7 @@ describe('getSummaryStatus', () => { summary: '[2] services are unavailable', detail: 'See the status page for more information', meta: { - affectedServices: { - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - detail: 'Vivamus pulvinar sem ac luctus ultrices.', - documentationUrl: 'http://helpmenow.com/problem1', - meta: { - custom: { data: 'here' }, - }, - }, - s3: { - level: ServiceStatusLevels.unavailable, - summary: 'Proin mattis', - detail: 'Nunc quis nulla at mi lobortis pretium.', - documentationUrl: 'http://helpmenow.com/problem2', - meta: { - other: { data: 'over there' }, - }, - }, - }, + affectedServices: ['s2', 's3'], }, }); }); diff --git a/src/core/server/status/get_summary_status.ts b/src/core/server/status/get_summary_status.ts index 627319d3cd433..9124023148dd1 100644 --- a/src/core/server/status/get_summary_status.ts +++ b/src/core/server/status/get_summary_status.ts @@ -31,7 +31,7 @@ export const getSummaryStatus = ( // TODO: include URL to status page detail: status.detail ?? `See the status page for more information`, meta: { - affectedServices: { [serviceName]: status }, + affectedServices: [serviceName], }, }; } else { @@ -41,7 +41,7 @@ export const getSummaryStatus = ( // TODO: include URL to status page detail: `See the status page for more information`, meta: { - affectedServices: Object.fromEntries(highestStatuses), + affectedServices: highestStatuses.map(([serviceName]) => serviceName), }, }; } diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts index 9dc1ddcddca3e..a6579069acbc0 100644 --- a/src/core/server/status/plugins_status.test.ts +++ b/src/core/server/status/plugins_status.test.ts @@ -303,12 +303,7 @@ describe('PluginStatusService', () => { summary: '[a]: Status check timed out after 30s', detail: 'See the status page for more information', meta: { - affectedServices: { - a: { - level: ServiceStatusLevels.unavailable, - summary: 'Status check timed out after 30s', - }, - }, + affectedServices: ['a'], }, }, }); diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index ed52c35d1becb..4ead81a6638dd 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -254,12 +254,9 @@ describe('StatusService', () => { "detail": "See the status page for more information", "level": degraded, "meta": Object { - "affectedServices": Object { - "savedObjects": Object { - "level": degraded, - "summary": "This is degraded!", - }, - }, + "affectedServices": Array [ + "savedObjects", + ], }, "summary": "[savedObjects]: This is degraded!", }, @@ -307,12 +304,9 @@ describe('StatusService', () => { "detail": "See the status page for more information", "level": degraded, "meta": Object { - "affectedServices": Object { - "savedObjects": Object { - "level": degraded, - "summary": "This is degraded!", - }, - }, + "affectedServices": Array [ + "savedObjects", + ], }, "summary": "[savedObjects]: This is degraded!", }, diff --git a/src/core/types/elasticsearch/search.ts b/src/core/types/elasticsearch/search.ts index 0960fb189a341..e8ce9f98501f9 100644 --- a/src/core/types/elasticsearch/search.ts +++ b/src/core/types/elasticsearch/search.ts @@ -39,8 +39,8 @@ type Source = estypes.SearchSourceFilter | boolean | estypes.Fields; type ValueTypeOfField = T extends Record ? ValuesType - : T extends string[] | number[] - ? ValueTypeOfField> + : T extends Array + ? ValueTypeOfField : T extends { field: estypes.Field } ? T['field'] : T extends string | number diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 159281ed71db0..b243bb10e507b 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -64,6 +64,7 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions await run(Tasks.TranspileBabel); await run(Tasks.CreatePackageJson); await run(Tasks.InstallDependencies); + await run(Tasks.GeneratePackagesOptimizedAssets); await run(Tasks.CleanPackages); await run(Tasks.CreateNoticeFile); await run(Tasks.UpdateLicenseFile); diff --git a/src/dev/build/tasks/generate_packages_optimized_assets.ts b/src/dev/build/tasks/generate_packages_optimized_assets.ts new file mode 100644 index 0000000000000..f638e38d14a7a --- /dev/null +++ b/src/dev/build/tasks/generate_packages_optimized_assets.ts @@ -0,0 +1,200 @@ +/* + * Copyright 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 { pipeline } from 'stream'; +import { promisify } from 'util'; + +import fs from 'fs'; +import gulpBrotli from 'gulp-brotli'; +// @ts-expect-error +import gulpGzip from 'gulp-gzip'; +// @ts-expect-error +import gulpPostCSS from 'gulp-postcss'; +// @ts-expect-error +import gulpTerser from 'gulp-terser'; +import terser from 'terser'; +import vfs from 'vinyl-fs'; + +import { ToolingLog } from '@kbn/dev-utils'; +import { Task, Build, write, deleteAll } from '../lib'; + +const asyncPipeline = promisify(pipeline); +const asyncStat = promisify(fs.stat); + +const removePreMinifySourceMaps = async (log: ToolingLog, build: Build) => { + log.debug('Remove Pre Minify Sourcemaps'); + + await deleteAll( + [build.resolvePath('node_modules/@kbn/ui-shared-deps/shared_built_assets', '**', '*.map')], + log + ); +}; + +const minifyKbnUiSharedDepsCSS = async (log: ToolingLog, build: Build) => { + const buildRoot = build.resolvePath(); + + log.debug('Minify CSS'); + + await asyncPipeline( + vfs.src(['node_modules/@kbn/ui-shared-deps/shared_built_assets/**/*.css'], { + cwd: buildRoot, + }), + + gulpPostCSS([ + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('cssnano')({ + preset: [ + 'default', + { + discardComments: false, + }, + ], + }), + ]), + + vfs.dest('node_modules/@kbn/ui-shared-deps/shared_built_assets', { cwd: buildRoot }) + ); +}; + +const minifyKbnUiSharedDepsJS = async (log: ToolingLog, build: Build) => { + const buildRoot = build.resolvePath(); + + log.debug('Minify JS'); + + await asyncPipeline( + vfs.src(['node_modules/@kbn/ui-shared-deps/shared_built_assets/**/*.js'], { + cwd: buildRoot, + }), + + gulpTerser( + { + compress: true, + mangle: true, + }, + terser.minify + ), + + vfs.dest('node_modules/@kbn/ui-shared-deps/shared_built_assets', { cwd: buildRoot }) + ); +}; + +const brotliCompressKbnUiSharedDeps = async (log: ToolingLog, build: Build) => { + const buildRoot = build.resolvePath(); + + log.debug('Brotli compress'); + + await asyncPipeline( + vfs.src(['node_modules/@kbn/ui-shared-deps/shared_built_assets/**/*.{js,css}'], { + cwd: buildRoot, + }), + + gulpBrotli(), + + vfs.dest('node_modules/@kbn/ui-shared-deps/shared_built_assets', { cwd: buildRoot }) + ); +}; + +const gzipCompressKbnUiSharedDeps = async (log: ToolingLog, build: Build) => { + const buildRoot = build.resolvePath(); + + log.debug('GZip compress'); + + await asyncPipeline( + vfs.src(['node_modules/@kbn/ui-shared-deps/shared_built_assets/**/*.{js,css}'], { + cwd: buildRoot, + }), + + gulpGzip(), + + vfs.dest('node_modules/@kbn/ui-shared-deps/shared_built_assets', { cwd: buildRoot }) + ); +}; + +const createKbnUiSharedDepsBundleMetrics = async (log: ToolingLog, build: Build) => { + const bundleMetricsFilePath = build.resolvePath( + 'node_modules/@kbn/ui-shared-deps/shared_built_assets', + 'metrics.json' + ); + + const kbnUISharedDepsJSFileSize = ( + await asyncStat( + build.resolvePath( + 'node_modules/@kbn/ui-shared-deps/shared_built_assets', + 'kbn-ui-shared-deps.js' + ) + ) + ).size; + + const kbnUISharedDepsCSSFileSize = + ( + await asyncStat( + build.resolvePath( + 'node_modules/@kbn/ui-shared-deps/shared_built_assets', + 'kbn-ui-shared-deps.css' + ) + ) + ).size + + ( + await asyncStat( + build.resolvePath( + 'node_modules/@kbn/ui-shared-deps/shared_built_assets', + 'kbn-ui-shared-deps.v7.light.css' + ) + ) + ).size; + + const kbnUISharedDepsElasticJSFileSize = ( + await asyncStat( + build.resolvePath( + 'node_modules/@kbn/ui-shared-deps/shared_built_assets', + 'kbn-ui-shared-deps.@elastic.js' + ) + ) + ).size; + + log.debug('Create metrics.json'); + + const metrics = [ + { + group: 'page load bundle size', + id: 'kbnUiSharedDeps-js', + value: kbnUISharedDepsJSFileSize, + }, + { + group: 'page load bundle size', + id: 'kbnUiSharedDeps-css', + value: kbnUISharedDepsCSSFileSize, + }, + { + group: 'page load bundle size', + id: 'kbnUiSharedDeps-elastic', + value: kbnUISharedDepsElasticJSFileSize, + }, + ]; + + await write(bundleMetricsFilePath, JSON.stringify(metrics, null, 2)); +}; + +const generateKbnUiSharedDepsOptimizedAssets = async (log: ToolingLog, build: Build) => { + log.info('Creating optimized assets for @kbn/ui-shared-deps'); + await removePreMinifySourceMaps(log, build); + await minifyKbnUiSharedDepsCSS(log, build); + await minifyKbnUiSharedDepsJS(log, build); + await createKbnUiSharedDepsBundleMetrics(log, build); + await brotliCompressKbnUiSharedDeps(log, build); + await gzipCompressKbnUiSharedDeps(log, build); +}; + +export const GeneratePackagesOptimizedAssets: Task = { + description: 'Generates Optimized Assets for Packages', + + async run(config, log, build) { + // Create optimized assets for @kbn/ui-shared-deps + await generateKbnUiSharedDepsOptimizedAssets(log, build); + }, +}; diff --git a/src/dev/build/tasks/index.ts b/src/dev/build/tasks/index.ts index f22e8aca01a2d..c07da103ca150 100644 --- a/src/dev/build/tasks/index.ts +++ b/src/dev/build/tasks/index.ts @@ -15,6 +15,7 @@ export * from './create_archives_sources_task'; export * from './create_archives_task'; export * from './create_empty_dirs_and_files_task'; export * from './create_readme_task'; +export * from './generate_packages_optimized_assets'; export * from './install_dependencies_task'; export * from './license_file_task'; export * from './nodejs'; diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index c7a129418765b..644dc32dd8140 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -389,6 +389,8 @@ kibana_vars=( xpack.securitySolution.maxTimelineImportExportSize xpack.securitySolution.maxTimelineImportPayloadBytes xpack.securitySolution.packagerTaskInterval + xpack.securitySolution.prebuiltRulesFromFileSystem + xpack.securitySolution.prebuiltRulesFromSavedObjects xpack.spaces.enabled xpack.spaces.maxSpaces xpack.task_manager.enabled diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 15497258d4574..7aca25d2013d2 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -18,7 +18,9 @@ export const storybookAliases = { data_enhanced: 'x-pack/plugins/data_enhanced/.storybook', embeddable: 'src/plugins/embeddable/.storybook', expression_error: 'src/plugins/expression_error/.storybook', + expression_repeat_image: 'src/plugins/expression_repeat_image/.storybook', expression_reveal_image: 'src/plugins/expression_reveal_image/.storybook', + expression_shape: 'src/plugins/expression_shape/.storybook', infra: 'x-pack/plugins/infra/.storybook', security_solution: 'x-pack/plugins/security_solution/.storybook', ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/.storybook', diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts index b2873febee0d8..7360725e39cc1 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts @@ -8,8 +8,16 @@ import { uniqBy } from 'lodash'; import deepEqual from 'fast-deep-equal'; -import { merge, Observable, pipe } from 'rxjs'; -import { distinctUntilChanged, switchMap, startWith, filter, mapTo, map } from 'rxjs/operators'; +import { merge, Observable, pipe, EMPTY } from 'rxjs'; +import { + distinctUntilChanged, + catchError, + switchMap, + startWith, + filter, + mapTo, + map, +} from 'rxjs/operators'; import { DashboardContainer } from '..'; import { isErrorEmbeddable } from '../../services/embeddable'; @@ -73,7 +81,16 @@ export const syncDashboardIndexPatterns = ({ map(() => dashboardContainer!.getChildIds()), distinctUntilChanged(deepEqual), switchMap((newChildIds: string[]) => - merge(...newChildIds.map((childId) => dashboardContainer!.getChild(childId).getOutput$())) + merge( + ...newChildIds.map((childId) => + dashboardContainer! + .getChild(childId) + .getOutput$() + // Embeddables often throw errors into their output streams. + // This should not affect dashboard loading + .pipe(catchError(() => EMPTY)) + ) + ) ) ) ) diff --git a/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts b/src/plugins/data/common/es_query/get_es_query_config.test.ts similarity index 97% rename from src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts rename to src/plugins/data/common/es_query/get_es_query_config.test.ts index 6963960d7ce03..5513f2649265f 100644 --- a/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts +++ b/src/plugins/data/common/es_query/get_es_query_config.test.ts @@ -9,7 +9,7 @@ import { get } from 'lodash'; import { getEsQueryConfig } from './get_es_query_config'; import { IUiSettingsClient } from 'kibana/public'; -import { UI_SETTINGS } from '../../'; +import { UI_SETTINGS } from '..'; const config = ({ get(item: string) { diff --git a/src/plugins/data/common/es_query/es_query/get_es_query_config.ts b/src/plugins/data/common/es_query/get_es_query_config.ts similarity index 82% rename from src/plugins/data/common/es_query/es_query/get_es_query_config.ts rename to src/plugins/data/common/es_query/get_es_query_config.ts index a074bb2ddc0e3..e3d39df75255c 100644 --- a/src/plugins/data/common/es_query/es_query/get_es_query_config.ts +++ b/src/plugins/data/common/es_query/get_es_query_config.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -import { EsQueryConfig } from './build_es_query'; -import { GetConfigFn, UI_SETTINGS } from '../../'; +import { EsQueryConfig } from '@kbn/es-query'; +import { GetConfigFn, UI_SETTINGS } from '..'; interface KibanaConfig { get: GetConfigFn; } -export function getEsQueryConfig(config: KibanaConfig) { +export function getEsQueryConfig(config: KibanaConfig): EsQueryConfig { const allowLeadingWildcards = config.get(UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS); const queryStringOptions = config.get(UI_SETTINGS.QUERY_STRING_OPTIONS); const ignoreFilterIfFieldNotInIndex = config.get( @@ -26,5 +26,5 @@ export function getEsQueryConfig(config: KibanaConfig) { queryStringOptions, ignoreFilterIfFieldNotInIndex, dateFormatTZ, - } as EsQueryConfig; + }; } diff --git a/src/plugins/data/common/es_query/index.ts b/src/plugins/data/common/es_query/index.ts index bbba52871d4c8..6d5084900b11d 100644 --- a/src/plugins/data/common/es_query/index.ts +++ b/src/plugins/data/common/es_query/index.ts @@ -6,6 +6,358 @@ * Side Public License, v 1. */ -export * from './es_query'; -export * from './filters'; -export * from './kuery'; +export { getEsQueryConfig } from './get_es_query_config'; + +// NOTE: Trick to deprecate exports https://stackoverflow.com/a/49152018/372086 +import { + isFilterDisabled as oldIsFilterDisabled, + disableFilter as oldDisableFilter, + fromKueryExpression as oldFromKueryExpression, + toElasticsearchQuery as oldToElasticsearchQuery, + nodeTypes as oldNodeTypes, + buildEsQuery as oldBuildEsQuery, + buildQueryFromFilters as oldBuildQueryFromFilters, + luceneStringToDsl as oldLuceneStringToDsl, + decorateQuery as olddecorateQuery, + getPhraseFilterField as oldgetPhraseFilterField, + getPhraseFilterValue as oldgetPhraseFilterValue, + isFilterPinned as oldIsFilterPinned, + nodeBuilder as oldNodeBuilder, + isFilters as oldIsFilters, + isExistsFilter as oldIsExistsFilter, + isMatchAllFilter as oldIsMatchAllFilter, + isGeoBoundingBoxFilter as oldIsGeoBoundingBoxFilter, + isGeoPolygonFilter as oldIsGeoPolygonFilter, + isMissingFilter as oldIsMissingFilter, + isPhraseFilter as oldIsPhraseFilter, + isPhrasesFilter as oldIsPhrasesFilter, + isRangeFilter as oldIsRangeFilter, + isQueryStringFilter as oldIsQueryStringFilter, + buildQueryFilter as oldBuildQueryFilter, + buildPhrasesFilter as oldBuildPhrasesFilter, + buildPhraseFilter as oldBuildPhraseFilter, + buildRangeFilter as oldBuildRangeFilter, + buildCustomFilter as oldBuildCustomFilter, + buildFilter as oldBuildFilter, + buildEmptyFilter as oldBuildEmptyFilter, + buildExistsFilter as oldBuildExistsFilter, + toggleFilterNegated as oldtoggleFilterNegated, + Filter as oldFilter, + RangeFilterMeta as oldRangeFilterMeta, + RangeFilterParams as oldRangeFilterParams, + ExistsFilter as oldExistsFilter, + GeoPolygonFilter as oldGeoPolygonFilter, + PhrasesFilter as oldPhrasesFilter, + PhraseFilter as oldPhraseFilter, + MatchAllFilter as oldMatchAllFilter, + CustomFilter as oldCustomFilter, + MissingFilter as oldMissingFilter, + RangeFilter as oldRangeFilter, + GeoBoundingBoxFilter as oldGeoBoundingBoxFilter, + KueryNode as oldKueryNode, + FilterMeta as oldFilterMeta, + FILTERS as oldFILTERS, + IFieldSubType as oldIFieldSubType, + EsQueryConfig as oldEsQueryConfig, + isFilter as oldIsFilter, + FilterStateStore, +} from '@kbn/es-query'; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isFilter = oldIsFilter; +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isFilterDisabled = oldIsFilterDisabled; +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const disableFilter = oldDisableFilter; +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const fromKueryExpression = oldFromKueryExpression; +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const toElasticsearchQuery = oldToElasticsearchQuery; +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const nodeTypes = oldNodeTypes; +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const buildEsQuery = oldBuildEsQuery; +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const buildQueryFromFilters = oldBuildQueryFromFilters; +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const luceneStringToDsl = oldLuceneStringToDsl; +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const decorateQuery = olddecorateQuery; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const getPhraseFilterField = oldgetPhraseFilterField; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const getPhraseFilterValue = oldgetPhraseFilterValue; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isFilterPinned = oldIsFilterPinned; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const nodeBuilder = oldNodeBuilder; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isFilters = oldIsFilters; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isExistsFilter = oldIsExistsFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isMatchAllFilter = oldIsMatchAllFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isGeoBoundingBoxFilter = oldIsGeoBoundingBoxFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isGeoPolygonFilter = oldIsGeoPolygonFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isMissingFilter = oldIsMissingFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isPhraseFilter = oldIsPhraseFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isPhrasesFilter = oldIsPhrasesFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isRangeFilter = oldIsRangeFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const isQueryStringFilter = oldIsQueryStringFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const buildQueryFilter = oldBuildQueryFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const buildPhrasesFilter = oldBuildPhrasesFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const buildPhraseFilter = oldBuildPhraseFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const buildRangeFilter = oldBuildRangeFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const buildCustomFilter = oldBuildCustomFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const buildFilter = oldBuildFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const buildEmptyFilter = oldBuildEmptyFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const buildExistsFilter = oldBuildExistsFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const toggleFilterNegated = oldtoggleFilterNegated; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +const FILTERS = oldFILTERS; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type Filter = oldFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type RangeFilterMeta = oldRangeFilterMeta; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type RangeFilterParams = oldRangeFilterParams; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type ExistsFilter = oldExistsFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type GeoPolygonFilter = oldGeoPolygonFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type PhrasesFilter = oldPhrasesFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type PhraseFilter = oldPhraseFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type MatchAllFilter = oldMatchAllFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type CustomFilter = oldCustomFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type MissingFilter = oldMissingFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type RangeFilter = oldRangeFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type GeoBoundingBoxFilter = oldGeoBoundingBoxFilter; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type KueryNode = oldKueryNode; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type FilterMeta = oldFilterMeta; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type IFieldSubType = oldIFieldSubType; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ +type EsQueryConfig = oldEsQueryConfig; + +/** + * @deprecated Please import from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ + +export { + disableFilter, + fromKueryExpression, + toElasticsearchQuery, + nodeTypes, + buildEsQuery, + buildQueryFromFilters, + luceneStringToDsl, + decorateQuery, + getPhraseFilterField, + getPhraseFilterValue, + isFilterPinned, + nodeBuilder, + isFilters, + isExistsFilter, + isMatchAllFilter, + isGeoBoundingBoxFilter, + isGeoPolygonFilter, + isMissingFilter, + isPhraseFilter, + isPhrasesFilter, + isRangeFilter, + isQueryStringFilter, + buildQueryFilter, + buildPhrasesFilter, + buildPhraseFilter, + buildRangeFilter, + buildCustomFilter, + buildFilter, + buildEmptyFilter, + buildExistsFilter, + toggleFilterNegated, + FILTERS, + isFilter, + isFilterDisabled, + FilterStateStore, + Filter, + RangeFilterMeta, + RangeFilterParams, + ExistsFilter, + GeoPolygonFilter, + PhrasesFilter, + PhraseFilter, + MatchAllFilter, + CustomFilter, + MissingFilter, + RangeFilter, + GeoBoundingBoxFilter, + KueryNode, + FilterMeta, + IFieldSubType, + EsQueryConfig, +}; diff --git a/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js b/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js deleted file mode 100644 index 7ee744ad5f4c8..0000000000000 --- a/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js +++ /dev/null @@ -1,2635 +0,0 @@ -module.exports = (function() { - "use strict"; - - /* - * Generated by PEG.js 0.9.0. - * - * http://pegjs.org/ - */ - - function peg$subclass(child, parent) { - function ctor() { this.constructor = child; } - ctor.prototype = parent.prototype; - child.prototype = new ctor(); - } - - function peg$SyntaxError(message, expected, found, location) { - this.message = message; - this.expected = expected; - this.found = found; - this.location = location; - this.name = "SyntaxError"; - - if (typeof Error.captureStackTrace === "function") { - Error.captureStackTrace(this, peg$SyntaxError); - } - } - - peg$subclass(peg$SyntaxError, Error); - - function peg$parse(input) { - var options = arguments.length > 1 ? arguments[1] : {}, - parser = this, - - peg$FAILED = {}, - - peg$startRuleFunctions = { start: peg$parsestart, Literal: peg$parseLiteral }, - peg$startRuleFunction = peg$parsestart, - - peg$c0 = function(query, trailing) { - if (trailing.type === 'cursor') { - return { - ...trailing, - suggestionTypes: ['conjunction'] - }; - } - if (query !== null) return query; - return nodeTypes.function.buildNode('is', '*', '*'); - }, - peg$c1 = function(head, query) { return query; }, - peg$c2 = function(head, tail) { - const nodes = [head, ...tail]; - const cursor = parseCursor && nodes.find(node => node.type === 'cursor'); - if (cursor) return cursor; - return buildFunctionNode('or', nodes); - }, - peg$c3 = function(head, tail) { - const nodes = [head, ...tail]; - const cursor = parseCursor && nodes.find(node => node.type === 'cursor'); - if (cursor) return cursor; - return buildFunctionNode('and', nodes); - }, - peg$c4 = function(query) { - if (query.type === 'cursor') return query; - return buildFunctionNode('not', [query]); - }, - peg$c5 = "(", - peg$c6 = { type: "literal", value: "(", description: "\"(\"" }, - peg$c7 = ")", - peg$c8 = { type: "literal", value: ")", description: "\")\"" }, - peg$c9 = function(query, trailing) { - if (trailing.type === 'cursor') { - return { - ...trailing, - suggestionTypes: ['conjunction'] - }; - } - return query; - }, - peg$c10 = ":", - peg$c11 = { type: "literal", value: ":", description: "\":\"" }, - peg$c12 = "{", - peg$c13 = { type: "literal", value: "{", description: "\"{\"" }, - peg$c14 = "}", - peg$c15 = { type: "literal", value: "}", description: "\"}\"" }, - peg$c16 = function(field, query, trailing) { - if (query.type === 'cursor') { - return { - ...query, - nestedPath: query.nestedPath ? `${field.value}.${query.nestedPath}` : field.value, - } - }; - - if (trailing.type === 'cursor') { - return { - ...trailing, - suggestionTypes: ['conjunction'] - }; - } - return buildFunctionNode('nested', [field, query]); - }, - peg$c17 = { type: "other", description: "fieldName" }, - peg$c18 = function(field, operator, value) { - if (value.type === 'cursor') { - return { - ...value, - suggestionTypes: ['conjunction'] - }; - } - const range = buildNamedArgNode(operator, value); - return buildFunctionNode('range', [field, range]); - }, - peg$c19 = function(field, partial) { - if (partial.type === 'cursor') { - return { - ...partial, - fieldName: field.value, - suggestionTypes: ['value', 'conjunction'] - }; - } - return partial(field); - }, - peg$c20 = function(partial) { - if (partial.type === 'cursor') { - const fieldName = `${partial.prefix}${partial.suffix}`.trim(); - return { - ...partial, - fieldName, - suggestionTypes: ['field', 'operator', 'conjunction'] - }; - } - const field = buildLiteralNode(null); - return partial(field); - }, - peg$c21 = function(partial, trailing) { - if (trailing.type === 'cursor') { - return { - ...trailing, - suggestionTypes: ['conjunction'] - }; - } - return partial; - }, - peg$c22 = function(head, partial) { return partial; }, - peg$c23 = function(head, tail) { - const nodes = [head, ...tail]; - const cursor = parseCursor && nodes.find(node => node.type === 'cursor'); - if (cursor) { - return { - ...cursor, - suggestionTypes: ['value'] - }; - } - return (field) => buildFunctionNode('or', nodes.map(partial => partial(field))); - }, - peg$c24 = function(head, tail) { - const nodes = [head, ...tail]; - const cursor = parseCursor && nodes.find(node => node.type === 'cursor'); - if (cursor) { - return { - ...cursor, - suggestionTypes: ['value'] - }; - } - return (field) => buildFunctionNode('and', nodes.map(partial => partial(field))); - }, - peg$c25 = function(partial) { - if (partial.type === 'cursor') { - return { - ...list, - suggestionTypes: ['value'] - }; - } - return (field) => buildFunctionNode('not', [partial(field)]); - }, - peg$c26 = { type: "other", description: "value" }, - peg$c27 = function(value) { - if (value.type === 'cursor') return value; - const isPhrase = buildLiteralNode(true); - return (field) => buildFunctionNode('is', [field, value, isPhrase]); - }, - peg$c28 = function(value) { - if (value.type === 'cursor') return value; - - if (!allowLeadingWildcards && value.type === 'wildcard' && nodeTypes.wildcard.hasLeadingWildcard(value)) { - error('Leading wildcards are disabled. See query:allowLeadingWildcards in Advanced Settings.'); - } - - const isPhrase = buildLiteralNode(false); - return (field) => buildFunctionNode('is', [field, value, isPhrase]); - }, - peg$c29 = { type: "other", description: "OR" }, - peg$c30 = "or", - peg$c31 = { type: "literal", value: "or", description: "\"or\"" }, - peg$c32 = { type: "other", description: "AND" }, - peg$c33 = "and", - peg$c34 = { type: "literal", value: "and", description: "\"and\"" }, - peg$c35 = { type: "other", description: "NOT" }, - peg$c36 = "not", - peg$c37 = { type: "literal", value: "not", description: "\"not\"" }, - peg$c38 = { type: "other", description: "literal" }, - peg$c39 = function() { return parseCursor; }, - peg$c40 = "\"", - peg$c41 = { type: "literal", value: "\"", description: "\"\\\"\"" }, - peg$c42 = function(prefix, cursor, suffix) { - const { start, end } = location(); - return { - type: 'cursor', - start: start.offset, - end: end.offset - cursor.length, - prefix: prefix.join(''), - suffix: suffix.join(''), - text: text().replace(cursor, '') - }; - }, - peg$c43 = function(chars) { - return buildLiteralNode(chars.join('')); - }, - peg$c44 = "\\", - peg$c45 = { type: "literal", value: "\\", description: "\"\\\\\"" }, - peg$c46 = /^[\\"]/, - peg$c47 = { type: "class", value: "[\\\\\"]", description: "[\\\\\"]" }, - peg$c48 = function(char) { return char; }, - peg$c49 = /^[^"]/, - peg$c50 = { type: "class", value: "[^\"]", description: "[^\"]" }, - peg$c51 = function(chars) { - const sequence = chars.join('').trim(); - if (sequence === 'null') return buildLiteralNode(null); - if (sequence === 'true') return buildLiteralNode(true); - if (sequence === 'false') return buildLiteralNode(false); - if (chars.includes(wildcardSymbol)) return buildWildcardNode(sequence); - return buildLiteralNode(sequence); - }, - peg$c52 = { type: "any", description: "any character" }, - peg$c53 = "*", - peg$c54 = { type: "literal", value: "*", description: "\"*\"" }, - peg$c55 = function() { return wildcardSymbol; }, - peg$c56 = "\\t", - peg$c57 = { type: "literal", value: "\\t", description: "\"\\\\t\"" }, - peg$c58 = function() { return '\t'; }, - peg$c59 = "\\r", - peg$c60 = { type: "literal", value: "\\r", description: "\"\\\\r\"" }, - peg$c61 = function() { return '\r'; }, - peg$c62 = "\\n", - peg$c63 = { type: "literal", value: "\\n", description: "\"\\\\n\"" }, - peg$c64 = function() { return '\n'; }, - peg$c65 = function(keyword) { return keyword; }, - peg$c66 = /^[\\():<>"*{}]/, - peg$c67 = { type: "class", value: "[\\\\():<>\"*{}]", description: "[\\\\():<>\"*{}]" }, - peg$c68 = function(sequence) { return sequence; }, - peg$c69 = "u", - peg$c70 = { type: "literal", value: "u", description: "\"u\"" }, - peg$c71 = function(digits) { - return String.fromCharCode(parseInt(digits, 16)); - }, - peg$c72 = /^[0-9a-f]/i, - peg$c73 = { type: "class", value: "[0-9a-f]i", description: "[0-9a-f]i" }, - peg$c74 = "<=", - peg$c75 = { type: "literal", value: "<=", description: "\"<=\"" }, - peg$c76 = function() { return 'lte'; }, - peg$c77 = ">=", - peg$c78 = { type: "literal", value: ">=", description: "\">=\"" }, - peg$c79 = function() { return 'gte'; }, - peg$c80 = "<", - peg$c81 = { type: "literal", value: "<", description: "\"<\"" }, - peg$c82 = function() { return 'lt'; }, - peg$c83 = ">", - peg$c84 = { type: "literal", value: ">", description: "\">\"" }, - peg$c85 = function() { return 'gt'; }, - peg$c86 = { type: "other", description: "whitespace" }, - peg$c87 = /^[ \t\r\n\xA0]/, - peg$c88 = { type: "class", value: "[\\ \\t\\r\\n\\u00A0]", description: "[\\ \\t\\r\\n\\u00A0]" }, - peg$c89 = "@kuery-cursor@", - peg$c90 = { type: "literal", value: "@kuery-cursor@", description: "\"@kuery-cursor@\"" }, - peg$c91 = function() { return cursorSymbol; }, - - peg$currPos = 0, - peg$savedPos = 0, - peg$posDetailsCache = [{ line: 1, column: 1, seenCR: false }], - peg$maxFailPos = 0, - peg$maxFailExpected = [], - peg$silentFails = 0, - - peg$resultsCache = {}, - - peg$result; - - if ("startRule" in options) { - if (!(options.startRule in peg$startRuleFunctions)) { - throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); - } - - peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; - } - - function text() { - return input.substring(peg$savedPos, peg$currPos); - } - - function location() { - return peg$computeLocation(peg$savedPos, peg$currPos); - } - - function expected(description) { - throw peg$buildException( - null, - [{ type: "other", description: description }], - input.substring(peg$savedPos, peg$currPos), - peg$computeLocation(peg$savedPos, peg$currPos) - ); - } - - function error(message) { - throw peg$buildException( - message, - null, - input.substring(peg$savedPos, peg$currPos), - peg$computeLocation(peg$savedPos, peg$currPos) - ); - } - - function peg$computePosDetails(pos) { - var details = peg$posDetailsCache[pos], - p, ch; - - if (details) { - return details; - } else { - p = pos - 1; - while (!peg$posDetailsCache[p]) { - p--; - } - - details = peg$posDetailsCache[p]; - details = { - line: details.line, - column: details.column, - seenCR: details.seenCR - }; - - while (p < pos) { - ch = input.charAt(p); - if (ch === "\n") { - if (!details.seenCR) { details.line++; } - details.column = 1; - details.seenCR = false; - } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { - details.line++; - details.column = 1; - details.seenCR = true; - } else { - details.column++; - details.seenCR = false; - } - - p++; - } - - peg$posDetailsCache[pos] = details; - return details; - } - } - - function peg$computeLocation(startPos, endPos) { - var startPosDetails = peg$computePosDetails(startPos), - endPosDetails = peg$computePosDetails(endPos); - - return { - start: { - offset: startPos, - line: startPosDetails.line, - column: startPosDetails.column - }, - end: { - offset: endPos, - line: endPosDetails.line, - column: endPosDetails.column - } - }; - } - - function peg$fail(expected) { - if (peg$currPos < peg$maxFailPos) { return; } - - if (peg$currPos > peg$maxFailPos) { - peg$maxFailPos = peg$currPos; - peg$maxFailExpected = []; - } - - peg$maxFailExpected.push(expected); - } - - function peg$buildException(message, expected, found, location) { - function cleanupExpected(expected) { - var i = 1; - - expected.sort(function(a, b) { - if (a.description < b.description) { - return -1; - } else if (a.description > b.description) { - return 1; - } else { - return 0; - } - }); - - while (i < expected.length) { - if (expected[i - 1] === expected[i]) { - expected.splice(i, 1); - } else { - i++; - } - } - } - - function buildMessage(expected, found) { - function stringEscape(s) { - function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); } - - return s - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\x08/g, '\\b') - .replace(/\t/g, '\\t') - .replace(/\n/g, '\\n') - .replace(/\f/g, '\\f') - .replace(/\r/g, '\\r') - .replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) - .replace(/[\x10-\x1F\x80-\xFF]/g, function(ch) { return '\\x' + hex(ch); }) - .replace(/[\u0100-\u0FFF]/g, function(ch) { return '\\u0' + hex(ch); }) - .replace(/[\u1000-\uFFFF]/g, function(ch) { return '\\u' + hex(ch); }); - } - - var expectedDescs = new Array(expected.length), - expectedDesc, foundDesc, i; - - for (i = 0; i < expected.length; i++) { - expectedDescs[i] = expected[i].description; - } - - expectedDesc = expected.length > 1 - ? expectedDescs.slice(0, -1).join(", ") - + " or " - + expectedDescs[expected.length - 1] - : expectedDescs[0]; - - foundDesc = found ? "\"" + stringEscape(found) + "\"" : "end of input"; - - return "Expected " + expectedDesc + " but " + foundDesc + " found."; - } - - if (expected !== null) { - cleanupExpected(expected); - } - - return new peg$SyntaxError( - message !== null ? message : buildMessage(expected, found), - expected, - found, - location - ); - } - - function peg$parsestart() { - var s0, s1, s2, s3; - - var key = peg$currPos * 37 + 0, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - s1 = []; - s2 = peg$parseSpace(); - while (s2 !== peg$FAILED) { - s1.push(s2); - s2 = peg$parseSpace(); - } - if (s1 !== peg$FAILED) { - s2 = peg$parseOrQuery(); - if (s2 === peg$FAILED) { - s2 = null; - } - if (s2 !== peg$FAILED) { - s3 = peg$parseOptionalSpace(); - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c0(s2, s3); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseOrQuery() { - var s0, s1, s2, s3, s4, s5; - - var key = peg$currPos * 37 + 1, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - s1 = peg$parseAndQuery(); - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$currPos; - s4 = peg$parseOr(); - if (s4 !== peg$FAILED) { - s5 = peg$parseAndQuery(); - if (s5 !== peg$FAILED) { - peg$savedPos = s3; - s4 = peg$c1(s1, s5); - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - if (s3 !== peg$FAILED) { - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$currPos; - s4 = peg$parseOr(); - if (s4 !== peg$FAILED) { - s5 = peg$parseAndQuery(); - if (s5 !== peg$FAILED) { - peg$savedPos = s3; - s4 = peg$c1(s1, s5); - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } - } else { - s2 = peg$FAILED; - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c2(s1, s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parseAndQuery(); - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseAndQuery() { - var s0, s1, s2, s3, s4, s5; - - var key = peg$currPos * 37 + 2, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - s1 = peg$parseNotQuery(); - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$currPos; - s4 = peg$parseAnd(); - if (s4 !== peg$FAILED) { - s5 = peg$parseNotQuery(); - if (s5 !== peg$FAILED) { - peg$savedPos = s3; - s4 = peg$c1(s1, s5); - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - if (s3 !== peg$FAILED) { - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$currPos; - s4 = peg$parseAnd(); - if (s4 !== peg$FAILED) { - s5 = peg$parseNotQuery(); - if (s5 !== peg$FAILED) { - peg$savedPos = s3; - s4 = peg$c1(s1, s5); - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } - } else { - s2 = peg$FAILED; - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c3(s1, s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parseNotQuery(); - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseNotQuery() { - var s0, s1, s2; - - var key = peg$currPos * 37 + 3, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - s1 = peg$parseNot(); - if (s1 !== peg$FAILED) { - s2 = peg$parseSubQuery(); - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c4(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parseSubQuery(); - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseSubQuery() { - var s0, s1, s2, s3, s4, s5; - - var key = peg$currPos * 37 + 4, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 40) { - s1 = peg$c5; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c6); } - } - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$parseSpace(); - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$parseSpace(); - } - if (s2 !== peg$FAILED) { - s3 = peg$parseOrQuery(); - if (s3 !== peg$FAILED) { - s4 = peg$parseOptionalSpace(); - if (s4 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 41) { - s5 = peg$c7; - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c8); } - } - if (s5 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c9(s3, s4); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parseNestedQuery(); - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseNestedQuery() { - var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9; - - var key = peg$currPos * 37 + 5, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - s1 = peg$parseField(); - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$parseSpace(); - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$parseSpace(); - } - if (s2 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 58) { - s3 = peg$c10; - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c11); } - } - if (s3 !== peg$FAILED) { - s4 = []; - s5 = peg$parseSpace(); - while (s5 !== peg$FAILED) { - s4.push(s5); - s5 = peg$parseSpace(); - } - if (s4 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 123) { - s5 = peg$c12; - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c13); } - } - if (s5 !== peg$FAILED) { - s6 = []; - s7 = peg$parseSpace(); - while (s7 !== peg$FAILED) { - s6.push(s7); - s7 = peg$parseSpace(); - } - if (s6 !== peg$FAILED) { - s7 = peg$parseOrQuery(); - if (s7 !== peg$FAILED) { - s8 = peg$parseOptionalSpace(); - if (s8 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 125) { - s9 = peg$c14; - peg$currPos++; - } else { - s9 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c15); } - } - if (s9 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c16(s1, s7, s8); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parseExpression(); - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseExpression() { - var s0; - - var key = peg$currPos * 37 + 6, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$parseFieldRangeExpression(); - if (s0 === peg$FAILED) { - s0 = peg$parseFieldValueExpression(); - if (s0 === peg$FAILED) { - s0 = peg$parseValueExpression(); - } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseField() { - var s0, s1; - - var key = peg$currPos * 37 + 7, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - peg$silentFails++; - s0 = peg$parseLiteral(); - peg$silentFails--; - if (s0 === peg$FAILED) { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c17); } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseFieldRangeExpression() { - var s0, s1, s2, s3, s4, s5; - - var key = peg$currPos * 37 + 8, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - s1 = peg$parseField(); - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$parseSpace(); - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$parseSpace(); - } - if (s2 !== peg$FAILED) { - s3 = peg$parseRangeOperator(); - if (s3 !== peg$FAILED) { - s4 = []; - s5 = peg$parseSpace(); - while (s5 !== peg$FAILED) { - s4.push(s5); - s5 = peg$parseSpace(); - } - if (s4 !== peg$FAILED) { - s5 = peg$parseLiteral(); - if (s5 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c18(s1, s3, s5); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseFieldValueExpression() { - var s0, s1, s2, s3, s4, s5; - - var key = peg$currPos * 37 + 9, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - s1 = peg$parseField(); - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$parseSpace(); - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$parseSpace(); - } - if (s2 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 58) { - s3 = peg$c10; - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c11); } - } - if (s3 !== peg$FAILED) { - s4 = []; - s5 = peg$parseSpace(); - while (s5 !== peg$FAILED) { - s4.push(s5); - s5 = peg$parseSpace(); - } - if (s4 !== peg$FAILED) { - s5 = peg$parseListOfValues(); - if (s5 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c19(s1, s5); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseValueExpression() { - var s0, s1; - - var key = peg$currPos * 37 + 10, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - s1 = peg$parseValue(); - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c20(s1); - } - s0 = s1; - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseListOfValues() { - var s0, s1, s2, s3, s4, s5; - - var key = peg$currPos * 37 + 11, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 40) { - s1 = peg$c5; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c6); } - } - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$parseSpace(); - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$parseSpace(); - } - if (s2 !== peg$FAILED) { - s3 = peg$parseOrListOfValues(); - if (s3 !== peg$FAILED) { - s4 = peg$parseOptionalSpace(); - if (s4 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 41) { - s5 = peg$c7; - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c8); } - } - if (s5 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c21(s3, s4); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parseValue(); - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseOrListOfValues() { - var s0, s1, s2, s3, s4, s5; - - var key = peg$currPos * 37 + 12, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - s1 = peg$parseAndListOfValues(); - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$currPos; - s4 = peg$parseOr(); - if (s4 !== peg$FAILED) { - s5 = peg$parseAndListOfValues(); - if (s5 !== peg$FAILED) { - peg$savedPos = s3; - s4 = peg$c22(s1, s5); - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - if (s3 !== peg$FAILED) { - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$currPos; - s4 = peg$parseOr(); - if (s4 !== peg$FAILED) { - s5 = peg$parseAndListOfValues(); - if (s5 !== peg$FAILED) { - peg$savedPos = s3; - s4 = peg$c22(s1, s5); - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } - } else { - s2 = peg$FAILED; - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c23(s1, s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parseAndListOfValues(); - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseAndListOfValues() { - var s0, s1, s2, s3, s4, s5; - - var key = peg$currPos * 37 + 13, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - s1 = peg$parseNotListOfValues(); - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$currPos; - s4 = peg$parseAnd(); - if (s4 !== peg$FAILED) { - s5 = peg$parseNotListOfValues(); - if (s5 !== peg$FAILED) { - peg$savedPos = s3; - s4 = peg$c22(s1, s5); - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - if (s3 !== peg$FAILED) { - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$currPos; - s4 = peg$parseAnd(); - if (s4 !== peg$FAILED) { - s5 = peg$parseNotListOfValues(); - if (s5 !== peg$FAILED) { - peg$savedPos = s3; - s4 = peg$c22(s1, s5); - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } - } else { - s2 = peg$FAILED; - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c24(s1, s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parseNotListOfValues(); - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseNotListOfValues() { - var s0, s1, s2; - - var key = peg$currPos * 37 + 14, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - s1 = peg$parseNot(); - if (s1 !== peg$FAILED) { - s2 = peg$parseListOfValues(); - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c25(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parseListOfValues(); - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseValue() { - var s0, s1; - - var key = peg$currPos * 37 + 15, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - peg$silentFails++; - s0 = peg$currPos; - s1 = peg$parseQuotedString(); - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c27(s1); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - s1 = peg$parseUnquotedLiteral(); - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c28(s1); - } - s0 = s1; - } - peg$silentFails--; - if (s0 === peg$FAILED) { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c26); } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseOr() { - var s0, s1, s2, s3, s4; - - var key = peg$currPos * 37 + 16, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - peg$silentFails++; - s0 = peg$currPos; - s1 = []; - s2 = peg$parseSpace(); - if (s2 !== peg$FAILED) { - while (s2 !== peg$FAILED) { - s1.push(s2); - s2 = peg$parseSpace(); - } - } else { - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - if (input.substr(peg$currPos, 2).toLowerCase() === peg$c30) { - s2 = input.substr(peg$currPos, 2); - peg$currPos += 2; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c31); } - } - if (s2 !== peg$FAILED) { - s3 = []; - s4 = peg$parseSpace(); - if (s4 !== peg$FAILED) { - while (s4 !== peg$FAILED) { - s3.push(s4); - s4 = peg$parseSpace(); - } - } else { - s3 = peg$FAILED; - } - if (s3 !== peg$FAILED) { - s1 = [s1, s2, s3]; - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - peg$silentFails--; - if (s0 === peg$FAILED) { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c29); } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseAnd() { - var s0, s1, s2, s3, s4; - - var key = peg$currPos * 37 + 17, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - peg$silentFails++; - s0 = peg$currPos; - s1 = []; - s2 = peg$parseSpace(); - if (s2 !== peg$FAILED) { - while (s2 !== peg$FAILED) { - s1.push(s2); - s2 = peg$parseSpace(); - } - } else { - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - if (input.substr(peg$currPos, 3).toLowerCase() === peg$c33) { - s2 = input.substr(peg$currPos, 3); - peg$currPos += 3; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c34); } - } - if (s2 !== peg$FAILED) { - s3 = []; - s4 = peg$parseSpace(); - if (s4 !== peg$FAILED) { - while (s4 !== peg$FAILED) { - s3.push(s4); - s4 = peg$parseSpace(); - } - } else { - s3 = peg$FAILED; - } - if (s3 !== peg$FAILED) { - s1 = [s1, s2, s3]; - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - peg$silentFails--; - if (s0 === peg$FAILED) { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c32); } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseNot() { - var s0, s1, s2, s3; - - var key = peg$currPos * 37 + 18, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - peg$silentFails++; - s0 = peg$currPos; - if (input.substr(peg$currPos, 3).toLowerCase() === peg$c36) { - s1 = input.substr(peg$currPos, 3); - peg$currPos += 3; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c37); } - } - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$parseSpace(); - if (s3 !== peg$FAILED) { - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$parseSpace(); - } - } else { - s2 = peg$FAILED; - } - if (s2 !== peg$FAILED) { - s1 = [s1, s2]; - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - peg$silentFails--; - if (s0 === peg$FAILED) { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c35); } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseLiteral() { - var s0, s1; - - var key = peg$currPos * 37 + 19, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - peg$silentFails++; - s0 = peg$parseQuotedString(); - if (s0 === peg$FAILED) { - s0 = peg$parseUnquotedLiteral(); - } - peg$silentFails--; - if (s0 === peg$FAILED) { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c38); } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseQuotedString() { - var s0, s1, s2, s3, s4, s5, s6; - - var key = peg$currPos * 37 + 20, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - peg$savedPos = peg$currPos; - s1 = peg$c39(); - if (s1) { - s1 = void 0; - } else { - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 34) { - s2 = peg$c40; - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c41); } - } - if (s2 !== peg$FAILED) { - s3 = []; - s4 = peg$parseQuotedCharacter(); - while (s4 !== peg$FAILED) { - s3.push(s4); - s4 = peg$parseQuotedCharacter(); - } - if (s3 !== peg$FAILED) { - s4 = peg$parseCursor(); - if (s4 !== peg$FAILED) { - s5 = []; - s6 = peg$parseQuotedCharacter(); - while (s6 !== peg$FAILED) { - s5.push(s6); - s6 = peg$parseQuotedCharacter(); - } - if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 34) { - s6 = peg$c40; - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c41); } - } - if (s6 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c42(s3, s4, s5); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 34) { - s1 = peg$c40; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c41); } - } - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$parseQuotedCharacter(); - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$parseQuotedCharacter(); - } - if (s2 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 34) { - s3 = peg$c40; - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c41); } - } - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c43(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseQuotedCharacter() { - var s0, s1, s2; - - var key = peg$currPos * 37 + 21, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$parseEscapedWhitespace(); - if (s0 === peg$FAILED) { - s0 = peg$parseEscapedUnicodeSequence(); - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 92) { - s1 = peg$c44; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c45); } - } - if (s1 !== peg$FAILED) { - if (peg$c46.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c47); } - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c48(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$currPos; - s1 = peg$currPos; - peg$silentFails++; - s2 = peg$parseCursor(); - peg$silentFails--; - if (s2 === peg$FAILED) { - s1 = void 0; - } else { - peg$currPos = s1; - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - if (peg$c49.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c50); } - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c48(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } - } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseUnquotedLiteral() { - var s0, s1, s2, s3, s4, s5; - - var key = peg$currPos * 37 + 22, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - peg$savedPos = peg$currPos; - s1 = peg$c39(); - if (s1) { - s1 = void 0; - } else { - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$parseUnquotedCharacter(); - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$parseUnquotedCharacter(); - } - if (s2 !== peg$FAILED) { - s3 = peg$parseCursor(); - if (s3 !== peg$FAILED) { - s4 = []; - s5 = peg$parseUnquotedCharacter(); - while (s5 !== peg$FAILED) { - s4.push(s5); - s5 = peg$parseUnquotedCharacter(); - } - if (s4 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c42(s2, s3, s4); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$currPos; - s1 = []; - s2 = peg$parseUnquotedCharacter(); - if (s2 !== peg$FAILED) { - while (s2 !== peg$FAILED) { - s1.push(s2); - s2 = peg$parseUnquotedCharacter(); - } - } else { - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c51(s1); - } - s0 = s1; - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseUnquotedCharacter() { - var s0, s1, s2, s3, s4; - - var key = peg$currPos * 37 + 23, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$parseEscapedWhitespace(); - if (s0 === peg$FAILED) { - s0 = peg$parseEscapedSpecialCharacter(); - if (s0 === peg$FAILED) { - s0 = peg$parseEscapedUnicodeSequence(); - if (s0 === peg$FAILED) { - s0 = peg$parseEscapedKeyword(); - if (s0 === peg$FAILED) { - s0 = peg$parseWildcard(); - if (s0 === peg$FAILED) { - s0 = peg$currPos; - s1 = peg$currPos; - peg$silentFails++; - s2 = peg$parseSpecialCharacter(); - peg$silentFails--; - if (s2 === peg$FAILED) { - s1 = void 0; - } else { - peg$currPos = s1; - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - s2 = peg$currPos; - peg$silentFails++; - s3 = peg$parseKeyword(); - peg$silentFails--; - if (s3 === peg$FAILED) { - s2 = void 0; - } else { - peg$currPos = s2; - s2 = peg$FAILED; - } - if (s2 !== peg$FAILED) { - s3 = peg$currPos; - peg$silentFails++; - s4 = peg$parseCursor(); - peg$silentFails--; - if (s4 === peg$FAILED) { - s3 = void 0; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - if (s3 !== peg$FAILED) { - if (input.length > peg$currPos) { - s4 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c52); } - } - if (s4 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c48(s4); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } - } - } - } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseWildcard() { - var s0, s1; - - var key = peg$currPos * 37 + 24, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 42) { - s1 = peg$c53; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c54); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c55(); - } - s0 = s1; - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseOptionalSpace() { - var s0, s1, s2, s3, s4, s5; - - var key = peg$currPos * 37 + 25, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - peg$savedPos = peg$currPos; - s1 = peg$c39(); - if (s1) { - s1 = void 0; - } else { - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$parseSpace(); - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$parseSpace(); - } - if (s2 !== peg$FAILED) { - s3 = peg$parseCursor(); - if (s3 !== peg$FAILED) { - s4 = []; - s5 = peg$parseSpace(); - while (s5 !== peg$FAILED) { - s4.push(s5); - s5 = peg$parseSpace(); - } - if (s4 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c42(s2, s3, s4); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = []; - s1 = peg$parseSpace(); - while (s1 !== peg$FAILED) { - s0.push(s1); - s1 = peg$parseSpace(); - } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseEscapedWhitespace() { - var s0, s1; - - var key = peg$currPos * 37 + 26, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c56) { - s1 = peg$c56; - peg$currPos += 2; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c57); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c58(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c59) { - s1 = peg$c59; - peg$currPos += 2; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c60); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c61(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c62) { - s1 = peg$c62; - peg$currPos += 2; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c63); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c64(); - } - s0 = s1; - } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseEscapedSpecialCharacter() { - var s0, s1, s2; - - var key = peg$currPos * 37 + 27, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 92) { - s1 = peg$c44; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c45); } - } - if (s1 !== peg$FAILED) { - s2 = peg$parseSpecialCharacter(); - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c48(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseEscapedKeyword() { - var s0, s1, s2; - - var key = peg$currPos * 37 + 28, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 92) { - s1 = peg$c44; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c45); } - } - if (s1 !== peg$FAILED) { - if (input.substr(peg$currPos, 2).toLowerCase() === peg$c30) { - s2 = input.substr(peg$currPos, 2); - peg$currPos += 2; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c31); } - } - if (s2 === peg$FAILED) { - if (input.substr(peg$currPos, 3).toLowerCase() === peg$c33) { - s2 = input.substr(peg$currPos, 3); - peg$currPos += 3; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c34); } - } - if (s2 === peg$FAILED) { - if (input.substr(peg$currPos, 3).toLowerCase() === peg$c36) { - s2 = input.substr(peg$currPos, 3); - peg$currPos += 3; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c37); } - } - } - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c65(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseKeyword() { - var s0; - - var key = peg$currPos * 37 + 29, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$parseOr(); - if (s0 === peg$FAILED) { - s0 = peg$parseAnd(); - if (s0 === peg$FAILED) { - s0 = peg$parseNot(); - } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseSpecialCharacter() { - var s0; - - var key = peg$currPos * 37 + 30, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - if (peg$c66.test(input.charAt(peg$currPos))) { - s0 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c67); } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseEscapedUnicodeSequence() { - var s0, s1, s2; - - var key = peg$currPos * 37 + 31, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 92) { - s1 = peg$c44; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c45); } - } - if (s1 !== peg$FAILED) { - s2 = peg$parseUnicodeSequence(); - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c68(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseUnicodeSequence() { - var s0, s1, s2, s3, s4, s5, s6, s7; - - var key = peg$currPos * 37 + 32, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 117) { - s1 = peg$c69; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c70); } - } - if (s1 !== peg$FAILED) { - s2 = peg$currPos; - s3 = peg$currPos; - s4 = peg$parseHexDigit(); - if (s4 !== peg$FAILED) { - s5 = peg$parseHexDigit(); - if (s5 !== peg$FAILED) { - s6 = peg$parseHexDigit(); - if (s6 !== peg$FAILED) { - s7 = peg$parseHexDigit(); - if (s7 !== peg$FAILED) { - s4 = [s4, s5, s6, s7]; - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - if (s3 !== peg$FAILED) { - s2 = input.substring(s2, peg$currPos); - } else { - s2 = s3; - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c71(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseHexDigit() { - var s0; - - var key = peg$currPos * 37 + 33, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - if (peg$c72.test(input.charAt(peg$currPos))) { - s0 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c73); } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseRangeOperator() { - var s0, s1; - - var key = peg$currPos * 37 + 34, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c74) { - s1 = peg$c74; - peg$currPos += 2; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c75); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c76(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c77) { - s1 = peg$c77; - peg$currPos += 2; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c78); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c79(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 60) { - s1 = peg$c80; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c81); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c82(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 62) { - s1 = peg$c83; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c84); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c85(); - } - s0 = s1; - } - } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseSpace() { - var s0, s1; - - var key = peg$currPos * 37 + 35, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - peg$silentFails++; - if (peg$c87.test(input.charAt(peg$currPos))) { - s0 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c88); } - } - peg$silentFails--; - if (s0 === peg$FAILED) { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c86); } - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - function peg$parseCursor() { - var s0, s1, s2; - - var key = peg$currPos * 37 + 36, - cached = peg$resultsCache[key]; - - if (cached) { - peg$currPos = cached.nextPos; - - return cached.result; - } - - s0 = peg$currPos; - peg$savedPos = peg$currPos; - s1 = peg$c39(); - if (s1) { - s1 = void 0; - } else { - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - if (input.substr(peg$currPos, 14) === peg$c89) { - s2 = peg$c89; - peg$currPos += 14; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c90); } - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c91(); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - peg$resultsCache[key] = { nextPos: peg$currPos, result: s0 }; - - return s0; - } - - - const { parseCursor, cursorSymbol, allowLeadingWildcards = true, helpers: { nodeTypes } } = options; - const buildFunctionNode = nodeTypes.function.buildNodeWithArgumentNodes; - const buildLiteralNode = nodeTypes.literal.buildNode; - const buildWildcardNode = nodeTypes.wildcard.buildNode; - const buildNamedArgNode = nodeTypes.namedArg.buildNode; - const { wildcardSymbol } = nodeTypes.wildcard; - - - peg$result = peg$startRuleFunction(); - - if (peg$result !== peg$FAILED && peg$currPos === input.length) { - return peg$result; - } else { - if (peg$result !== peg$FAILED && peg$currPos < input.length) { - peg$fail({ type: "end", description: "end of input" }); - } - - throw peg$buildException( - null, - peg$maxFailExpected, - peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, - peg$maxFailPos < input.length - ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) - : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) - ); - } - } - - return { - SyntaxError: peg$SyntaxError, - parse: peg$parse - }; -})(); \ No newline at end of file diff --git a/src/plugins/data/common/es_query/stubs/exists_filter.ts b/src/plugins/data/common/es_query/stubs/exists_filter.ts new file mode 100644 index 0000000000000..b10aa67db517e --- /dev/null +++ b/src/plugins/data/common/es_query/stubs/exists_filter.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 { ExistsFilter, FilterStateStore } from '..'; + +export const existsFilter: ExistsFilter = { + meta: { + index: 'logstash-*', + negate: false, + disabled: false, + type: 'exists', + key: 'machine.os', + alias: null, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, +}; diff --git a/src/plugins/data/common/es_query/filters/stubs/index.ts b/src/plugins/data/common/es_query/stubs/index.ts similarity index 100% rename from src/plugins/data/common/es_query/filters/stubs/index.ts rename to src/plugins/data/common/es_query/stubs/index.ts diff --git a/src/plugins/data/common/es_query/stubs/phrase_filter.ts b/src/plugins/data/common/es_query/stubs/phrase_filter.ts new file mode 100644 index 0000000000000..23b51afd64e51 --- /dev/null +++ b/src/plugins/data/common/es_query/stubs/phrase_filter.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { FilterStateStore, PhraseFilter } from '@kbn/es-query'; + +export const phraseFilter: PhraseFilter = { + meta: { + negate: false, + index: 'logstash-*', + type: 'phrase', + key: 'machine.os', + value: 'ios', + disabled: false, + alias: null, + params: { + query: 'ios', + }, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, +}; diff --git a/src/plugins/data/common/es_query/stubs/phrases_filter.ts b/src/plugins/data/common/es_query/stubs/phrases_filter.ts new file mode 100644 index 0000000000000..56c3af56175da --- /dev/null +++ b/src/plugins/data/common/es_query/stubs/phrases_filter.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 { FilterStateStore, PhrasesFilter } from '@kbn/es-query'; + +export const phrasesFilter: PhrasesFilter = { + meta: { + index: 'logstash-*', + type: 'phrases', + key: 'machine.os.raw', + value: 'win xp, osx', + params: ['win xp', 'osx'], + negate: false, + disabled: false, + alias: null, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, +}; diff --git a/src/plugins/data/common/es_query/stubs/range_filter.ts b/src/plugins/data/common/es_query/stubs/range_filter.ts new file mode 100644 index 0000000000000..485a569eb9d4b --- /dev/null +++ b/src/plugins/data/common/es_query/stubs/range_filter.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FilterStateStore, RangeFilter } from '@kbn/es-query'; + +export const rangeFilter: RangeFilter = { + meta: { + index: 'logstash-*', + negate: false, + disabled: false, + alias: null, + type: 'range', + key: 'bytes', + value: '0 to 10', + params: { + gte: 0, + lt: 10, + }, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + range: {}, +}; diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index 3b2e25d3d80a6..38258dd4f53f4 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -5,7 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { IndexPatternFieldBase, FieldSpec, IndexPattern } from '../..'; +import { IndexPatternFieldBase } from '@kbn/es-query'; +import { FieldSpec, IndexPattern } from '../..'; /** * @deprecated diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 58cc6d0478d5e..d05a7ea6e2d93 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ import type { estypes } from '@elastic/elasticsearch'; +import type { IndexPatternFieldBase, IFieldSubType, IndexPatternBase } from '@kbn/es-query'; import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications'; // eslint-disable-next-line import type { SavedObject } from 'src/core/server'; -import type { IndexPatternFieldBase, IFieldSubType, IndexPatternBase } from '../es_query'; import { IFieldType } from './fields'; import { RUNTIME_FIELD_TYPES } from './constants'; import { SerializedFieldFormat } from '../../../expressions/common'; diff --git a/src/plugins/data/common/kbn_field_types/kbn_field_types_factory.ts b/src/plugins/data/common/kbn_field_types/kbn_field_types_factory.ts index a48763a5ab788..2ded968c3cdf4 100644 --- a/src/plugins/data/common/kbn_field_types/kbn_field_types_factory.ts +++ b/src/plugins/data/common/kbn_field_types/kbn_field_types_factory.ts @@ -22,6 +22,7 @@ export const createKbnFieldTypes = (): KbnFieldType[] => [ ES_FIELD_TYPES.STRING, ES_FIELD_TYPES.TEXT, ES_FIELD_TYPES.KEYWORD, + ES_FIELD_TYPES.VERSION, ES_FIELD_TYPES._TYPE, ES_FIELD_TYPES._ID, ], diff --git a/src/plugins/data/common/kbn_field_types/types.ts b/src/plugins/data/common/kbn_field_types/types.ts index e6f815e058ce3..c14e7e4b03661 100644 --- a/src/plugins/data/common/kbn_field_types/types.ts +++ b/src/plugins/data/common/kbn_field_types/types.ts @@ -24,6 +24,7 @@ export enum ES_FIELD_TYPES { STRING = 'string', TEXT = 'text', KEYWORD = 'keyword', + VERSION = 'version', BOOLEAN = 'boolean', OBJECT = 'object', diff --git a/src/plugins/data/common/query/filter_manager/compare_filters.ts b/src/plugins/data/common/query/filter_manager/compare_filters.ts index a6190adc1ba65..fc820779b2461 100644 --- a/src/plugins/data/common/query/filter_manager/compare_filters.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.ts @@ -7,7 +7,7 @@ */ import { defaults, isEqual, omit, map } from 'lodash'; -import { FilterMeta, Filter } from '../../es_query'; +import { FilterMeta, Filter } from '@kbn/es-query'; export interface FilterCompareOptions { index?: boolean; diff --git a/src/plugins/data/common/query/persistable_state.test.ts b/src/plugins/data/common/query/persistable_state.test.ts index 62ea17e030413..807cc72a071be 100644 --- a/src/plugins/data/common/query/persistable_state.test.ts +++ b/src/plugins/data/common/query/persistable_state.test.ts @@ -7,7 +7,7 @@ */ import { extract, inject } from './persistable_state'; -import { Filter } from '../es_query/filters'; +import { Filter } from '@kbn/es-query'; describe('filter manager persistable state tests', () => { const filters: Filter[] = [ diff --git a/src/plugins/data/common/query/persistable_state.ts b/src/plugins/data/common/query/persistable_state.ts index ef0543bb84a2c..08cda6eb59fbf 100644 --- a/src/plugins/data/common/query/persistable_state.ts +++ b/src/plugins/data/common/query/persistable_state.ts @@ -7,9 +7,9 @@ */ import uuid from 'uuid'; +import { Filter } from '@kbn/es-query'; import { SerializableState } from '../../../kibana_utils/common/persistable_state'; import { SavedObjectReference } from '../../../../core/types'; -import { Filter } from '../es_query/filters'; export const extract = (filters: Filter[]) => { const references: SavedObjectReference[] = []; diff --git a/src/plugins/data/common/query/timefilter/get_time.ts b/src/plugins/data/common/query/timefilter/get_time.ts index 58194fc72dfcf..64842be20fbad 100644 --- a/src/plugins/data/common/query/timefilter/get_time.ts +++ b/src/plugins/data/common/query/timefilter/get_time.ts @@ -7,7 +7,8 @@ */ import dateMath from '@elastic/datemath'; -import { buildRangeFilter, IIndexPattern, TimeRange, TimeRangeBounds } from '../..'; +import { buildRangeFilter } from '@kbn/es-query'; +import { IIndexPattern, TimeRange, TimeRangeBounds } from '../..'; interface CalculateBoundsOptions { forceNow?: Date; diff --git a/src/plugins/data/common/query/types.ts b/src/plugins/data/common/query/types.ts index e4e386afdec77..c1861beb1ed90 100644 --- a/src/plugins/data/common/query/types.ts +++ b/src/plugins/data/common/query/types.ts @@ -8,8 +8,4 @@ export * from './timefilter/types'; -// eslint-disable-next-line -export type Query = { - query: string | { [key: string]: any }; - language: string; -}; +export { Query } from '@kbn/es-query'; diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index c205b46e077f0..c80becd271bba 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -10,6 +10,7 @@ import moment from 'moment'; import _, { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; +import { isRangeFilter } from '@kbn/es-query'; import type { estypes } from '@elastic/elasticsearch'; import { @@ -23,7 +24,7 @@ import { IAggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { AggGroupNames } from './agg_groups'; import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; -import { TimeRange, getTime, isRangeFilter, calculateBounds } from '../../../common'; +import { TimeRange, getTime, calculateBounds } from '../../../common'; import { IBucketAggConfig } from './buckets'; import { insertTimeShiftSplit, mergeTimeShifts } from './utils/time_splits'; diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts index 2a1cd873f6282..39fba23a42210 100644 --- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts +++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts @@ -8,7 +8,7 @@ import { isNumber, keys, values, find, each, cloneDeep, flatten } from 'lodash'; import { estypes } from '@elastic/elasticsearch'; -import { buildExistsFilter, buildPhrasesFilter, buildQueryFromFilters } from '../../../../common'; +import { buildExistsFilter, buildPhrasesFilter, buildQueryFromFilters } from '@kbn/es-query'; import { AggGroupNames } from '../agg_groups'; import { IAggConfigs } from '../agg_configs'; import { IBucketAggConfig } from './bucket_agg_type'; diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.ts index 2c0f35d28e989..1fd2250ec9e8b 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.ts @@ -7,8 +7,8 @@ */ import moment from 'moment'; +import { buildRangeFilter } from '@kbn/es-query'; import { IBucketDateHistogramAggConfig } from '../date_histogram'; -import { buildRangeFilter } from '../../../../../common'; export const createFilterDateHistogram = ( agg: IBucketDateHistogramAggConfig, diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.ts index 5ba80d35e2cb3..1231637771e32 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.ts @@ -7,9 +7,9 @@ */ import moment from 'moment'; +import { buildRangeFilter, RangeFilterParams } from '@kbn/es-query'; import { IBucketAggConfig } from '../bucket_agg_type'; import { DateRangeKey } from '../lib/date_range'; -import { buildRangeFilter, RangeFilterParams } from '../../../../../common'; export const createFilterDateRange = (agg: IBucketAggConfig, { from, to }: DateRangeKey) => { const filter: RangeFilterParams = {}; diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/filters.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/filters.ts index 948dac1d23b5b..a265584cf3765 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/filters.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/filters.ts @@ -7,8 +7,8 @@ */ import { get } from 'lodash'; +import { buildQueryFilter } from '@kbn/es-query'; import { IBucketAggConfig } from '../bucket_agg_type'; -import { buildQueryFilter } from '../../../../../common'; export const createFilterFilters = (aggConfig: IBucketAggConfig, key: string) => { // have the aggConfig write agg dsl params diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.ts index d104c8f5c57f5..d5c98dc51562c 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { buildRangeFilter, RangeFilterParams } from '../../../../../common'; +import { buildRangeFilter, RangeFilterParams } from '@kbn/es-query'; import { AggTypesDependencies } from '../../agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.ts index 374c2fdbcf705..bd16b0210a856 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ +import { buildRangeFilter, RangeFilterParams } from '@kbn/es-query'; import { CidrMask } from '../lib/cidr_mask'; import { IBucketAggConfig } from '../bucket_agg_type'; import { IpRangeKey } from '../lib/ip_range'; -import { buildRangeFilter, RangeFilterParams } from '../../../../../common'; export const createFilterIpRange = (aggConfig: IBucketAggConfig, key: IpRangeKey) => { let range: RangeFilterParams; diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts index 4c2f929aa0675..258f10f2fd15a 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { buildRangeFilter } from '../../../../../common'; +import { buildRangeFilter } from '@kbn/es-query'; import { AggTypesDependencies } from '../../agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts index 935b52dc55708..25aa3f334e4b8 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts @@ -6,13 +6,8 @@ * Side Public License, v 1. */ +import { buildPhrasesFilter, buildExistsFilter, buildPhraseFilter, Filter } from '@kbn/es-query'; import { IBucketAggConfig } from '../bucket_agg_type'; -import { - buildPhrasesFilter, - buildExistsFilter, - buildPhraseFilter, - Filter, -} from '../../../../../common'; export const createFilterTerms = (aggConfig: IBucketAggConfig, key: string, params: any) => { const field = aggConfig.params.field; diff --git a/src/plugins/data/common/search/aggs/buckets/filter.ts b/src/plugins/data/common/search/aggs/buckets/filter.ts index 900848bb9517f..2f04e71d0af87 100644 --- a/src/plugins/data/common/search/aggs/buckets/filter.ts +++ b/src/plugins/data/common/search/aggs/buckets/filter.ts @@ -8,13 +8,13 @@ import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { buildEsQuery, Query } from '@kbn/es-query'; import { BucketAggType } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { GeoBoundingBox } from './lib/geo_point'; import { aggFilterFnName } from './filter_fn'; import { BaseAggParams } from '../types'; -import { Query } from '../../../types'; -import { buildEsQuery, getEsQueryConfig } from '../../../es_query'; +import { getEsQueryConfig } from '../../../es_query'; const filterTitle = i18n.translate('data.search.aggs.buckets.filterTitle', { defaultMessage: 'Filter', diff --git a/src/plugins/data/common/search/aggs/buckets/filters.ts b/src/plugins/data/common/search/aggs/buckets/filters.ts index 107b86de04058..c2bb7a6d7c81f 100644 --- a/src/plugins/data/common/search/aggs/buckets/filters.ts +++ b/src/plugins/data/common/search/aggs/buckets/filters.ts @@ -8,13 +8,14 @@ import { i18n } from '@kbn/i18n'; import { size, transform, cloneDeep } from 'lodash'; +import { buildEsQuery, Query } from '@kbn/es-query'; import { createFilterFilters } from './create_filter/filters'; import { toAngularJSON } from '../utils'; import { BucketAggType } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { aggFiltersFnName } from './filters_fn'; -import { getEsQueryConfig, buildEsQuery, Query, UI_SETTINGS } from '../../../../common'; +import { getEsQueryConfig, UI_SETTINGS } from '../../../../common'; import { BaseAggParams } from '../types'; const filtersTitle = i18n.translate('data.search.aggs.buckets.filtersTitle', { diff --git a/src/plugins/data/common/search/expressions/esaggs/create_filter.test.ts b/src/plugins/data/common/search/expressions/esaggs/create_filter.test.ts index 591bcc4947b08..b78980cb5136e 100644 --- a/src/plugins/data/common/search/expressions/esaggs/create_filter.test.ts +++ b/src/plugins/data/common/search/expressions/esaggs/create_filter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { isRangeFilter } from '../../../es_query/filters'; +import { isRangeFilter } from '@kbn/es-query'; import { BytesFormat, FieldFormatsGetConfigFn } from '../../../field_formats'; import { AggConfigs, IAggConfig } from '../../aggs'; import { mockAggTypesRegistry } from '../../aggs/test_helpers'; diff --git a/src/plugins/data/common/search/expressions/esdsl.ts b/src/plugins/data/common/search/expressions/esdsl.ts index dee1b19eb3360..a5834001143e4 100644 --- a/src/plugins/data/common/search/expressions/esdsl.ts +++ b/src/plugins/data/common/search/expressions/esdsl.ts @@ -7,12 +7,13 @@ */ import { i18n } from '@kbn/i18n'; +import { buildEsQuery } from '@kbn/es-query'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { EsRawResponse } from './es_raw_response'; import { RequestStatistics, RequestAdapter } from '../../../../inspector/common'; import { ISearchGeneric, KibanaContext } from '..'; -import { buildEsQuery, getEsQueryConfig } from '../../es_query/es_query'; +import { getEsQueryConfig } from '../../es_query'; import { UiSettingsCommon } from '../../index_patterns'; const name = 'esdsl'; diff --git a/src/plugins/data/common/search/expressions/exists_filter.ts b/src/plugins/data/common/search/expressions/exists_filter.ts index 0979328860b4c..75d83ca7f2592 100644 --- a/src/plugins/data/common/search/expressions/exists_filter.ts +++ b/src/plugins/data/common/search/expressions/exists_filter.ts @@ -8,8 +8,8 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { buildFilter, FILTERS } from '@kbn/es-query'; import { KibanaField, KibanaFilter } from './kibana_context_type'; -import { buildFilter, FILTERS } from '../../es_query/filters'; import { IndexPattern } from '../../index_patterns/index_patterns'; interface Arguments { diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.ts b/src/plugins/data/common/search/expressions/filters_to_ast.ts index edcf884b3ed31..3eb3a11b09857 100644 --- a/src/plugins/data/common/search/expressions/filters_to_ast.ts +++ b/src/plugins/data/common/search/expressions/filters_to_ast.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ +import { Filter } from '@kbn/es-query'; import { buildExpression, buildExpressionFunction } from '../../../../expressions/common'; -import { Filter } from '../../es_query/filters'; import { ExpressionFunctionKibanaFilter } from './kibana_filter'; export const filtersToAst = (filters: Filter[] | Filter) => { diff --git a/src/plugins/data/common/search/expressions/kibana_context.test.ts b/src/plugins/data/common/search/expressions/kibana_context.test.ts new file mode 100644 index 0000000000000..77d89792b63c3 --- /dev/null +++ b/src/plugins/data/common/search/expressions/kibana_context.test.ts @@ -0,0 +1,264 @@ +/* + * Copyright 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 { FilterStateStore, buildFilter, FILTERS } from '@kbn/es-query'; +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { ExecutionContext } from 'src/plugins/expressions/common'; +import { KibanaContext } from './kibana_context_type'; + +import { + getKibanaContextFn, + ExpressionFunctionKibanaContext, + KibanaContextStartDependencies, +} from './kibana_context'; + +type StartServicesMock = DeeplyMockedKeys; + +const createExecutionContextMock = (): DeeplyMockedKeys => ({ + abortSignal: {} as any, + getExecutionContext: jest.fn(), + getSearchContext: jest.fn(), + getSearchSessionId: jest.fn(), + inspectorAdapters: jest.fn(), + types: {}, + variables: {}, + getKibanaRequest: jest.fn(), +}); + +const emptyArgs = { q: null, timeRange: null, savedSearchId: null }; + +describe('kibanaContextFn', () => { + let kibanaContextFn: ExpressionFunctionKibanaContext; + let startServicesMock: StartServicesMock; + + const getStartServicesMock = (): Promise => Promise.resolve(startServicesMock); + + beforeEach(async () => { + kibanaContextFn = getKibanaContextFn(getStartServicesMock); + startServicesMock = { + savedObjectsClient: { + create: jest.fn(), + delete: jest.fn(), + find: jest.fn(), + get: jest.fn(), + update: jest.fn(), + }, + }; + }); + + it('merges and deduplicates queries from different sources', async () => { + const { fn } = kibanaContextFn; + startServicesMock.savedObjectsClient.get.mockResolvedValue({ + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + query: [ + { + language: 'kuery', + query: { + match_phrase: { + DUPLICATE: 'DUPLICATE', + }, + }, + }, + { + language: 'kuery', + query: { + match_phrase: { + DUPLICATE: 'DUPLICATE', + }, + }, + }, + { + language: 'kuery', + query: { + match_phrase: { + test: 'something1', + }, + }, + }, + ], + }), + }, + }, + } as any); + const args = { + ...emptyArgs, + q: { + type: 'kibana_query' as 'kibana_query', + language: 'test', + query: { + type: 'test', + match_phrase: { + test: 'something2', + }, + }, + }, + savedSearchId: 'test', + }; + const input: KibanaContext = { + type: 'kibana_context', + query: [ + { + language: 'kuery', + query: [ + // TODO: Is it expected that if we pass in an array that the values in the array are not deduplicated? + { + language: 'kuery', + query: { + match_phrase: { + DUPLICATE: 'DUPLICATE', + }, + }, + }, + { + language: 'kuery', + query: { + match_phrase: { + DUPLICATE: 'DUPLICATE', + }, + }, + }, + { + language: 'kuery', + query: { + match_phrase: { + test: 'something3', + }, + }, + }, + ], + }, + ], + timeRange: { + from: 'now-24h', + to: 'now', + }, + }; + + const { query } = await fn(input, args, createExecutionContextMock()); + + expect(query).toEqual([ + { + language: 'kuery', + query: [ + { + language: 'kuery', + query: { + match_phrase: { + DUPLICATE: 'DUPLICATE', + }, + }, + }, + { + language: 'kuery', + query: { + match_phrase: { + DUPLICATE: 'DUPLICATE', + }, + }, + }, + { + language: 'kuery', + query: { + match_phrase: { + test: 'something3', + }, + }, + }, + ], + }, + { + type: 'kibana_query', + language: 'test', + query: { + type: 'test', + match_phrase: { + test: 'something2', + }, + }, + }, + { + language: 'kuery', + query: { + match_phrase: { + DUPLICATE: 'DUPLICATE', + }, + }, + }, + { + language: 'kuery', + query: { + match_phrase: { + test: 'something1', + }, + }, + }, + ]); + }); + + it('deduplicates duplicated filters and keeps the first enabled filter', async () => { + const { fn } = kibanaContextFn; + const filter1 = buildFilter( + { fields: [] }, + { name: 'test', type: 'test' }, + FILTERS.PHRASE, + false, + true, + { + query: 'JetBeats', + }, + null, + FilterStateStore.APP_STATE + ); + const filter2 = buildFilter( + { fields: [] }, + { name: 'test', type: 'test' }, + FILTERS.PHRASE, + false, + false, + { + query: 'JetBeats', + }, + null, + FilterStateStore.APP_STATE + ); + + const filter3 = buildFilter( + { fields: [] }, + { name: 'test', type: 'test' }, + FILTERS.PHRASE, + false, + false, + { + query: 'JetBeats', + }, + null, + FilterStateStore.APP_STATE + ); + + const input: KibanaContext = { + type: 'kibana_context', + query: [ + { + language: 'kuery', + query: '', + }, + ], + filters: [filter1, filter2, filter3], + timeRange: { + from: 'now-24h', + to: 'now', + }, + }; + + const { filters } = await fn(input, emptyArgs, createExecutionContextMock()); + expect(filters!.length).toBe(1); + expect(filters![0]).toBe(filter2); + }); +}); diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts index 22a7150d4a64e..8112777b9b0f3 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.ts @@ -10,6 +10,7 @@ import { uniqBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, ExecutionContext } from 'src/plugins/expressions/common'; import { Adapters } from 'src/plugins/inspector/common'; +import { Filter } from '@kbn/es-query'; import { unboxExpressionValue } from '../../../../expressions/common'; import { Query, uniqFilters } from '../../query'; import { ExecutionContextSearch, KibanaContext, KibanaFilter } from './kibana_context_type'; @@ -17,7 +18,6 @@ import { KibanaQueryOutput } from './kibana_context_type'; import { KibanaTimerangeOutput } from './timerange'; import { SavedObjectReference } from '../../../../../core/types'; import { SavedObjectsClientCommon } from '../../index_patterns'; -import { Filter } from '../../es_query/filters'; /** @internal */ export interface KibanaContextStartDependencies { @@ -146,7 +146,7 @@ export const getKibanaContextFn = ( return { type: 'kibana_context', query: queries, - filters: uniqFilters(filters).filter((f: any) => !f.meta?.disabled), + filters: uniqFilters(filters.filter((f: any) => !f.meta?.disabled)), timeRange, }; }, diff --git a/src/plugins/data/common/search/expressions/phrase_filter.ts b/src/plugins/data/common/search/expressions/phrase_filter.ts index 0b19e8a1e416d..837d4be4d52ea 100644 --- a/src/plugins/data/common/search/expressions/phrase_filter.ts +++ b/src/plugins/data/common/search/expressions/phrase_filter.ts @@ -8,8 +8,8 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { buildFilter, FILTERS } from '@kbn/es-query'; import { KibanaField, KibanaFilter } from './kibana_context_type'; -import { buildFilter, FILTERS } from '../../es_query/filters'; import { IndexPattern } from '../../index_patterns/index_patterns'; interface Arguments { diff --git a/src/plugins/data/common/search/expressions/range_filter.ts b/src/plugins/data/common/search/expressions/range_filter.ts index ed71f5362fe85..ed65475a9d285 100644 --- a/src/plugins/data/common/search/expressions/range_filter.ts +++ b/src/plugins/data/common/search/expressions/range_filter.ts @@ -8,8 +8,8 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { buildFilter, FILTERS } from '@kbn/es-query'; import { KibanaField, KibanaFilter } from './kibana_context_type'; -import { buildFilter, FILTERS } from '../../es_query/filters'; import { IndexPattern } from '../../index_patterns/index_patterns'; import { KibanaRange } from './range'; diff --git a/src/plugins/data/common/search/search_source/create_search_source.test.ts b/src/plugins/data/common/search/search_source/create_search_source.test.ts index 6a6ac1dfa93e7..c084b029a5bd2 100644 --- a/src/plugins/data/common/search/search_source/create_search_source.test.ts +++ b/src/plugins/data/common/search/search_source/create_search_source.test.ts @@ -10,7 +10,7 @@ import { createSearchSource as createSearchSourceFactory } from './create_search import { SearchSourceDependencies } from './search_source'; import { IIndexPattern } from '../../index_patterns'; import { IndexPatternsContract } from '../../index_patterns/index_patterns'; -import { Filter } from '../../es_query/filters'; +import { Filter } from '../../es_query'; describe('createSearchSource', () => { const indexPatternMock: IIndexPattern = {} as IIndexPattern; diff --git a/src/plugins/data/common/search/search_source/extract_references.ts b/src/plugins/data/common/search/search_source/extract_references.ts index b63b8ed1cfee2..f099443ef7605 100644 --- a/src/plugins/data/common/search/search_source/extract_references.ts +++ b/src/plugins/data/common/search/search_source/extract_references.ts @@ -7,7 +7,7 @@ */ import { SavedObjectReference } from 'src/core/types'; -import { Filter } from '../../es_query/filters'; +import { Filter } from '@kbn/es-query'; import { SearchSourceFields } from './types'; import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../constants'; diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index e60e6fa00b270..13f157da731a6 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -72,6 +72,7 @@ import { } from 'rxjs/operators'; import { defer, EMPTY, from, Observable } from 'rxjs'; import { estypes } from '@elastic/elasticsearch'; +import { buildEsQuery, Filter } from '@kbn/es-query'; import { normalizeSortRequest } from './normalize_sort_request'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; import { IIndexPattern, IndexPattern, IndexPatternField } from '../../index_patterns'; @@ -93,8 +94,6 @@ import { getRequestInspectorStats, getResponseInspectorStats } from './inspect'; import { getEsQueryConfig, - buildEsQuery, - Filter, UI_SETTINGS, isErrorResponse, isPartialResponse, diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts index a7ba8ab9576b6..8e3c298aa9316 100644 --- a/src/plugins/data/common/search/session/types.ts +++ b/src/plugins/data/common/search/session/types.ts @@ -71,6 +71,10 @@ export interface SearchSessionSavedObjectAttributes { realmType?: string; realmName?: string; username?: string; + /** + * Version information to display warnings when trying to restore a session from a different version + */ + version: string; } export interface SearchSessionRequestInfo { diff --git a/src/plugins/data/common/search/tabify/types.ts b/src/plugins/data/common/search/tabify/types.ts index c170b13774932..758a2dfb181f2 100644 --- a/src/plugins/data/common/search/tabify/types.ts +++ b/src/plugins/data/common/search/tabify/types.ts @@ -7,7 +7,7 @@ */ import { Moment } from 'moment'; -import { RangeFilterParams } from '../../../common'; +import { RangeFilterParams } from '@kbn/es-query'; import { IAggConfig } from '../aggs'; /** @internal **/ diff --git a/src/plugins/data/common/stubs.ts b/src/plugins/data/common/stubs.ts index 25f9dda7d33b4..d64d788d60ead 100644 --- a/src/plugins/data/common/stubs.ts +++ b/src/plugins/data/common/stubs.ts @@ -8,4 +8,4 @@ export { stubIndexPattern, stubIndexPatternWithFields } from './index_patterns/index_pattern.stub'; export { stubFields } from './index_patterns/field.stub'; -export * from './es_query/filters/stubs'; +export * from './es_query/stubs'; diff --git a/src/plugins/data/common/types.ts b/src/plugins/data/common/types.ts index 8072928a1b670..7f6ae4a346bd0 100644 --- a/src/plugins/data/common/types.ts +++ b/src/plugins/data/common/types.ts @@ -21,3 +21,9 @@ export * from './index_patterns/types'; * not possible. */ export type GetConfigFn = (key: string, defaultOverride?: T) => T; + +type FilterFormatterFunction = (value: any) => string; +export interface FilterValueFormatter { + convert: FilterFormatterFunction; + getConverterFor: (type: string) => FilterFormatterFunction; +} diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index 9644a96a68752..eb8e991cb8891 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ +import type { Filter } from '@kbn/es-query'; import { Datatable } from 'src/plugins/expressions/public'; import { Action, createAction, UiActionsStart } from '../../../../plugins/ui_actions/public'; import { APPLY_FILTER_TRIGGER } from '../triggers'; import { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click'; -import type { Filter } from '../../common/es_query/filters'; export type ValueClickActionContext = ValueClickContext; export const ACTION_VALUE_CLICK = 'ACTION_VALUE_CLICK'; diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/types.ts b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/types.ts index 48e87a73f3671..12a0dae97ebaa 100644 --- a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/types.ts +++ b/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/types.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ +import { KueryNode } from '@kbn/es-query'; import { CoreSetup } from 'kibana/public'; import { DataPublicPluginStart, - KueryNode, QuerySuggestionBasic, QuerySuggestionGetFnArgs, } from '../../../../../../../src/plugins/data/public'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 51f3d9fd660e5..e6a5a02123c2b 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -6,6 +6,10 @@ * Side Public License, v 1. */ +/* + * esQuery and esKuery: + */ + import { PluginInitializerContext } from '../../../core/public'; import { ConfigSchema } from '../config'; @@ -14,15 +18,6 @@ import { ConfigSchema } from '../config'; */ import { - buildEmptyFilter, - buildExistsFilter, - buildPhraseFilter, - buildPhrasesFilter, - buildQueryFilter, - buildRangeFilter, - disableFilter, - FILTERS, - FilterStateStore, getPhraseFilterField, getPhraseFilterValue, isExistsFilter, @@ -34,6 +29,22 @@ import { isQueryStringFilter, isRangeFilter, toggleFilterNegated, + buildEmptyFilter, + buildExistsFilter, + buildPhraseFilter, + buildPhrasesFilter, + buildQueryFilter, + buildRangeFilter, + disableFilter, + fromKueryExpression, + toElasticsearchQuery, + nodeTypes, + buildEsQuery, + buildQueryFromFilters, + luceneStringToDsl, + decorateQuery, + FILTERS, + FilterStateStore, compareFilters, COMPARE_ALL_OPTIONS, } from '../common'; @@ -94,7 +105,8 @@ export const esFilters = { extractTimeRange, }; -export type { +export { + KueryNode, RangeFilter, RangeFilterMeta, RangeFilterParams, @@ -103,29 +115,26 @@ export type { PhraseFilter, CustomFilter, MatchAllFilter, + IFieldSubType, + EsQueryConfig, + isFilter, + isFilters, } from '../common'; -/* - * esQuery and esKuery: - */ - -import { - fromKueryExpression, - toElasticsearchQuery, - nodeTypes, - buildEsQuery, - getEsQueryConfig, - buildQueryFromFilters, - luceneStringToDsl, - decorateQuery, -} from '../common'; +import { getEsQueryConfig } from '../common'; +/** + * @deprecated Please import helpers from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ export const esKuery = { nodeTypes, fromKueryExpression, toElasticsearchQuery, }; +/** + * @deprecated Please import helpers from the package kbn/es-query directly. This import will be deprecated in v8.0.0. + */ export const esQuery = { buildEsQuery, getEsQueryConfig, @@ -134,8 +143,6 @@ export const esQuery = { decorateQuery, }; -export { EsQueryConfig, KueryNode } from '../common'; - /* * Field Formatters: */ @@ -260,7 +267,6 @@ export { export { IIndexPattern, IFieldType, - IFieldSubType, ES_FIELD_TYPES, KBN_FIELD_TYPES, IndexPatternAttributes, @@ -489,7 +495,7 @@ export { getKbnTypeNames, } from '../common'; -export { isTimeRange, isQuery, isFilter, isFilters } from '../common'; +export { isTimeRange, isQuery } from '../common'; export { ACTION_GLOBAL_APPLY_FILTER, ApplyGlobalFilterActionContext } from './actions'; export { APPLY_FILTER_TRIGGER } from './triggers'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 0d075e6c3872f..781d073b34978 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -17,6 +17,7 @@ import { CoreSetup } from 'src/core/public'; import { CoreSetup as CoreSetup_2 } from 'kibana/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; +import { CustomFilter as CustomFilter_2 } from '@kbn/es-query'; import { Datatable as Datatable_2 } from 'src/plugins/expressions'; import { Datatable as Datatable_3 } from 'src/plugins/expressions/common'; import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions'; @@ -25,6 +26,7 @@ import { DetailedPeerCertificate } from 'tls'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; +import { EsQueryConfig as EsQueryConfig_2 } from '@kbn/es-query'; import { estypes } from '@elastic/elasticsearch'; import { EuiBreadcrumb } from '@elastic/eui'; import { EuiButtonEmptyProps } from '@elastic/eui'; @@ -35,10 +37,13 @@ import { EuiGlobalToastListToast } from '@elastic/eui'; import { EuiIconProps } from '@elastic/eui'; import { EventEmitter } from 'events'; import { ExecutionContext } from 'src/plugins/expressions/common'; +import { ExistsFilter as ExistsFilter_2 } from '@kbn/es-query'; import { ExpressionAstExpression } from 'src/plugins/expressions/common'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; +import { Filter as Filter_2 } from '@kbn/es-query'; +import { FilterStateStore } from '@kbn/es-query'; import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils'; import { History } from 'history'; import { Href } from 'history'; @@ -46,19 +51,23 @@ import { HttpSetup } from 'kibana/public'; import { IAggConfigs as IAggConfigs_2 } from 'src/plugins/data/public'; import { IconType } from '@elastic/eui'; import { IEsSearchResponse as IEsSearchResponse_2 } from 'src/plugins/data/public'; +import { IFieldSubType as IFieldSubType_2 } from '@kbn/es-query'; import { IncomingHttpHeaders } from 'http'; +import { IndexPatternBase } from '@kbn/es-query'; +import { IndexPatternFieldBase } from '@kbn/es-query'; import { InjectedIntl } from '@kbn/i18n/react'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource as ISearchSource_2 } from 'src/plugins/data/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IUiSettingsClient } from 'src/core/public'; -import { JsonValue } from '@kbn/common-utils'; import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { KibanaExecutionContext } from 'src/core/public'; +import { KueryNode as KueryNode_2 } from '@kbn/es-query'; import { Location } from 'history'; import { LocationDescriptorObject } from 'history'; import { Logger } from '@kbn/logging'; import { LogMeta } from '@kbn/logging'; +import { MatchAllFilter as MatchAllFilter_2 } from '@kbn/es-query'; import { MaybePromise } from '@kbn/utility-types'; import { Moment } from 'moment'; import moment from 'moment'; @@ -68,6 +77,8 @@ import { Observable } from 'rxjs'; import { PackageInfo } from '@kbn/config'; import { Path } from 'history'; import { PeerCertificate } from 'tls'; +import { PhraseFilter as PhraseFilter_2 } from '@kbn/es-query'; +import { PhrasesFilter as PhrasesFilter_2 } from '@kbn/es-query'; import { Plugin } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public'; @@ -75,7 +86,10 @@ import { PopoverAnchorPosition } from '@elastic/eui'; import { PublicContract } from '@kbn/utility-types'; import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams } from 'src/core/server/types'; -import { RangeFilter as RangeFilter_2 } from 'src/plugins/data/public'; +import { Query } from '@kbn/es-query'; +import { RangeFilter as RangeFilter_2 } from '@kbn/es-query'; +import { RangeFilterMeta as RangeFilterMeta_2 } from '@kbn/es-query'; +import { RangeFilterParams as RangeFilterParams_2 } from '@kbn/es-query'; import React from 'react'; import * as React_2 from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; @@ -262,7 +276,7 @@ export class AggConfigs { getResponseAggById(id: string): AggConfig | undefined; getResponseAggs(): AggConfig[]; // (undocumented) - getSearchSourceTimeFilter(forceNow?: Date): RangeFilter_2[] | { + getSearchSourceTimeFilter(forceNow?: Date): import("@kbn/es-query").RangeFilter[] | { meta: { index: string | undefined; params: {}; @@ -617,10 +631,8 @@ export const createSavedQueryService: (savedObjectsClient: SavedObjectsClientCon // Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export type CustomFilter = Filter & { - query: any; -}; +// @public @deprecated (undocumented) +export type CustomFilter = CustomFilter_2; // Warning: (ae-forgotten-export) The symbol "DataSetupDependencies" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DataStartDependencies" needs to be exported by the entry point index.d.ts @@ -773,7 +785,9 @@ export enum ES_FIELD_TYPES { // (undocumented) _TYPE = "_type", // (undocumented) - UNSIGNED_LONG = "unsigned_long" + UNSIGNED_LONG = "unsigned_long", + // (undocumented) + VERSION = "version" } // Warning: (ae-missing-release-tag) "ES_SEARCH_STRATEGY" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -804,23 +818,23 @@ export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition JSX.Element; FilterItem: (props: import("./ui/filter_bar/filter_item").FilterItemProps) => JSX.Element; - FILTERS: typeof FILTERS; + FILTERS: typeof import("@kbn/es-query").FILTERS; FilterStateStore: typeof FilterStateStore; - buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; - buildPhrasesFilter: (field: import("../common").IndexPatternFieldBase, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter; - buildExistsFilter: (field: import("../common").IndexPatternFieldBase, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter; - buildPhraseFilter: (field: import("../common").IndexPatternFieldBase, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter; - buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter; - buildRangeFilter: (field: import("../common").IndexPatternFieldBase, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter; - isPhraseFilter: (filter: any) => filter is import("../common").PhraseFilter; - isExistsFilter: (filter: any) => filter is import("../common").ExistsFilter; - isPhrasesFilter: (filter: any) => filter is import("../common").PhrasesFilter; - isRangeFilter: (filter: any) => filter is import("../common").RangeFilter; - isMatchAllFilter: (filter: any) => filter is import("../common").MatchAllFilter; - isMissingFilter: (filter: any) => filter is import("../common").MissingFilter; - isQueryStringFilter: (filter: any) => filter is import("../common").QueryStringFilter; - isFilterPinned: (filter: import("../common").Filter) => boolean | undefined; - toggleFilterNegated: (filter: import("../common").Filter) => { + buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("@kbn/es-query").Filter; + buildPhrasesFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, params: any[], indexPattern: import("@kbn/es-query").IndexPatternBase) => import("@kbn/es-query").PhrasesFilter; + buildExistsFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, indexPattern: import("@kbn/es-query").IndexPatternBase) => import("@kbn/es-query").ExistsFilter; + buildPhraseFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, value: any, indexPattern: import("@kbn/es-query").IndexPatternBase) => import("@kbn/es-query").PhraseFilter; + buildQueryFilter: (query: any, index: string, alias: string) => import("@kbn/es-query").QueryStringFilter; + buildRangeFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, params: import("@kbn/es-query").RangeFilterParams, indexPattern: import("@kbn/es-query").IndexPatternBase, formattedValue?: string | undefined) => import("@kbn/es-query").RangeFilter; + isPhraseFilter: (filter: any) => filter is import("@kbn/es-query").PhraseFilter; + isExistsFilter: (filter: any) => filter is import("@kbn/es-query").ExistsFilter; + isPhrasesFilter: (filter: any) => filter is import("@kbn/es-query").PhrasesFilter; + isRangeFilter: (filter: any) => filter is import("@kbn/es-query").RangeFilter; + isMatchAllFilter: (filter: any) => filter is import("@kbn/es-query").MatchAllFilter; + isMissingFilter: (filter: any) => filter is import("@kbn/es-query").MissingFilter; + isQueryStringFilter: (filter: any) => filter is import("@kbn/es-query").QueryStringFilter; + isFilterPinned: (filter: import("@kbn/es-query").Filter) => boolean | undefined; + toggleFilterNegated: (filter: import("@kbn/es-query").Filter) => { meta: { negate: boolean; alias: string | null; @@ -833,62 +847,53 @@ export const esFilters: { params?: any; value?: string | undefined; }; - $state?: import("../common").FilterState | undefined; + $state?: import("@kbn/es-query/target_types/filters/types").FilterState | undefined; query?: any; }; - disableFilter: (filter: import("../common").Filter) => import("../common").Filter; - getPhraseFilterField: (filter: import("../common").PhraseFilter) => string; - getPhraseFilterValue: (filter: import("../common").PhraseFilter) => string | number | boolean; + disableFilter: (filter: import("@kbn/es-query").Filter) => import("@kbn/es-query").Filter; + getPhraseFilterField: (filter: import("@kbn/es-query").PhraseFilter) => string; + getPhraseFilterValue: (filter: import("@kbn/es-query").PhraseFilter) => string | number | boolean; getDisplayValueFromFilter: typeof getDisplayValueFromFilter; - compareFilters: (first: import("../common").Filter | import("../common").Filter[], second: import("../common").Filter | import("../common").Filter[], comparatorOptions?: import("../common").FilterCompareOptions) => boolean; + compareFilters: (first: import("@kbn/es-query").Filter | import("@kbn/es-query").Filter[], second: import("@kbn/es-query").Filter | import("@kbn/es-query").Filter[], comparatorOptions?: import("../common").FilterCompareOptions) => boolean; COMPARE_ALL_OPTIONS: import("../common").FilterCompareOptions; generateFilters: typeof generateFilters; - onlyDisabledFiltersChanged: (newFilters?: import("../common").Filter[] | undefined, oldFilters?: import("../common").Filter[] | undefined) => boolean; + onlyDisabledFiltersChanged: (newFilters?: import("@kbn/es-query").Filter[] | undefined, oldFilters?: import("@kbn/es-query").Filter[] | undefined) => boolean; changeTimeFilter: typeof changeTimeFilter; convertRangeFilterToTimeRangeString: typeof convertRangeFilterToTimeRangeString; - mapAndFlattenFilters: (filters: import("../common").Filter[]) => import("../common").Filter[]; + mapAndFlattenFilters: (filters: import("@kbn/es-query").Filter[]) => import("@kbn/es-query").Filter[]; extractTimeFilter: typeof extractTimeFilter; extractTimeRange: typeof extractTimeRange; }; // Warning: (ae-missing-release-tag) "esKuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export const esKuery: { - nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; - fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; + fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; + toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; }; // Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export const esQuery: { - buildEsQuery: typeof buildEsQuery; + buildEsQuery: typeof import("@kbn/es-query").buildEsQuery; getEsQueryConfig: typeof getEsQueryConfig; - buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { + buildQueryFromFilters: (filters: import("@kbn/es-query").Filter[] | undefined, indexPattern: import("@kbn/es-query").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean | undefined) => { must: never[]; - filter: import("../common").Filter[]; + filter: import("@kbn/es-query").Filter[]; should: never[]; - must_not: import("../common").Filter[]; + must_not: import("@kbn/es-query").Filter[]; }; - luceneStringToDsl: typeof luceneStringToDsl; - decorateQuery: typeof decorateQuery; + luceneStringToDsl: typeof import("@kbn/es-query").luceneStringToDsl; + decorateQuery: typeof import("@kbn/es-query").decorateQuery; }; // Warning: (ae-missing-release-tag) "EsQueryConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export interface EsQueryConfig { - // (undocumented) - allowLeadingWildcards: boolean; - // (undocumented) - dateFormatTZ?: string; - // (undocumented) - ignoreFilterIfFieldNotInIndex: boolean; - // (undocumented) - queryStringOptions: Record; -} +// @public @deprecated (undocumented) +export type EsQueryConfig = EsQueryConfig_2; // Warning: (ae-forgotten-export) The symbol "SortDirectionNumeric" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SortDirectionFormat" needs to be exported by the entry point index.d.ts @@ -916,11 +921,8 @@ export type ExecutionContextSearch = { // Warning: (ae-missing-release-tag) "ExistsFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export type ExistsFilter = Filter & { - meta: ExistsFilterMeta; - exists?: FilterExistsProperty; -}; +// @public @deprecated (undocumented) +export type ExistsFilter = ExistsFilter_2; // Warning: (ae-missing-release-tag) "exporters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1079,12 +1081,8 @@ export const fieldList: (specs?: FieldSpec[], shortDotsEnable?: boolean) => IInd // Warning: (ae-missing-release-tag) "Filter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export type Filter = { - $state?: FilterState; - meta: FilterMeta; - query?: any; -}; +// @public @deprecated (undocumented) +export type Filter = Filter_2; // Warning: (ae-forgotten-export) The symbol "PersistableStateService" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "FilterManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1093,19 +1091,19 @@ export type Filter = { export class FilterManager implements PersistableStateService { constructor(uiSettings: IUiSettingsClient); // (undocumented) - addFilters(filters: Filter[] | Filter, pinFilterStatus?: boolean): void; + addFilters(filters: Filter_2[] | Filter_2, pinFilterStatus?: boolean): void; // (undocumented) extract: any; // (undocumented) getAllMigrations: () => {}; // (undocumented) - getAppFilters(): Filter[]; + getAppFilters(): Filter_2[]; // (undocumented) getFetches$(): import("rxjs").Observable; // (undocumented) - getFilters(): Filter[]; + getFilters(): Filter_2[]; // (undocumented) - getGlobalFilters(): Filter[]; + getGlobalFilters(): Filter_2[]; // Warning: (ae-forgotten-export) The symbol "PartitionedFilters" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1119,13 +1117,13 @@ export class FilterManager implements PersistableStateService { // (undocumented) removeAll(): void; // (undocumented) - removeFilter(filter: Filter): void; - setAppFilters(newAppFilters: Filter[]): void; + removeFilter(filter: Filter_2): void; + setAppFilters(newAppFilters: Filter_2[]): void; // (undocumented) - setFilters(newFilters: Filter[], pinFilterStatus?: boolean): void; + setFilters(newFilters: Filter_2[], pinFilterStatus?: boolean): void; // (undocumented) - static setFiltersStore(filters: Filter[], store: FilterStateStore, shouldOverrideStore?: boolean): void; - setGlobalFilters(newGlobalFilters: Filter[]): void; + static setFiltersStore(filters: Filter_2[], store: FilterStateStore, shouldOverrideStore?: boolean): void; + setGlobalFilters(newGlobalFilters: Filter_2[]): void; // (undocumented) telemetry: (filters: import("../../../../kibana_utils/common/persistable_state").SerializableState, collector: unknown) => {}; } @@ -1164,7 +1162,7 @@ export function getSearchParamsFromRequest(searchRequest: SearchRequest, depende export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: { forceNow?: Date; fieldName?: string; -}): import("../..").RangeFilter | undefined; +}): import("@kbn/es-query").RangeFilter | undefined; // Warning: (ae-missing-release-tag) "IAggConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1242,19 +1240,9 @@ export type IFieldParamType = FieldParamType; // Warning: (ae-missing-release-tag) "IFieldSubType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export interface IFieldSubType { - // (undocumented) - multi?: { - parent: string; - }; - // (undocumented) - nested?: { - path: string; - }; -} +// @public @deprecated (undocumented) +export type IFieldSubType = IFieldSubType_2; -// Warning: (ae-forgotten-export) The symbol "IndexPatternFieldBase" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IFieldType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated (undocumented) @@ -1287,7 +1275,6 @@ export interface IFieldType extends IndexPatternFieldBase { visualizable?: boolean; } -// Warning: (ae-forgotten-export) The symbol "IndexPatternBase" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated (undocumented) @@ -1565,7 +1552,7 @@ export class IndexPatternField implements IFieldType { // (undocumented) readonly spec: FieldSpec; // (undocumented) - get subType(): import("../..").IFieldSubType | undefined; + get subType(): import("@kbn/es-query").IFieldSubType | undefined; // (undocumented) toJSON(): { count: number; @@ -1579,7 +1566,7 @@ export class IndexPatternField implements IFieldType { searchable: boolean; aggregatable: boolean; readFromDocValues: boolean; - subType: import("../..").IFieldSubType | undefined; + subType: import("@kbn/es-query").IFieldSubType | undefined; customLabel: string | undefined; }; // (undocumented) @@ -1826,13 +1813,13 @@ export type ISessionService = PublicContract; // Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export const isFilter: (x: unknown) => x is Filter; +// @public @deprecated (undocumented) +export const isFilter: (x: unknown) => x is Filter_2; // Warning: (ae-missing-release-tag) "isFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export const isFilters: (x: unknown) => x is Filter[]; +// @public @deprecated (undocumented) +export const isFilters: (x: unknown) => x is Filter_2[]; // Warning: (ae-missing-release-tag) "isPartialResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1898,23 +1885,13 @@ export type KibanaContext = ExpressionValueSearchContext; // Warning: (ae-missing-release-tag) "KueryNode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export interface KueryNode { - // (undocumented) - [key: string]: any; - // Warning: (ae-forgotten-export) The symbol "NodeTypes" needs to be exported by the entry point index.d.ts - // - // (undocumented) - type: keyof NodeTypes; -} +// @public @deprecated (undocumented) +export type KueryNode = KueryNode_2; // Warning: (ae-missing-release-tag) "MatchAllFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export type MatchAllFilter = Filter & { - meta: MatchAllFilterMeta; - match_all: any; -}; +// @public @deprecated (undocumented) +export type MatchAllFilter = MatchAllFilter_2; // Warning: (ae-missing-release-tag) "METRIC_TYPES" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2023,24 +2000,13 @@ export const parseSearchSourceJSON: (searchSourceJSON: string) => SearchSourceFi // Warning: (ae-missing-release-tag) "PhraseFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export type PhraseFilter = Filter & { - meta: PhraseFilterMeta; - script?: { - script: { - source?: any; - lang?: estypes.ScriptLanguage; - params: any; - }; - }; -}; +// @public @deprecated (undocumented) +export type PhraseFilter = PhraseFilter_2; // Warning: (ae-missing-release-tag) "PhrasesFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export type PhrasesFilter = Filter & { - meta: PhrasesFilterMeta; -}; +// @public @deprecated (undocumented) +export type PhrasesFilter = PhrasesFilter_2; // Warning: (ae-forgotten-export) The symbol "PluginInitializerContext" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "plugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2048,15 +2014,7 @@ export type PhrasesFilter = Filter & { // @public (undocumented) export function plugin(initializerContext: PluginInitializerContext): DataPlugin; -// Warning: (ae-missing-release-tag) "Query" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type Query = { - query: string | { - [key: string]: any; - }; - language: string; -}; +export { Query } // Warning: (ae-forgotten-export) The symbol "QueryService" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "QueryStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2226,50 +2184,20 @@ export enum QuerySuggestionTypes { Value = "value" } -// Warning: (ae-forgotten-export) The symbol "EsRangeFilter" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "RangeFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export type RangeFilter = Filter & EsRangeFilter & { - meta: RangeFilterMeta; - script?: { - script: { - params: any; - lang: estypes.ScriptLanguage; - source: any; - }; - }; - match_all?: any; -}; +// @public @deprecated (undocumented) +export type RangeFilter = RangeFilter_2; // Warning: (ae-missing-release-tag) "RangeFilterMeta" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export type RangeFilterMeta = FilterMeta & { - params: RangeFilterParams; - field?: any; - formattedValue?: string; -}; +// @public @deprecated (undocumented) +export type RangeFilterMeta = RangeFilterMeta_2; // Warning: (ae-missing-release-tag) "RangeFilterParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export interface RangeFilterParams { - // (undocumented) - format?: string; - // (undocumented) - from?: number | string; - // (undocumented) - gt?: number | string; - // (undocumented) - gte?: number | string; - // (undocumented) - lt?: number | string; - // (undocumented) - lte?: number | string; - // (undocumented) - to?: number | string; -} +// @public @deprecated (undocumented) +export type RangeFilterParams = RangeFilterParams_2; // Warning: (ae-missing-release-tag) "Reason" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2565,6 +2493,8 @@ export interface SearchSourceFields { index?: IndexPattern; // (undocumented) parent?: SearchSourceFields; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported + // // (undocumented) query?: Query; // Warning: (ae-forgotten-export) The symbol "EsQuerySearchAfter" needs to be exported by the entry point index.d.ts @@ -2754,65 +2684,53 @@ export interface WaitUntilNextSessionCompletesOptions { // Warnings were encountered during analysis: // -// src/plugins/data/common/es_query/filters/exists_filter.ts:19:3 - (ae-forgotten-export) The symbol "ExistsFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/exists_filter.ts:20:3 - (ae-forgotten-export) The symbol "FilterExistsProperty" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/match_all_filter.ts:17:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/meta_filter.ts:44:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/phrase_filter.ts:22:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/phrases_filter.ts:20:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:129:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:56:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "convertRangeFilterToTimeRangeString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "extractTimeRange" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:129:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:129:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:129:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:129:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:170:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:213:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:240:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:240:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:240:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:240:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:240:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:432:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:435:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:436:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:439:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "convertRangeFilterToTimeRangeString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "extractTimeRange" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:138:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:220:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:247:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:247:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:247:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:247:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:247:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:432:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:437:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:438:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:441:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:442:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:445:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:62:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index d47e4fb33f2a5..d514e0eb18705 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -11,6 +11,7 @@ import { Subject } from 'rxjs'; import { IUiSettingsClient } from 'src/core/public'; +import { isFilterPinned, Filter } from '@kbn/es-query'; import { sortFilters } from './lib/sort_filters'; import { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; import { onlyDisabledFiltersChanged } from './lib/only_disabled'; @@ -18,9 +19,7 @@ import { PartitionedFilters } from './types'; import { FilterStateStore, - Filter, uniqFilters, - isFilterPinned, compareFilters, COMPARE_ALL_OPTIONS, UI_SETTINGS, 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 0a4998a159523..566b26b0698dd 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 @@ -8,8 +8,6 @@ import _ from 'lodash'; import { - IFieldType, - IIndexPattern, Filter, isExistsFilter, isPhraseFilter, @@ -19,7 +17,9 @@ import { buildFilter, FilterStateStore, FILTERS, -} from '../../../../common'; +} from '@kbn/es-query'; + +import { IFieldType, IIndexPattern } from '../../../../common'; import { FilterManager } from '../filter_manager'; function getExistingFilter( diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_default.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_default.ts index f4abd65385da2..c30965e777c46 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_default.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_default.ts @@ -7,7 +7,7 @@ */ import { find, keys, get } from 'lodash'; -import { Filter, FILTERS } from '../../../../../common'; +import { Filter, FILTERS } from '@kbn/es-query'; export const mapDefault = (filter: Filter) => { const metaProperty = /(^\$|meta)/; diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_exists.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_exists.ts index b40701726d52d..19ebc66ae2d46 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_exists.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_exists.ts @@ -7,7 +7,7 @@ */ import { get } from 'lodash'; -import { Filter, isExistsFilter, FILTERS } from '../../../../../common'; +import { Filter, isExistsFilter, FILTERS } from '@kbn/es-query'; export const mapExists = (filter: Filter) => { if (isExistsFilter(filter)) { diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_bounding_box.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_bounding_box.ts index d1e2a649c400f..dfa8e862e1b4c 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_bounding_box.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_bounding_box.ts @@ -6,13 +6,9 @@ * Side Public License, v 1. */ -import { - FilterValueFormatter, - GeoBoundingBoxFilter, - FILTERS, - isGeoBoundingBoxFilter, - Filter, -} from '../../../../../common'; +import { GeoBoundingBoxFilter, FILTERS, isGeoBoundingBoxFilter, Filter } from '@kbn/es-query'; + +import { FilterValueFormatter } from '../../../../../common'; const getFormattedValueFn = (params: any) => { return (formatter?: FilterValueFormatter) => { diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.ts index 7bcedd9a7f951..e1c21aae64da6 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.ts @@ -6,13 +6,9 @@ * Side Public License, v 1. */ -import { - FilterValueFormatter, - GeoPolygonFilter, - FILTERS, - Filter, - isGeoPolygonFilter, -} from '../../../../../common'; +import { GeoPolygonFilter, FILTERS, Filter, isGeoPolygonFilter } from '@kbn/es-query'; + +import { FilterValueFormatter } from '../../../../../common'; const POINTS_SEPARATOR = ', '; diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_match_all.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_match_all.ts index a3c9086676f66..32231492b2f22 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_match_all.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_match_all.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Filter, isMatchAllFilter, FILTERS } from '../../../../../common'; +import { Filter, isMatchAllFilter, FILTERS } from '@kbn/es-query'; export const mapMatchAll = (filter: Filter) => { if (isMatchAllFilter(filter)) { diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_missing.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_missing.ts index b14fe4fb9da0f..41af3760a652e 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_missing.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_missing.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Filter, isMissingFilter, FILTERS } from '../../../../../common'; +import { Filter, isMissingFilter, FILTERS } from '@kbn/es-query'; export const mapMissing = (filter: Filter) => { if (isMissingFilter(filter)) { diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.ts index d8eb14ec5a67c..64576d4978c99 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrase.ts @@ -9,14 +9,15 @@ import { get } from 'lodash'; import { PhraseFilter, - FilterValueFormatter, getPhraseFilterValue, getPhraseFilterField, FILTERS, isScriptedPhraseFilter, Filter, isPhraseFilter, -} from '../../../../../common'; +} from '@kbn/es-query'; + +import { FilterValueFormatter } from '../../../../../common'; const getScriptedPhraseValue = (filter: PhraseFilter) => get(filter, ['script', 'script', 'params', 'value']); diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.ts index 5601dd66e5206..9ffdd3070e43a 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ -import { Filter, FilterValueFormatter, isPhrasesFilter } from '../../../../../common'; +import { Filter, isPhrasesFilter } from '@kbn/es-query'; + +import { FilterValueFormatter } from '../../../../../common'; const getFormattedValueFn = (params: any) => { return (formatter?: FilterValueFormatter) => { diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_query_string.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_query_string.ts index 9c11f74b60516..89355b6d1f423 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_query_string.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_query_string.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FILTERS, Filter, isQueryStringFilter } from '../../../../../common'; +import { FILTERS, Filter, isQueryStringFilter } from '@kbn/es-query'; export const mapQueryString = (filter: Filter) => { if (isQueryStringFilter(filter)) { diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.ts index b58128bd5dbdb..ee86c1aa3c309 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.ts @@ -7,14 +7,9 @@ */ import { get, hasIn } from 'lodash'; -import { - FilterValueFormatter, - RangeFilter, - isScriptedRangeFilter, - isRangeFilter, - Filter, - FILTERS, -} from '../../../../../common'; +import { RangeFilter, isScriptedRangeFilter, isRangeFilter, Filter, FILTERS } from '@kbn/es-query'; + +import { FilterValueFormatter } from '../../../../../common'; const getFormattedValueFn = (left: any, right: any) => { return (formatter?: FilterValueFormatter) => { diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts index 229257c1a7d81..d31b5bb9e608d 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Filter, FILTERS } from '../../../../../common'; +import { Filter, FILTERS } from '@kbn/es-query'; // Use mapSpatialFilter mapper to avoid bloated meta with value and params for spatial filters. export const mapSpatialFilter = (filter: Filter) => { diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index 3e86c6aa01fd9..5104a934fdec8 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -9,13 +9,14 @@ import { share } from 'rxjs/operators'; import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { buildEsQuery } from '@kbn/es-query'; import { FilterManager } from './filter_manager'; import { createAddToQueryLog } from './lib'; import { TimefilterService, TimefilterSetup } from './timefilter'; import { createSavedQueryService } from './saved_query/saved_query_service'; import { createQueryStateObservable } from './state_sync/create_global_query_observable'; import { QueryStringManager, QueryStringContract } from './query_string'; -import { buildEsQuery, getEsQueryConfig, TimeRange } from '../../common'; +import { getEsQueryConfig, TimeRange } from '../../common'; import { getUiSettings } from '../services'; import { NowProviderInternalContract } from '../now_provider'; import { IndexPattern } from '..'; diff --git a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts index 7e5b4ebed8785..3c94d6eb3c056 100644 --- a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts +++ b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts @@ -8,11 +8,12 @@ import { Observable, Subscription } from 'rxjs'; import { map, tap } from 'rxjs/operators'; +import { isFilterPinned } from '@kbn/es-query'; import { TimefilterSetup } from '../timefilter'; import { FilterManager } from '../filter_manager'; import { QueryState, QueryStateChange } from './index'; import { createStateContainer } from '../../../../kibana_utils/public'; -import { isFilterPinned, compareFilters, COMPARE_ALL_OPTIONS } from '../../../common'; +import { compareFilters, COMPARE_ALL_OPTIONS } from '../../../common'; import { QueryStringContract } from '../query_string'; export function createQueryStateObservable({ diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.ts index abb9953b844da..2f068ccdfab4f 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.ts @@ -14,7 +14,7 @@ import { import { QuerySetup, QueryStart } from '../query_service'; import { connectToQueryState } from './connect_to_query_state'; import { QueryState } from './types'; -import { FilterStateStore } from '../../../common/es_query/filters'; +import { FilterStateStore } from '../../../common'; const GLOBAL_STATE_STORAGE_KEY = '_g'; diff --git a/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts b/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts index a5601bbc2457a..0f4cdabc69d6b 100644 --- a/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts +++ b/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts @@ -8,8 +8,9 @@ import moment from 'moment'; import { keys } from 'lodash'; +import { RangeFilter } from '@kbn/es-query'; import { TimefilterContract } from '../../timefilter'; -import { RangeFilter, TimeRange } from '../../../../common'; +import { TimeRange } from '../../../../common'; export function convertRangeFilterToTimeRange(filter: RangeFilter) { const key = keys(filter.range)[0]; diff --git a/src/plugins/data/public/query/timefilter/lib/extract_time_filter.ts b/src/plugins/data/public/query/timefilter/lib/extract_time_filter.ts index 9e2b19002ff7f..19e2c656be50d 100644 --- a/src/plugins/data/public/query/timefilter/lib/extract_time_filter.ts +++ b/src/plugins/data/public/query/timefilter/lib/extract_time_filter.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ +import { Filter, isRangeFilter, RangeFilter } from '@kbn/es-query'; import { keys, partition } from 'lodash'; -import { Filter, isRangeFilter, RangeFilter, TimeRange } from '../../../../common'; +import { TimeRange } from '../../../../common'; import { convertRangeFilterToTimeRangeString } from './change_time_filter'; export function extractTimeFilter(timeFieldName: string, filters: Filter[]) { diff --git a/src/plugins/data/public/search/session/search_session_state.test.ts b/src/plugins/data/public/search/session/search_session_state.test.ts index d702d5c71a24b..65b931f23cf2e 100644 --- a/src/plugins/data/public/search/session/search_session_state.test.ts +++ b/src/plugins/data/public/search/session/search_session_state.test.ts @@ -24,6 +24,7 @@ const mockSavedObject: SearchSessionSavedObject = { expires: new Date().toISOString(), status: SearchSessionStatus.COMPLETE, persisted: true, + version: '8.0.0', }, references: [], }; diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index c2c4d1540c387..5c1882248f76a 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -33,6 +33,7 @@ const mockSavedObject: SearchSessionSavedObject = { expires: new Date().toISOString(), status: SearchSessionStatus.COMPLETE, persisted: true, + version: '8.0.0', }, references: [], }; diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index cc796ad749f0b..70f25bd510ef0 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -8,15 +8,6 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import classNames from 'classnames'; -import React, { useState, useRef } from 'react'; - -import { METRIC_TYPE } from '@kbn/analytics'; -import { FilterEditor } from './filter_editor'; -import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item'; -import { FilterOptions } from './filter_options'; -import { useKibana } from '../../../../kibana_react/public'; -import { IDataPluginServices, IIndexPattern } from '../..'; import { buildEmptyFilter, Filter, @@ -26,8 +17,18 @@ import { toggleFilterDisabled, toggleFilterNegated, unpinFilter, - UI_SETTINGS, -} from '../../../common'; +} from '@kbn/es-query'; +import classNames from 'classnames'; +import React, { useState, useRef } from 'react'; + +import { METRIC_TYPE } from '@kbn/analytics'; +import { FilterEditor } from './filter_editor'; +import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item'; +import { FilterOptions } from './filter_options'; +import { useKibana } from '../../../../kibana_react/public'; +import { IDataPluginServices, IIndexPattern } from '../..'; + +import { UI_SETTINGS } from '../../../common'; interface Props { filters: Filter[]; diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index 734161ea87232..90ca8e0783a20 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -23,6 +23,14 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { + Filter, + FieldFilter, + buildFilter, + buildCustomFilter, + cleanFilter, + getFilterParams, +} from '@kbn/es-query'; import { get } from 'lodash'; import React, { Component } from 'react'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; @@ -39,14 +47,6 @@ import { PhrasesValuesInput } from './phrases_values_input'; import { RangeValueInput } from './range_value_input'; import { getIndexPatternFromFilter } from '../../../query'; import { IIndexPattern, IFieldType } from '../../..'; -import { - Filter, - FieldFilter, - buildFilter, - buildCustomFilter, - cleanFilter, - getFilterParams, -} from '../../../../common'; export interface Props { filter: Filter; diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts index e15f667128c4f..ea172c2ccac35 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -7,15 +7,9 @@ */ import dateMath from '@elastic/datemath'; +import { Filter, FieldFilter } from '@kbn/es-query'; import { FILTER_OPERATORS, Operator } from './filter_operators'; -import { - isFilterable, - IIndexPattern, - IFieldType, - IpAddress, - Filter, - FieldFilter, -} from '../../../../../common'; +import { isFilterable, IIndexPattern, IFieldType, IpAddress } from '../../../../../common'; export function getFieldFromFilter(filter: FieldFilter, indexPattern: IIndexPattern) { return indexPattern.fields.find((field) => field.name === filter.meta.key); diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts index 218f8f5790e44..bc3f01aeb3c8f 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import { FILTERS } from '../../../../../common/es_query/filters'; +import { FILTERS } from '@kbn/es-query'; export interface Operator { message: string; diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 09e0571c2a870..b37fc9108ccd2 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -8,6 +8,13 @@ import { EuiContextMenu, EuiPopover } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n/react'; +import { + Filter, + isFilterPinned, + toggleFilterNegated, + toggleFilterPinned, + toggleFilterDisabled, +} from '@kbn/es-query'; import classNames from 'classnames'; import React, { MouseEvent, useState, useEffect } from 'react'; import { IUiSettingsClient } from 'src/core/public'; @@ -15,13 +22,6 @@ import { FilterEditor } from './filter_editor'; import { FilterView } from './filter_view'; import { IIndexPattern } from '../..'; import { getDisplayValueFromFilter, getIndexPatternFromFilter } from '../../query'; -import { - Filter, - isFilterPinned, - toggleFilterNegated, - toggleFilterPinned, - toggleFilterDisabled, -} from '../../../common'; import { getIndexPatterns } from '../../services'; type PanelOptions = 'pinFilter' | 'editFilter' | 'negateFilter' | 'disableFilter' | 'deleteFilter'; diff --git a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx index a15cec4bd286d..d551af87c7279 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx @@ -9,8 +9,8 @@ import { EuiBadge, useInnerText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { FC } from 'react'; +import { Filter, isFilterPinned } from '@kbn/es-query'; import { FilterLabel } from '../'; -import { Filter, isFilterPinned } from '../../../../common'; import type { FilterLabelStatus } from '../filter_item'; interface Props { diff --git a/src/plugins/data/server/autocomplete/terms_enum.test.ts b/src/plugins/data/server/autocomplete/terms_enum.test.ts index 41eaf3f4032ab..4e1eecc71db7c 100644 --- a/src/plugins/data/server/autocomplete/terms_enum.test.ts +++ b/src/plugins/data/server/autocomplete/terms_enum.test.ts @@ -12,6 +12,7 @@ import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { ConfigSchema } from '../../config'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import type { ApiResponse } from '@elastic/elasticsearch'; +import { TermsEnumResponse } from '@elastic/elasticsearch/api/types'; let savedObjectsClientMock: jest.Mocked; let esClientMock: DeeplyMockedKeys; @@ -29,7 +30,9 @@ describe('_terms_enum suggestions', () => { const requestHandlerContext = coreMock.createRequestHandlerContext(); savedObjectsClientMock = requestHandlerContext.savedObjects.client; esClientMock = requestHandlerContext.elasticsearch.client.asCurrentUser; - esClientMock.transport.request.mockResolvedValue((mockResponse as unknown) as ApiResponse); + esClientMock.termsEnum.mockResolvedValue( + (mockResponse as unknown) as ApiResponse + ); }); it('calls the _terms_enum API with the field, query, filters, and config tiers', async () => { @@ -44,7 +47,7 @@ describe('_terms_enum suggestions', () => { { name: 'field_name', type: 'string' } ); - const [[args]] = esClientMock.transport.request.mock.calls; + const [[args]] = esClientMock.termsEnum.mock.calls; expect(args).toMatchInlineSnapshot(` Object { @@ -67,8 +70,7 @@ describe('_terms_enum suggestions', () => { }, "string": "query", }, - "method": "POST", - "path": "/index/_terms_enum", + "index": "index", } `); expect(result).toEqual(mockResponse.body.terms); @@ -85,7 +87,7 @@ describe('_terms_enum suggestions', () => { [] ); - const [[args]] = esClientMock.transport.request.mock.calls; + const [[args]] = esClientMock.termsEnum.mock.calls; expect(args).toMatchInlineSnapshot(` Object { @@ -108,8 +110,7 @@ describe('_terms_enum suggestions', () => { }, "string": "query", }, - "method": "POST", - "path": "/index/_terms_enum", + "index": "index", } `); expect(result).toEqual(mockResponse.body.terms); diff --git a/src/plugins/data/server/autocomplete/terms_enum.ts b/src/plugins/data/server/autocomplete/terms_enum.ts index 40329586a3621..4bfd1545c6fe5 100644 --- a/src/plugins/data/server/autocomplete/terms_enum.ts +++ b/src/plugins/data/server/autocomplete/terms_enum.ts @@ -11,7 +11,6 @@ import { estypes } from '@elastic/elasticsearch'; import { IFieldType } from '../../common'; import { findIndexPatternById, getFieldByName } from '../index_patterns'; import { shimAbortSignal } from '../search'; -import { getKbnServerError } from '../../../kibana_utils/server'; import { ConfigSchema } from '../../config'; export async function termsEnumSuggestions( @@ -31,32 +30,26 @@ export async function termsEnumSuggestions( field = indexPattern && getFieldByName(fieldName, indexPattern); } - try { - const promise = esClient.transport.request({ - method: 'POST', - path: encodeURI(`/${index}/_terms_enum`), - body: { - field: field?.name ?? fieldName, - string: query, - index_filter: { - bool: { - must: [ - ...(filters ?? []), - { - terms: { - _tier: tiers, - }, + const promise = esClient.termsEnum({ + index, + body: { + field: field?.name ?? fieldName, + string: query, + index_filter: { + bool: { + must: [ + ...(filters ?? []), + { + terms: { + _tier: tiers, }, - ], - }, + }, + ], }, }, - }); + }, + }); - const result = await shimAbortSignal(promise, abortSignal); - - return result.body.terms; - } catch (e) { - throw getKbnServerError(e); - } + const result = await shimAbortSignal(promise, abortSignal); + return result.body.terms; } diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 143400a2c09d3..a9e8cda082314 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -6,10 +6,6 @@ * Side Public License, v 1. */ -import { PluginConfigDescriptor, PluginInitializerContext } from '../../../core/server'; -import { ConfigSchema, configSchema } from '../config'; -import { DataServerPlugin, DataPluginSetup, DataPluginStart } from './plugin'; - import { buildQueryFilter, buildCustomFilter, @@ -20,12 +16,20 @@ import { buildPhrasesFilter, buildRangeFilter, isFilterDisabled, + nodeTypes, + fromKueryExpression, + toElasticsearchQuery, + buildEsQuery, + buildQueryFromFilters, } from '../common'; +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../core/server'; +import { ConfigSchema, configSchema } from '../config'; +import { DataServerPlugin, DataPluginSetup, DataPluginStart } from './plugin'; /* + * @deprecated Please import from the package kbn/es-query directly. This will be deprecated in v8.0.0. * Filter helper namespace: */ - export const esFilters = { buildQueryFilter, buildCustomFilter, @@ -52,28 +56,29 @@ export const exporters = { * esQuery and esKuery: */ -import { - nodeTypes, - fromKueryExpression, - toElasticsearchQuery, - buildEsQuery, - buildQueryFromFilters, - getEsQueryConfig, -} from '../common'; +import { getEsQueryConfig } from '../common'; +/* + * Filter helper namespace + * @deprecated Please import from the package kbn/es-query directly. This will be deprecated in v8.0.0. + */ export const esKuery = { nodeTypes, fromKueryExpression, toElasticsearchQuery, }; +/* + * Filter helper namespace + * @deprecated Please import from the package kbn/es-query directly. This will be deprecated in v8.0.0. + */ export const esQuery = { buildQueryFromFilters, getEsQueryConfig, buildEsQuery, }; -export { EsQueryConfig, KueryNode } from '../common'; +export type { EsQueryConfig, KueryNode, IFieldSubType } from '../common'; /* * Field Formats: @@ -146,7 +151,6 @@ export { export { IFieldType, - IFieldSubType, ES_FIELD_TYPES, KBN_FIELD_TYPES, IndexPatternAttributes, diff --git a/src/plugins/data/server/search/session/mocks.ts b/src/plugins/data/server/search/session/mocks.ts index 4deaecbf8056d..ec99853088f78 100644 --- a/src/plugins/data/server/search/session/mocks.ts +++ b/src/plugins/data/server/search/session/mocks.ts @@ -27,7 +27,8 @@ export function createSearchSessionsClientMock(): jest.Mocked< getConfig: jest.fn( () => (({ - defaultExpiration: moment.duration('1', 'm'), + defaultExpiration: moment.duration('1', 'w'), + enabled: true, } as unknown) as SearchSessionsConfigSchema) ), }; diff --git a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts index 91de0fca3674c..4c75d62f12190 100644 --- a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts @@ -45,11 +45,11 @@ export const eqlSearchStrategyProvider = ( uiSettingsClient ); const params = id - ? getDefaultAsyncGetParams(options) + ? getDefaultAsyncGetParams(null, options) : { ...(await getIgnoreThrottled(uiSettingsClient)), ...defaultParams, - ...getDefaultAsyncGetParams(options), + ...getDefaultAsyncGetParams(null, options), ...request.params, }; const promise = id diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts index 56b26a7ebe02c..7a1ef2fe0a48b 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts @@ -171,7 +171,7 @@ describe('ES search strategy', () => { expect(request.index).toEqual(params.index); expect(request.body).toEqual(params.body); - expect(request).toHaveProperty('keep_alive', '60000ms'); + expect(request).toHaveProperty('keep_alive', '604800000ms'); }); it('makes a GET request to async search without keepalive', async () => { diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts index d6af00ada80fa..271032a9e1e27 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts @@ -59,7 +59,7 @@ export const enhancedEsSearchStrategyProvider = ( const search = async () => { const params = id - ? getDefaultAsyncGetParams(options) + ? getDefaultAsyncGetParams(searchSessionsClient.getConfig(), options) : { ...(await getDefaultAsyncSubmitParams( uiSettingsClient, diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts new file mode 100644 index 0000000000000..272e41e8bf82d --- /dev/null +++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { + getDefaultAsyncSubmitParams, + getDefaultAsyncGetParams, + getIgnoreThrottled, +} from './request_utils'; +import { IUiSettingsClient } from 'kibana/server'; +import { UI_SETTINGS } from '../../../../common'; +import moment from 'moment'; +import { SearchSessionsConfigSchema } from '../../../../config'; + +const getMockUiSettingsClient = (config: Record) => { + return { get: async (key: string) => config[key] } as IUiSettingsClient; +}; + +const getMockSearchSessionsConfig = ({ + enabled = true, + defaultExpiration = moment.duration(7, 'd'), +} = {}) => + ({ + enabled, + defaultExpiration, + } as SearchSessionsConfigSchema); + +describe('request utils', () => { + describe('getIgnoreThrottled', () => { + test('returns `ignore_throttled` as `true` when `includeFrozen` is `false`', async () => { + const mockUiSettingsClient = getMockUiSettingsClient({ + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, + }); + const result = await getIgnoreThrottled(mockUiSettingsClient); + expect(result.ignore_throttled).toBe(true); + }); + + test('returns `ignore_throttled` as `false` when `includeFrozen` is `true`', async () => { + const mockUiSettingsClient = getMockUiSettingsClient({ + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: true, + }); + const result = await getIgnoreThrottled(mockUiSettingsClient); + expect(result.ignore_throttled).toBe(false); + }); + }); + + describe('getDefaultAsyncSubmitParams', () => { + test('Uses `keep_alive` from default params if no `sessionId` is provided', async () => { + const mockUiSettingsClient = getMockUiSettingsClient({ + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, + }); + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + }); + const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, {}); + expect(params).toHaveProperty('keep_alive', '1m'); + }); + + test('Uses `keep_alive` from config if enabled', async () => { + const mockUiSettingsClient = getMockUiSettingsClient({ + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, + }); + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + }); + const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, { + sessionId: 'foo', + }); + expect(params).toHaveProperty('keep_alive', '259200000ms'); + }); + + test('Uses `keepAlive` of `1m` if disabled', async () => { + const mockUiSettingsClient = getMockUiSettingsClient({ + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, + }); + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: false, + }); + const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, { + sessionId: 'foo', + }); + expect(params).toHaveProperty('keep_alive', '1m'); + }); + + test('Uses `keep_on_completion` if enabled', async () => { + const mockUiSettingsClient = getMockUiSettingsClient({ + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, + }); + const mockConfig = getMockSearchSessionsConfig({}); + const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, { + sessionId: 'foo', + }); + expect(params).toHaveProperty('keep_on_completion', true); + }); + + test('Does not use `keep_on_completion` if disabled', async () => { + const mockUiSettingsClient = getMockUiSettingsClient({ + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, + }); + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: false, + }); + const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, { + sessionId: 'foo', + }); + expect(params).toHaveProperty('keep_on_completion', false); + }); + }); + + describe('getDefaultAsyncGetParams', () => { + test('Uses `wait_for_completion_timeout`', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: true, + }); + const params = getDefaultAsyncGetParams(mockConfig, {}); + expect(params).toHaveProperty('wait_for_completion_timeout'); + }); + + test('Uses `keep_alive` if `sessionId` is not provided', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: true, + }); + const params = getDefaultAsyncGetParams(mockConfig, {}); + expect(params).toHaveProperty('keep_alive', '1m'); + }); + + test('Has no `keep_alive` if `sessionId` is provided', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: true, + }); + const params = getDefaultAsyncGetParams(mockConfig, { sessionId: 'foo' }); + expect(params).not.toHaveProperty('keep_alive'); + }); + + test('Uses `keep_alive` if `sessionId` is provided but sessions disabled', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: false, + }); + const params = getDefaultAsyncGetParams(mockConfig, { sessionId: 'foo' }); + expect(params).toHaveProperty('keep_alive', '1m'); + }); + }); +}); diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts index 70da0ba2edcc3..8bf4473355ccf 100644 --- a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts +++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts @@ -46,21 +46,26 @@ export async function getDefaultAsyncSubmitParams( | 'keep_on_completion' > > { + const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; + + // TODO: searchSessionsConfig could be "null" if we are running without x-pack which happens only in tests. + // This can be cleaned up when we completely stop separating basic and oss + const keepAlive = useSearchSessions + ? `${searchSessionsConfig!.defaultExpiration.asMilliseconds()}ms` + : '1m'; + return { + // TODO: adjust for partial results batched_reduce_size: 64, - keep_on_completion: !!options.sessionId, // Always return an ID, even if the request completes quickly - ...getDefaultAsyncGetParams(options), + // Wait up to 100ms for the response to return + wait_for_completion_timeout: '100ms', + // If search sessions are used, store and get an async ID even for short running requests. + keep_on_completion: useSearchSessions, + // The initial keepalive is as defined in defaultExpiration if search sessions are used or 1m otherwise. + keep_alive: keepAlive, ...(await getIgnoreThrottled(uiSettingsClient)), ...(await getDefaultSearchParams(uiSettingsClient)), - ...(options.sessionId - ? { - // TODO: searchSessionsConfig could be "null" if we are running without x-pack which happens only in tests. - // This can be cleaned up when we completely stop separating basic and oss - keep_alive: searchSessionsConfig - ? `${searchSessionsConfig.defaultExpiration.asMilliseconds()}ms` - : '1m', - } - : {}), + // If search sessions are used, set the initial expiration time. }; } @@ -68,15 +73,20 @@ export async function getDefaultAsyncSubmitParams( @internal */ export function getDefaultAsyncGetParams( + searchSessionsConfig: SearchSessionsConfigSchema | null, options: ISearchOptions ): Pick { + const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; + return { - wait_for_completion_timeout: '100ms', // Wait up to 100ms for the response to return - ...(options.sessionId - ? undefined + // Wait up to 100ms for the response to return + wait_for_completion_timeout: '100ms', + ...(useSearchSessions + ? // Don't change the expiration of search requests that are tracked in a search session + undefined : { + // We still need to do polling for searches not within the context of a search session or when search session disabled keep_alive: '1m', - // We still need to do polling for searches not within the context of a search session }), }; } diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index adce494cf4782..7303557449956 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -23,6 +23,7 @@ import { ElasticsearchClient as ElasticsearchClient_2 } from 'kibana/server'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; +import { EsQueryConfig as EsQueryConfig_2 } from '@kbn/es-query'; import { estypes } from '@elastic/elasticsearch'; import { EventEmitter } from 'events'; import { ExecutionContext } from 'src/plugins/expressions/common'; @@ -30,18 +31,22 @@ import { ExpressionAstExpression } from 'src/plugins/expressions/common'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; +import { Filter as Filter_2 } from '@kbn/es-query'; import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils'; import { IAggConfigs as IAggConfigs_2 } from 'src/plugins/data/public'; import { IEsSearchResponse as IEsSearchResponse_2 } from 'src/plugins/data/public'; +import { IFieldSubType as IFieldSubType_2 } from '@kbn/es-query'; +import { IndexPatternBase } from '@kbn/es-query'; +import { IndexPatternFieldBase } from '@kbn/es-query'; import { IScopedClusterClient } from 'src/core/server'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; import { IUiSettingsClient } from 'src/core/server'; import { IUiSettingsClient as IUiSettingsClient_3 } from 'kibana/server'; -import { JsonValue } from '@kbn/common-utils'; import { KibanaExecutionContext } from 'src/core/public'; import { KibanaRequest } from 'src/core/server'; import { KibanaRequest as KibanaRequest_2 } from 'kibana/server'; +import { KueryNode as KueryNode_2 } from '@kbn/es-query'; import { Logger } from 'src/core/server'; import { Logger as Logger_2 } from 'kibana/server'; import { LoggerFactory } from '@kbn/logging'; @@ -55,7 +60,7 @@ import { Plugin as Plugin_2 } from 'src/core/server'; import { Plugin as Plugin_3 } from 'kibana/server'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/server'; import { PublicMethodsOf } from '@kbn/utility-types'; -import { RangeFilter } from 'src/plugins/data/public'; +import { Query } from '@kbn/es-query'; import { RecursiveReadonly } from '@kbn/utility-types'; import { RequestAdapter } from 'src/plugins/inspector/common'; import { RequestHandlerContext } from 'src/core/server'; @@ -425,7 +430,9 @@ export enum ES_FIELD_TYPES { // (undocumented) _TYPE = "_type", // (undocumented) - UNSIGNED_LONG = "unsigned_long" + UNSIGNED_LONG = "unsigned_long", + // (undocumented) + VERSION = "version" } // Warning: (ae-missing-release-tag) "ES_SEARCH_STRATEGY" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -445,53 +452,44 @@ export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'e // // @public (undocumented) export const esFilters: { - buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter; - buildCustomFilter: typeof buildCustomFilter; - buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; - buildExistsFilter: (field: import("../common").IndexPatternFieldBase, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter; - buildFilter: typeof buildFilter; - buildPhraseFilter: (field: import("../common").IndexPatternFieldBase, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter; - buildPhrasesFilter: (field: import("../common").IndexPatternFieldBase, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter; - buildRangeFilter: (field: import("../common").IndexPatternFieldBase, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter; - isFilterDisabled: (filter: import("../common").Filter) => boolean; + buildQueryFilter: (query: any, index: string, alias: string) => import("@kbn/es-query").QueryStringFilter; + buildCustomFilter: typeof import("@kbn/es-query").buildCustomFilter; + buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("@kbn/es-query").Filter; + buildExistsFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, indexPattern: import("@kbn/es-query").IndexPatternBase) => import("@kbn/es-query").ExistsFilter; + buildFilter: typeof import("@kbn/es-query").buildFilter; + buildPhraseFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, value: any, indexPattern: import("@kbn/es-query").IndexPatternBase) => import("@kbn/es-query").PhraseFilter; + buildPhrasesFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, params: any[], indexPattern: import("@kbn/es-query").IndexPatternBase) => import("@kbn/es-query").PhrasesFilter; + buildRangeFilter: (field: import("@kbn/es-query").IndexPatternFieldBase, params: import("@kbn/es-query").RangeFilterParams, indexPattern: import("@kbn/es-query").IndexPatternBase, formattedValue?: string | undefined) => import("@kbn/es-query").RangeFilter; + isFilterDisabled: (filter: import("@kbn/es-query").Filter) => boolean; }; // Warning: (ae-missing-release-tag) "esKuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const esKuery: { - nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; - fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; + fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; + toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; }; // Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const esQuery: { - buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { + buildQueryFromFilters: (filters: import("@kbn/es-query").Filter[] | undefined, indexPattern: import("@kbn/es-query").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean | undefined) => { must: never[]; - filter: import("../common").Filter[]; + filter: import("@kbn/es-query").Filter[]; should: never[]; - must_not: import("../common").Filter[]; + must_not: import("@kbn/es-query").Filter[]; }; getEsQueryConfig: typeof getEsQueryConfig; - buildEsQuery: typeof buildEsQuery; + buildEsQuery: typeof import("@kbn/es-query").buildEsQuery; }; // Warning: (ae-missing-release-tag) "EsQueryConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export interface EsQueryConfig { - // (undocumented) - allowLeadingWildcards: boolean; - // (undocumented) - dateFormatTZ?: string; - // (undocumented) - ignoreFilterIfFieldNotInIndex: boolean; - // (undocumented) - queryStringOptions: Record; -} +// @public @deprecated (undocumented) +export type EsQueryConfig = EsQueryConfig_2; // Warning: (ae-missing-release-tag) "ExecutionContextSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -596,12 +594,8 @@ export type FieldFormatsGetConfigFn = GetConfigFn; // Warning: (ae-missing-release-tag) "Filter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export type Filter = { - $state?: FilterState; - meta: FilterMeta; - query?: any; -}; +// @public @deprecated (undocumented) +export type Filter = Filter_2; // Warning: (ae-missing-release-tag) "getCapabilitiesForRollupIndices" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -629,7 +623,7 @@ export function getShardTimeout(config: SharedGlobalConfig_2): Pick): { @@ -682,19 +676,9 @@ export type IFieldParamType = FieldParamType; // Warning: (ae-missing-release-tag) "IFieldSubType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export interface IFieldSubType { - // (undocumented) - multi?: { - parent: string; - }; - // (undocumented) - nested?: { - path: string; - }; -} +// @public @deprecated (undocumented) +export type IFieldSubType = IFieldSubType_2; -// Warning: (ae-forgotten-export) The symbol "IndexPatternFieldBase" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IFieldType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated (undocumented) @@ -1131,15 +1115,8 @@ export type KibanaContext = ExpressionValueSearchContext; // Warning: (ae-missing-release-tag) "KueryNode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export interface KueryNode { - // (undocumented) - [key: string]: any; - // Warning: (ae-forgotten-export) The symbol "NodeTypes" needs to be exported by the entry point index.d.ts - // - // (undocumented) - type: keyof NodeTypes; -} +// @public @deprecated (undocumented) +export type KueryNode = KueryNode_2; // Warning: (ae-missing-release-tag) "mergeCapabilitiesWithFields" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1304,15 +1281,7 @@ export interface PluginStart { search: ISearchStart; } -// Warning: (ae-missing-release-tag) "Query" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type Query = { - query: string | { - [key: string]: any; - }; - language: string; -}; +export { Query } // Warning: (ae-missing-release-tag) "RefreshInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1511,47 +1480,42 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // Warnings were encountered during analysis: // -// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/meta_filter.ts:44:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:52:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:29:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:29:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:46:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:70:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:70:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "FieldFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:133:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:133:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:269:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:273:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:276:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:277:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:50:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:75:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "FieldFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:106:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:138:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:138:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:254:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:254:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:273:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:277:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:280:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:281:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:81:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:115:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss b/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss index c4bba1a8bbf2b..ead426fa9c4eb 100644 --- a/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss +++ b/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss @@ -33,10 +33,6 @@ doc-table { th { white-space: nowrap; padding-right: $euiSizeS; - - .fa { - font-size: 1.1em; - } } } @@ -134,25 +130,3 @@ doc-table { } } } - -table { - th { - i.fa-sort { - color: $euiColorLightShade; - } - - button.fa-sort-asc, - button.fa-sort-down, - i.fa-sort-asc, - i.fa-sort-down { - color: $euiColorPrimary; - } - - button.fa-sort-desc, - button.fa-sort-up, - i.fa-sort-desc, - i.fa-sort-up { - color: $euiColorPrimary; - } - } -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/_table_header.scss b/src/plugins/discover/public/application/angular/doc_table/components/_table_header.scss index 9ea4e21632ace..3450084e19269 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/_table_header.scss +++ b/src/plugins/discover/public/application/angular/doc_table/components/_table_header.scss @@ -2,7 +2,7 @@ white-space: nowrap; } .kbnDocTableHeader button { - margin-left: $euiSizeXS; + margin-left: $euiSizeXS * .5; } .kbnDocTableHeader__move, .kbnDocTableHeader__sortChange { @@ -12,3 +12,7 @@ opacity: 1; } } +.kbnDocTableHeader__actions { + display: flex; + align-items: center; +} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap b/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap index 20e503fd5ff91..fd0c1b4b2af8d 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap +++ b/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap @@ -1,30 +1,38 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`it renders ToolBarPagerButtons 1`] = ` -
- - -
+ + `; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap b/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap index fe168c013cb1a..96d2994bbe68f 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap +++ b/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap @@ -2,7 +2,7 @@ exports[`it renders ToolBarPagerText without crashing 1`] = `
1–2 of 3 diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx index eafa1e8fe59b2..d825220163165 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; interface Props { hasPreviousPage: boolean; @@ -18,35 +19,41 @@ interface Props { export function ToolBarPagerButtons(props: Props) { return ( -
- - -
+ + + props.onPagePrevious()} + isDisabled={!props.hasPreviousPage} + data-test-subj="btnPrevPage" + aria-label={i18n.translate( + 'discover.docTable.pager.toolbarPagerButtons.previousButtonAriaLabel', + { + defaultMessage: 'Previous page in table', + } + )} + /> + + + props.onPageNext()} + isDisabled={!props.hasNextPage} + data-test-subj="btnNextPage" + aria-label={i18n.translate( + 'discover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel', + { + defaultMessage: 'Next page in table', + } + )} + /> + + ); } diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss new file mode 100644 index 0000000000000..446e852f51e05 --- /dev/null +++ b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss @@ -0,0 +1,5 @@ +.kbnDocTable__toolBarText { + line-height: $euiLineHeight; + color: #69707D; + white-space: nowrap; +} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx index 131950cb6391e..5db68952b69ca 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx @@ -7,6 +7,7 @@ */ import React from 'react'; +import './tool_bar_pager_text.scss'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; interface Props { @@ -18,7 +19,7 @@ interface Props { export function ToolBarPagerText({ startItem, endItem, totalItems }: Props) { return ( -
+
Time @@ -20,9 +21,18 @@ exports[`TableHeader with time column renders correctly 1`] = ` > @@ -30,6 +40,7 @@ exports[`TableHeader with time column renders correctly 1`] = ` data-test-subj="docTableHeaderField" > first @@ -38,18 +49,36 @@ exports[`TableHeader with time column renders correctly 1`] = ` > @@ -57,6 +86,7 @@ exports[`TableHeader with time column renders correctly 1`] = ` data-test-subj="docTableHeaderField" > middle @@ -65,27 +95,54 @@ exports[`TableHeader with time column renders correctly 1`] = ` > @@ -93,6 +150,7 @@ exports[`TableHeader with time column renders correctly 1`] = ` data-test-subj="docTableHeaderField" > last @@ -101,18 +159,36 @@ exports[`TableHeader with time column renders correctly 1`] = ` > @@ -131,6 +207,7 @@ exports[`TableHeader without time column renders correctly 1`] = ` data-test-subj="docTableHeaderField" > first @@ -139,18 +216,36 @@ exports[`TableHeader without time column renders correctly 1`] = ` > @@ -158,6 +253,7 @@ exports[`TableHeader without time column renders correctly 1`] = ` data-test-subj="docTableHeaderField" > middle @@ -166,27 +262,54 @@ exports[`TableHeader without time column renders correctly 1`] = ` > @@ -194,6 +317,7 @@ exports[`TableHeader without time column renders correctly 1`] = ` data-test-subj="docTableHeaderField" > last @@ -202,18 +326,36 @@ exports[`TableHeader without time column renders correctly 1`] = ` > diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx index 6d69d2e5029f2..e4cbac052ca67 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiToolTip } from '@elastic/eui'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { SortOrder } from './helpers'; interface Props { @@ -24,12 +24,29 @@ interface Props { sortOrder: SortOrder[]; } -const sortDirectionToIcon: Record = { - desc: 'fa fa-sort-down', - asc: 'fa fa-sort-up', - '': 'fa fa-sort', +interface IconProps { + iconType: string; + color: 'primary' | 'text'; +} + +interface IconButtonProps { + active: boolean; + ariaLabel: string; + className: string; + iconProps: IconProps; + onClick: () => void | undefined; + testSubject: string; + tooltip: string; +} + +const sortDirectionToIcon: Record = { + desc: { iconType: 'sortDown', color: 'primary' }, + asc: { iconType: 'sortUp', color: 'primary' }, + '': { iconType: 'sortable', color: 'text' }, }; +const ICON_BUTTON_STYLE = { width: 12, height: 12 }; + export function TableHeaderColumn({ colLeftIdx, colRightIdx, @@ -43,28 +60,24 @@ export function TableHeaderColumn({ sortOrder, }: Props) { const [, sortDirection = ''] = sortOrder.find((sortPair) => name === sortPair[0]) || []; - const currentSortWithoutColumn = sortOrder.filter((pair) => pair[0] !== name); - const currentColumnSort = sortOrder.find((pair) => pair[0] === name); - const currentColumnSortDirection = (currentColumnSort && currentColumnSort[1]) || ''; - - const btnSortIcon = sortDirectionToIcon[sortDirection]; - const btnSortClassName = - sortDirection !== '' ? btnSortIcon : `kbnDocTableHeader__sortChange ${btnSortIcon}`; + const curSortWithoutCol = sortOrder.filter((pair) => pair[0] !== name); + const curColSort = sortOrder.find((pair) => pair[0] === name); + const curColSortDir = (curColSort && curColSort[1]) || ''; const handleChangeSortOrder = () => { if (!onChangeSortOrder) return; // Cycle goes Unsorted -> Asc -> Desc -> Unsorted - if (currentColumnSort === undefined) { - onChangeSortOrder([...currentSortWithoutColumn, [name, 'asc']]); - } else if (currentColumnSortDirection === 'asc') { - onChangeSortOrder([...currentSortWithoutColumn, [name, 'desc']]); - } else if (currentColumnSortDirection === 'desc' && currentSortWithoutColumn.length === 0) { + if (curColSort === undefined) { + onChangeSortOrder([...curSortWithoutCol, [name, 'asc']]); + } else if (curColSortDir === 'asc') { + onChangeSortOrder([...curSortWithoutCol, [name, 'desc']]); + } else if (curColSortDir === 'desc' && curSortWithoutCol.length === 0) { // If we're at the end of the cycle and this is the only existing sort, we switch // back to ascending sort instead of removing it. onChangeSortOrder([[name, 'asc']]); } else { - onChangeSortOrder(currentSortWithoutColumn); + onChangeSortOrder(curSortWithoutCol); } }; @@ -91,11 +104,11 @@ export function TableHeaderColumn({ } ); - if (currentColumnSort === undefined) { + if (curColSort === undefined) { return sortAscendingMessage; } else if (sortDirection === 'asc') { return sortDescendingMessage; - } else if (sortDirection === 'desc' && currentSortWithoutColumn.length === 0) { + } else if (sortDirection === 'desc' && curSortWithoutCol.length === 0) { return sortAscendingMessage; } else { return stopSortingMessage; @@ -103,12 +116,13 @@ export function TableHeaderColumn({ }; // action buttons displayed on the right side of the column name - const buttons = [ + const buttons: IconButtonProps[] = [ // Sort Button { active: isSortable && typeof onChangeSortOrder === 'function', ariaLabel: getSortButtonAriaLabel(), - className: btnSortClassName, + className: !sortDirection ? 'kbnDocTableHeader__sortChange' : '', + iconProps: sortDirectionToIcon[sortDirection], onClick: handleChangeSortOrder, testSubject: `docTableHeaderFieldSort_${name}`, tooltip: getSortButtonAriaLabel(), @@ -120,7 +134,8 @@ export function TableHeaderColumn({ defaultMessage: 'Remove {columnName} column', values: { columnName: name }, }), - className: 'fa fa-remove kbnDocTableHeader__move', + className: 'kbnDocTableHeader__move', + iconProps: { iconType: 'cross', color: 'text' }, onClick: () => onRemoveColumn && onRemoveColumn(name), testSubject: `docTableRemoveHeader-${name}`, tooltip: i18n.translate('discover.docTable.tableHeader.removeColumnButtonTooltip', { @@ -134,7 +149,8 @@ export function TableHeaderColumn({ defaultMessage: 'Move {columnName} column to the left', values: { columnName: name }, }), - className: 'fa fa-angle-double-left kbnDocTableHeader__move', + className: 'kbnDocTableHeader__move', + iconProps: { iconType: 'sortLeft', color: 'text' }, onClick: () => onMoveColumn && onMoveColumn(name, colLeftIdx), testSubject: `docTableMoveLeftHeader-${name}`, tooltip: i18n.translate('discover.docTable.tableHeader.moveColumnLeftButtonTooltip', { @@ -148,7 +164,8 @@ export function TableHeaderColumn({ defaultMessage: 'Move {columnName} column to the right', values: { columnName: name }, }), - className: 'fa fa-angle-double-right kbnDocTableHeader__move', + className: 'kbnDocTableHeader__move', + iconProps: { iconType: 'sortRight', color: 'text' }, onClick: () => onMoveColumn && onMoveColumn(name, colRightIdx), testSubject: `docTableMoveRightHeader-${name}`, tooltip: i18n.translate('discover.docTable.tableHeader.moveColumnRightButtonTooltip', { @@ -159,7 +176,7 @@ export function TableHeaderColumn({ return ( - + {displayName} {buttons .filter((button) => button.active) @@ -169,11 +186,14 @@ export function TableHeaderColumn({ content={button.tooltip} key={`button-${idx}`} > -
- - ", - } - } - /> + > + + + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
diff --git a/x-pack/plugins/canvas/public/components/shape_picker/__stories__/shape_picker.stories.tsx b/x-pack/plugins/canvas/public/components/shape_picker/__stories__/shape_picker.stories.tsx index f6357e3976c23..5fdea5591f538 100644 --- a/x-pack/plugins/canvas/public/components/shape_picker/__stories__/shape_picker.stories.tsx +++ b/x-pack/plugins/canvas/public/components/shape_picker/__stories__/shape_picker.stories.tsx @@ -9,9 +9,8 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import React from 'react'; import { ShapePicker } from '../shape_picker'; - -import { shapes } from '../../../../canvas_plugin_src/renderers/shape/shapes'; +import { getAvailableShapes } from '../../../../../../../src/plugins/expression_shape/common'; storiesOf('components/Shapes/ShapePicker', module).add('default', () => ( - + )); diff --git a/x-pack/plugins/canvas/public/components/shape_picker/shape_picker.tsx b/x-pack/plugins/canvas/public/components/shape_picker/shape_picker.tsx index 78cd989543ca9..0470699943bf1 100644 --- a/x-pack/plugins/canvas/public/components/shape_picker/shape_picker.tsx +++ b/x-pack/plugins/canvas/public/components/shape_picker/shape_picker.tsx @@ -9,29 +9,24 @@ import React, { FC } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGrid, EuiFlexItem, EuiLink } from '@elastic/eui'; import { ShapePreview } from '../shape_preview'; +import { Shape } from '../../../../../../src/plugins/expression_shape/common'; interface Props { - shapes: { - [key: string]: string; - }; + shapes: Shape[]; onChange?: (key: string) => void; } -export const ShapePicker: FC = ({ shapes, onChange = () => {} }) => { - return ( - - {Object.keys(shapes) - .sort() - .map((shapeKey) => ( - - onChange(shapeKey)}> - - - - ))} - - ); -}; +export const ShapePicker: FC = ({ shapes, onChange = () => {} }) => ( + + {shapes.sort().map((shapeKey: string) => ( + + onChange(shapeKey)}> + + + + ))} + +); ShapePicker.propTypes = { onChange: PropTypes.func, diff --git a/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/__snapshots__/shape_picker_popover.stories.storyshot b/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/__snapshots__/shape_picker_popover.stories.storyshot index aca6e9770573c..247616e842956 100644 --- a/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/__snapshots__/shape_picker_popover.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/__snapshots__/shape_picker_popover.stories.storyshot @@ -53,14 +53,21 @@ exports[`Storyshots components/Shapes/ShapePickerPopover interactive 1`] = ` >
- - ", - } - } - /> + > + + + +
@@ -90,14 +97,21 @@ exports[`Storyshots components/Shapes/ShapePickerPopover shape selected 1`] = ` >
- - ", - } - } - /> + > + + + +
diff --git a/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/shape_picker_popover.stories.tsx b/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/shape_picker_popover.stories.tsx index f91f509318b47..babc03b8f6763 100644 --- a/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/shape_picker_popover.stories.tsx +++ b/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/shape_picker_popover.stories.tsx @@ -9,18 +9,20 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import React from 'react'; import { ShapePickerPopover } from '../shape_picker_popover'; - -import { shapes } from '../../../../canvas_plugin_src/renderers/shape/shapes'; +import { + getAvailableShapes, + Shape, +} from '../../../../../../../src/plugins/expression_shape/common'; class Interactive extends React.Component<{}, { value: string }> { public state = { - value: 'square', + value: Shape.SQUARE, }; public render() { return ( this.setState({ value })} value={this.state.value} /> @@ -29,9 +31,15 @@ class Interactive extends React.Component<{}, { value: string }> { } storiesOf('components/Shapes/ShapePickerPopover', module) - .add('default', () => ) + .add('default', () => ( + + )) .add('shape selected', () => ( - + )) .add('interactive', () => , { info: { diff --git a/x-pack/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx b/x-pack/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx index cb3556c2c0fef..5701c3cb1e799 100644 --- a/x-pack/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx +++ b/x-pack/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx @@ -11,13 +11,12 @@ import { EuiLink, EuiPanel } from '@elastic/eui'; import { Popover } from '../popover'; import { ShapePicker } from '../shape_picker'; import { ShapePreview } from '../shape_preview'; +import { Shape } from '../../../../../../src/plugins/expression_shape/common'; interface Props { - shapes: { - [key: string]: string; - }; + shapes: Shape[]; onChange?: (key: string) => void; - value?: string; + value?: Shape; ariaLabel?: string; } @@ -25,7 +24,7 @@ export const ShapePickerPopover: FC = ({ shapes, onChange, value, ariaLab const button = (handleClick: React.MouseEventHandler) => ( - + ); diff --git a/x-pack/plugins/canvas/public/components/shape_preview/__stories__/__snapshots__/shape_preview.stories.storyshot b/x-pack/plugins/canvas/public/components/shape_preview/__stories__/__snapshots__/shape_preview.stories.storyshot index 8c7ff2e92b86d..747938d33f532 100644 --- a/x-pack/plugins/canvas/public/components/shape_preview/__stories__/__snapshots__/shape_preview.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/shape_preview/__stories__/__snapshots__/shape_preview.stories.storyshot @@ -3,25 +3,36 @@ exports[`Storyshots components/Shapes/ShapePreview arrow 1`] = `
- - ", - } - } -/> +> + + + +
`; exports[`Storyshots components/Shapes/ShapePreview square 1`] = `
- - ", - } - } -/> +> + + + +
`; diff --git a/x-pack/plugins/canvas/public/components/shape_preview/__stories__/shape_preview.stories.tsx b/x-pack/plugins/canvas/public/components/shape_preview/__stories__/shape_preview.stories.tsx index 1a135bae1c096..de7ce4b411860 100644 --- a/x-pack/plugins/canvas/public/components/shape_preview/__stories__/shape_preview.stories.tsx +++ b/x-pack/plugins/canvas/public/components/shape_preview/__stories__/shape_preview.stories.tsx @@ -8,9 +8,8 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import { ShapePreview } from '../shape_preview'; - -import { shapes } from '../../../../canvas_plugin_src/renderers/shape/shapes'; +import { Shape } from '../../../../../../../src/plugins/expression_shape/public'; storiesOf('components/Shapes/ShapePreview', module) - .add('arrow', () => ) - .add('square', () => ); + .add('arrow', () => ) + .add('square', () => ); diff --git a/x-pack/plugins/canvas/public/components/shape_preview/shape_preview.tsx b/x-pack/plugins/canvas/public/components/shape_preview/shape_preview.tsx index 7fbb28b771f39..48a6874eace0c 100644 --- a/x-pack/plugins/canvas/public/components/shape_preview/shape_preview.tsx +++ b/x-pack/plugins/canvas/public/components/shape_preview/shape_preview.tsx @@ -5,45 +5,54 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, RefCallback, useCallback, useState } from 'react'; import PropTypes from 'prop-types'; +import { + LazyShapeDrawer, + ShapeDrawerComponentProps, + getDefaultShapeData, + SvgConfig, + ShapeRef, + ViewBoxParams, +} from '../../../../../../src/plugins/expression_shape/public'; +import { withSuspense } from '../../../../../../src/plugins/presentation_util/public'; interface Props { shape?: string; } +const ShapeDrawer = withSuspense(LazyShapeDrawer); + +function getViewBox(defaultWidth: number, defaultViewBox: ViewBoxParams): ViewBoxParams { + const { minX, minY, width, height } = defaultViewBox; + return { + minX: minX - defaultWidth / 2, + minY: minY - defaultWidth / 2, + width: width + defaultWidth, + height: height + defaultWidth, + }; +} + export const ShapePreview: FC = ({ shape }) => { - if (!shape) { - return
; - } - - const weight = 5; - const parser = new DOMParser(); - const shapeSvg = parser - .parseFromString(shape, 'image/svg+xml') - .getElementsByTagName('svg') - .item(0); - - if (!shapeSvg) { - throw new Error('An unexpected error occurred: the SVG was not parseable'); - } - - shapeSvg.setAttribute('fill', 'none'); - shapeSvg.setAttribute('stroke', 'black'); - - const viewBox = shapeSvg.getAttribute('viewBox') || '0 0 0 0'; - const initialViewBox = viewBox.split(' ').map((v: string) => parseInt(v, 10)); - - let [minX, minY, width, height] = initialViewBox; - minX -= weight / 2; - minY -= weight / 2; - width += weight; - height += weight; - shapeSvg.setAttribute('viewBox', [minX, minY, width, height].join(' ')); + const [shapeData, setShapeData] = useState(getDefaultShapeData()); + + const shapeRef = useCallback>((node) => { + if (node !== null) setShapeData(node.getData()); + }, []); + if (!shape) return
; return ( - // eslint-disable-next-line react/no-danger -
+
+ +
); }; diff --git a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js index df894a65afab1..d5f0a2196814e 100644 --- a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js +++ b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js @@ -6,20 +6,20 @@ */ import { image } from '../canvas_plugin_src/renderers/image'; -import { repeatImage } from '../canvas_plugin_src/renderers/repeat_image'; import { markdown } from '../canvas_plugin_src/renderers/markdown'; import { metric } from '../canvas_plugin_src/renderers/metric'; import { pie } from '../canvas_plugin_src/renderers/pie'; import { plot } from '../canvas_plugin_src/renderers/plot'; import { progress } from '../canvas_plugin_src/renderers/progress'; -import { shape } from '../canvas_plugin_src/renderers/shape'; import { table } from '../canvas_plugin_src/renderers/table'; import { text } from '../canvas_plugin_src/renderers/text'; -import { revealImageRenderer as revealImage } from '../../../../src/plugins/expression_reveal_image/public'; import { errorRenderer as error, debugRenderer as debug, } from '../../../../src/plugins/expression_error/public'; +import { repeatImageRenderer as repeatImage } from '../../../../src/plugins/expression_repeat_image/public'; +import { revealImageRenderer as revealImage } from '../../../../src/plugins/expression_reveal_image/public'; +import { shapeRenderer as shape } from '../../../../src/plugins/expression_shape/public'; /** * This is a collection of renderers which are bundled with the runtime. If diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx index 643bd1dc22041..f99a1ed5d4335 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -39,19 +39,6 @@ jest.mock('../public/lib/ui_metric', () => ({ trackCanvasUiMetric: () => {} })); // Mock EUI generated ids to be consistently predictable for snapshots. jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); -// Jest automatically mocks SVGs to be a plain-text string that isn't an SVG. Canvas uses -// them in examples, so let's mock a few for tests. -jest.mock('../canvas_plugin_src/renderers/shape/shapes', () => ({ - shapes: { - arrow: ` - - `, - square: ` - - `, - }, -})); - // Mock react-datepicker dep used by eui to avoid rendering the entire large component jest.mock('@elastic/eui/packages/react-datepicker', () => { return { diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index bf9544a173f16..6181df5abe464 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -32,7 +32,9 @@ { "path": "../../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../../src/plugins/expressions/tsconfig.json" }, { "path": "../../../src/plugins/expression_error/tsconfig.json" }, + { "path": "../../../src/plugins/expression_repeat_image/tsconfig.json" }, { "path": "../../../src/plugins/expression_reveal_image/tsconfig.json" }, + { "path": "../../../src/plugins/expression_shape/tsconfig.json" }, { "path": "../../../src/plugins/home/tsconfig.json" }, { "path": "../../../src/plugins/inspector/tsconfig.json" }, { "path": "../../../src/plugins/kibana_legacy/tsconfig.json" }, diff --git a/x-pack/plugins/cloud/public/fullstory.ts b/x-pack/plugins/cloud/public/fullstory.ts index a5b735bce9387..25d5320a063bd 100644 --- a/x-pack/plugins/cloud/public/fullstory.ts +++ b/x-pack/plugins/cloud/public/fullstory.ts @@ -5,27 +5,30 @@ * 2.0. */ -import { sha256 } from 'js-sha256'; +import { sha256 } from 'js-sha256'; // loaded here to reduce page load bundle size when FullStory is disabled import type { IBasePath, PackageInfo } from '../../../../src/core/public'; export interface FullStoryDeps { basePath: IBasePath; orgId: string; packageInfo: PackageInfo; - userId?: string; } -interface FullStoryApi { +export interface FullStoryApi { identify(userId: string, userVars?: Record): void; event(eventName: string, eventProperties: Record): void; } -export const initializeFullStory = async ({ +export interface FullStoryService { + fullStory: FullStoryApi; + sha256: typeof sha256; +} + +export const initializeFullStory = ({ basePath, orgId, packageInfo, - userId, -}: FullStoryDeps) => { +}: FullStoryDeps): FullStoryService => { // @ts-expect-error window._fs_debug = false; // @ts-expect-error @@ -75,22 +78,8 @@ export const initializeFullStory = async ({ // @ts-expect-error const fullStory: FullStoryApi = window.FSKibana; - try { - // This needs to be called syncronously to be sure that we populate the user ID soon enough to make sessions merging - // across domains work - if (userId) { - // Do the hashing here to keep it at clear as possible in our source code that we do not send literal user IDs - const hashedId = sha256(userId.toString()); - fullStory.identify(hashedId); - } - } catch (e) { - // eslint-disable-next-line no-console - console.error(`[cloud.full_story] Could not call FS.identify due to error: ${e.toString()}`, e); - } - - // Record an event that Kibana was opened so we can easily search for sessions that use Kibana - fullStory.event('Loaded Kibana', { - // `str` suffix is required, see docs: https://help.fullstory.com/hc/en-us/articles/360020623234 - kibana_version_str: packageInfo.version, - }); + return { + fullStory, + sha256, + }; }; diff --git a/x-pack/plugins/cloud/public/plugin.test.mocks.ts b/x-pack/plugins/cloud/public/plugin.test.mocks.ts index 889b8492d5b1b..4eb206d07bf85 100644 --- a/x-pack/plugins/cloud/public/plugin.test.mocks.ts +++ b/x-pack/plugins/cloud/public/plugin.test.mocks.ts @@ -5,9 +5,17 @@ * 2.0. */ -import type { FullStoryDeps } from './fullstory'; +import { sha256 } from 'js-sha256'; +import type { FullStoryDeps, FullStoryApi, FullStoryService } from './fullstory'; -export const initializeFullStoryMock = jest.fn(); +export const fullStoryApiMock: jest.Mocked = { + event: jest.fn(), + identify: jest.fn(), +}; +export const initializeFullStoryMock = jest.fn(() => ({ + fullStory: fullStoryApiMock, + sha256, +})); jest.doMock('./fullstory', () => { return { initializeFullStory: initializeFullStoryMock }; }); diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts index 264ae61c050e8..9b3ddc8e7294e 100644 --- a/x-pack/plugins/cloud/public/plugin.test.ts +++ b/x-pack/plugins/cloud/public/plugin.test.ts @@ -9,14 +9,14 @@ import { nextTick } from '@kbn/test/jest'; import { coreMock } from 'src/core/public/mocks'; import { homePluginMock } from 'src/plugins/home/public/mocks'; import { securityMock } from '../../security/public/mocks'; -import { initializeFullStoryMock } from './plugin.test.mocks'; +import { fullStoryApiMock, initializeFullStoryMock } from './plugin.test.mocks'; import { CloudPlugin, CloudConfigType, loadFullStoryUserId } from './plugin'; describe('Cloud Plugin', () => { describe('#setup', () => { describe('setupFullstory', () => { beforeEach(() => { - initializeFullStoryMock.mockReset(); + jest.clearAllMocks(); }); const setupPlugin = async ({ @@ -63,23 +63,72 @@ describe('Cloud Plugin', () => { }); expect(initializeFullStoryMock).toHaveBeenCalled(); - const { basePath, orgId, packageInfo, userId } = initializeFullStoryMock.mock.calls[0][0]; + const { basePath, orgId, packageInfo } = initializeFullStoryMock.mock.calls[0][0]; expect(basePath.prepend).toBeDefined(); expect(orgId).toEqual('foo'); expect(packageInfo).toEqual(initContext.env.packageInfo); - expect(userId).toEqual('1234'); }); - it('passes undefined user ID when security is not available', async () => { + it('calls FS.identify with hashed user ID when security is available', async () => { + await setupPlugin({ + config: { full_story: { enabled: true, org_id: 'foo' } }, + currentUserProps: { + username: '1234', + }, + }); + + expect(fullStoryApiMock.identify).toHaveBeenCalledWith( + '03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4' + ); + }); + + it('does not call FS.identify when security is not available', async () => { await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' } }, securityEnabled: false, }); - expect(initializeFullStoryMock).toHaveBeenCalled(); - const { orgId, userId } = initializeFullStoryMock.mock.calls[0][0]; - expect(orgId).toEqual('foo'); - expect(userId).toEqual(undefined); + expect(fullStoryApiMock.identify).not.toHaveBeenCalled(); + }); + + it('calls FS.event when security is available', async () => { + const { initContext } = await setupPlugin({ + config: { full_story: { enabled: true, org_id: 'foo' } }, + currentUserProps: { + username: '1234', + }, + }); + + expect(fullStoryApiMock.event).toHaveBeenCalledWith('Loaded Kibana', { + kibana_version_str: initContext.env.packageInfo.version, + }); + }); + + it('calls FS.event when security is not available', async () => { + const { initContext } = await setupPlugin({ + config: { full_story: { enabled: true, org_id: 'foo' } }, + securityEnabled: false, + }); + + expect(fullStoryApiMock.event).toHaveBeenCalledWith('Loaded Kibana', { + kibana_version_str: initContext.env.packageInfo.version, + }); + }); + + it('calls FS.event when FS.identify throws an error', async () => { + fullStoryApiMock.identify.mockImplementationOnce(() => { + throw new Error(`identify failed!`); + }); + const { initContext } = await setupPlugin({ + config: { full_story: { enabled: true, org_id: 'foo' } }, + currentUserProps: { + username: '1234', + }, + }); + + expect(fullStoryApiMock.event).toHaveBeenCalledWith('Loaded Kibana', { + kibana_version_str: initContext.env.packageInfo.version, + }); }); it('does not call initializeFullStory when enabled=false', async () => { diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 98017d09ef807..16c11d569c5f7 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -162,7 +162,7 @@ export class CloudPlugin implements Plugin { }: CloudSetupDependencies & { basePath: IBasePath }) { const { enabled, org_id: orgId } = this.config.full_story; if (!enabled || !orgId) { - return; + return; // do not load any fullstory code in the browser if not enabled } // Keep this import async so that we do not load any FullStory code into the browser when it is disabled. @@ -171,16 +171,39 @@ export class CloudPlugin implements Plugin { ? loadFullStoryUserId({ getCurrentUser: security.authc.getCurrentUser }) : Promise.resolve(undefined); + // We need to call FS.identify synchronously after FullStory is initialized, so we must load the user upfront const [{ initializeFullStory }, userId] = await Promise.all([ fullStoryChunkPromise, userIdPromise, ]); - initializeFullStory({ + const { fullStory, sha256 } = initializeFullStory({ basePath, orgId, packageInfo: this.initializerContext.env.packageInfo, - userId, + }); + + // Very defensive try/catch to avoid any UnhandledPromiseRejections + try { + // This needs to be called syncronously to be sure that we populate the user ID soon enough to make sessions merging + // across domains work + if (userId) { + // Do the hashing here to keep it at clear as possible in our source code that we do not send literal user IDs + const hashedId = sha256(userId.toString()); + fullStory.identify(hashedId); + } + } catch (e) { + // eslint-disable-next-line no-console + console.error( + `[cloud.full_story] Could not call FS.identify due to error: ${e.toString()}`, + e + ); + } + + // Record an event that Kibana was opened so we can easily search for sessions that use Kibana + fullStory.event('Loaded Kibana', { + // `str` suffix is required, see docs: https://help.fullstory.com/hc/en-us/articles/360020623234 + kibana_version_str: this.initializerContext.env.packageInfo.version, }); } } diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index ebc270e96767e..a222f7c734a1e 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -51,7 +51,12 @@ export class DataEnhancedPlugin this.config = this.initializerContext.config.get(); if (this.config.search.sessions.enabled) { const sessionsConfig = this.config.search.sessions; - registerSearchSessionsMgmt(core, sessionsConfig, { data, management }); + registerSearchSessionsMgmt( + core, + sessionsConfig, + this.initializerContext.env.packageInfo.version, + { data, management } + ); } this.usageCollector = data.search.usageCollector; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/index.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/index.tsx index 2dfca534c20b5..a2d51d7d21248 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/index.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/index.tsx @@ -22,6 +22,7 @@ export class SearchSessionsMgmtApp { constructor( private coreSetup: CoreSetup, private config: SessionsConfigSchema, + private kibanaVersion: string, private params: ManagementAppMountParams, private pluginsSetup: IManagementSectionsPluginsSetup ) {} @@ -65,6 +66,7 @@ export class SearchSessionsMgmtApp { i18n, uiSettings, share, + kibanaVersion: this.kibanaVersion, }; const { element } = params; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx index 13dfe12ccb5e4..4c945e717464c 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx @@ -82,6 +82,7 @@ describe('Background Search Session Management Main', () => { timezone="UTC" documentation={new AsyncSearchIntroDocumentation(docLinks)} config={mockConfig} + kibanaVersion={'8.0.0'} /> ); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx index c398c92abf3a9..e82ab2bbb7043 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx @@ -23,6 +23,7 @@ interface Props { timezone: string; config: SessionsConfigSchema; plugins: IManagementSectionsPluginsSetup; + kibanaVersion: string; } export function SearchSessionsMgmtMain({ documentation, ...tableProps }: Props) { diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx index 59da0f0f4d17e..4711ff975fbd7 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx @@ -34,6 +34,7 @@ describe('Background Search Session management status labels', () => { expires: '2020-12-07T00:19:32Z', initialState: {}, restoreState: {}, + version: '8.0.0', }; }); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx index 6dfe3a5153670..b122155d8c93a 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx @@ -91,6 +91,7 @@ describe('Background Search Session Management Table', () => { api={api} timezone="UTC" config={mockConfig} + kibanaVersion={'8.0.0'} /> ); @@ -123,6 +124,7 @@ describe('Background Search Session Management Table', () => { api={api} timezone="UTC" config={mockConfig} + kibanaVersion={'8.0.0'} /> ); @@ -132,7 +134,7 @@ describe('Background Search Session Management Table', () => { expect(table.find('tbody td').map((node) => node.text())).toMatchInlineSnapshot(` Array [ "App", - "Namevery background search ", + "Namevery background search ", "# Searches0", "StatusExpired", "Created2 Dec, 2020, 00:19:32", @@ -166,6 +168,7 @@ describe('Background Search Session Management Table', () => { api={api} timezone="UTC" config={mockConfig} + kibanaVersion={'8.0.0'} /> ); @@ -199,6 +202,7 @@ describe('Background Search Session Management Table', () => { api={api} timezone="UTC" config={mockConfig} + kibanaVersion={'8.0.0'} /> ); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx index 962ad5a91efb0..803a5f9c10273 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx @@ -28,9 +28,18 @@ interface Props { timezone: string; config: SessionsConfigSchema; plugins: IManagementSectionsPluginsSetup; + kibanaVersion: string; } -export function SearchSessionsMgmtTable({ core, api, timezone, config, plugins, ...props }: Props) { +export function SearchSessionsMgmtTable({ + core, + api, + timezone, + config, + plugins, + kibanaVersion, + ...props +}: Props) { const [tableData, setTableData] = useState([]); const [isLoading, setIsLoading] = useState(false); const [debouncedIsLoading, setDebouncedIsLoading] = useState(false); @@ -111,7 +120,7 @@ export function SearchSessionsMgmtTable({ core, api, timezone, config, plugins, rowProps={() => ({ 'data-test-subj': 'searchSessionsRow', })} - columns={getColumns(core, plugins, api, config, timezone, onActionComplete)} + columns={getColumns(core, plugins, api, config, timezone, onActionComplete, kibanaVersion)} items={tableData} pagination={pagination} search={search} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/index.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/index.ts index 0ac8fa798cc92..87d3792df2fb7 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/index.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/index.ts @@ -37,6 +37,7 @@ export interface AppDependencies { http: HttpStart; i18n: I18nStart; config: SessionsConfigSchema; + kibanaVersion: string; } export const APP = { @@ -52,6 +53,7 @@ export type SessionsConfigSchema = ConfigSchema['search']['sessions']; export function registerSearchSessionsMgmt( coreSetup: CoreSetup, config: SessionsConfigSchema, + kibanaVersion: string, services: IManagementSectionsPluginsSetup ) { services.management.sections.section.kibana.registerApp({ @@ -60,7 +62,7 @@ export function registerSearchSessionsMgmt( order: 1.75, mount: async (params) => { const { SearchSessionsMgmtApp: MgmtApp } = await import('./application'); - const mgmtApp = new MgmtApp(coreSetup, config, params, services); + const mgmtApp = new MgmtApp(coreSetup, config, kibanaVersion, params, services); return mgmtApp.mountManagementSection(); }, }); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts index cc79f8002a98c..a3bc3b51f61bd 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts @@ -84,6 +84,7 @@ describe('Search Sessions Management API', () => { "restoreState": Object {}, "restoreUrl": "hello-cool-undefined-url", "status": "complete", + "version": undefined, }, ] `); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts index 0369dc4a839b5..eb38d47c26ffb 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts @@ -91,6 +91,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema) initialState, restoreState, idMapping, + version, } = savedObject.attributes; const status = getUIStatus(savedObject.attributes); @@ -115,6 +116,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema) initialState, restoreState, numSearches: Object.keys(idMapping).length, + version, }; }; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx index fc4e67360ea4a..f46ded96d877c 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx @@ -76,11 +76,20 @@ describe('Search Sessions Management table column factory', () => { expires: '2020-12-07T00:19:32Z', initialState: {}, restoreState: {}, + version: '7.14.0', }; }); test('returns columns', () => { - const columns = getColumns(mockCoreStart, mockPluginsSetup, api, mockConfig, tz, handleAction); + const columns = getColumns( + mockCoreStart, + mockPluginsSetup, + api, + mockConfig, + tz, + handleAction, + '7.14.0' + ); expect(columns).toMatchInlineSnapshot(` Array [ Object { @@ -144,7 +153,8 @@ describe('Search Sessions Management table column factory', () => { api, mockConfig, tz, - handleAction + handleAction, + '7.14.0' ) as Array>; const name = mount(nameColumn.render!(mockSession.name, mockSession) as ReactElement); @@ -162,7 +172,8 @@ describe('Search Sessions Management table column factory', () => { api, mockConfig, tz, - handleAction + handleAction, + '7.14.0' ) as Array>; const numOfSearchesLine = mount( @@ -181,7 +192,8 @@ describe('Search Sessions Management table column factory', () => { api, mockConfig, tz, - handleAction + handleAction, + '7.14.0' ) as Array>; const statusLine = mount(status.render!(mockSession.status, mockSession) as ReactElement); @@ -197,7 +209,8 @@ describe('Search Sessions Management table column factory', () => { api, mockConfig, tz, - handleAction + handleAction, + '7.14.0' ) as Array>; mockSession.status = 'INVALID' as SearchSessionStatus; @@ -220,7 +233,8 @@ describe('Search Sessions Management table column factory', () => { api, mockConfig, tz, - handleAction + handleAction, + '7.14.0' ) as Array>; const date = mount(createdDateCol.render!(mockSession.created, mockSession) as ReactElement); @@ -237,7 +251,8 @@ describe('Search Sessions Management table column factory', () => { api, mockConfig, tz, - handleAction + handleAction, + '7.14.0' ) as Array>; const date = mount(createdDateCol.render!(mockSession.created, mockSession) as ReactElement); @@ -252,7 +267,8 @@ describe('Search Sessions Management table column factory', () => { api, mockConfig, tz, - handleAction + handleAction, + '7.14.0' ) as Array>; mockSession.created = 'INVALID'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx index 94033a2536a87..9583fc4b843a9 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx @@ -49,7 +49,8 @@ export const getColumns = ( api: SearchSessionsMgmtAPI, config: SessionsConfigSchema, timezone: string, - onActionComplete: OnActionComplete + onActionComplete: OnActionComplete, + kibanaVersion: string ): Array> => { // Use a literal array of table column definitions to detail a UISession object return [ @@ -82,7 +83,7 @@ export const getColumns = ( }), sortable: true, width: '20%', - render: (name: UISession['name'], { restoreUrl, reloadUrl, status }) => { + render: (name: UISession['name'], { restoreUrl, reloadUrl, status, version }) => { const isRestorable = isSessionRestorable(status); const href = isRestorable ? restoreUrl : reloadUrl; const trackAction = isRestorable @@ -102,6 +103,21 @@ export const getColumns = ( /> ); + const versionIncompatibleWarning = + isRestorable && version === kibanaVersion ? null : ( + <> + {' '} + + } + /> + + ); return ( {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} @@ -113,6 +129,7 @@ export const getColumns = ( {name} {notRestorableWarning} + {versionIncompatibleWarning} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts index 6a8ace8dbdc79..f4f928e67e19c 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts @@ -40,4 +40,5 @@ export interface UISession { restoreUrl: string; initialState: Record; restoreState: Record; + version: string; } diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index a1ce3709e51a7..ce98cf06a9dfe 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -31,7 +31,12 @@ export class EnhancedDataServerPlugin public setup(core: CoreSetup, deps: SetupDependencies) { core.savedObjects.registerType(searchSessionSavedObjectType); - this.sessionService = new SearchSessionService(this.logger, this.config, deps.security); + this.sessionService = new SearchSessionService( + this.logger, + this.config, + this.initializerContext.env.packageInfo.version, + deps.security + ); deps.data.__enhance({ search: { diff --git a/x-pack/plugins/data_enhanced/server/saved_objects/search_session.ts b/x-pack/plugins/data_enhanced/server/saved_objects/search_session.ts index 9b5af9a6fa9e8..9a359679c0e7a 100644 --- a/x-pack/plugins/data_enhanced/server/saved_objects/search_session.ts +++ b/x-pack/plugins/data_enhanced/server/saved_objects/search_session.ts @@ -66,6 +66,9 @@ export const searchSessionSavedObjectType: SavedObjectsType = { username: { type: 'keyword', }, + version: { + type: 'keyword', + }, }, }, migrations: searchSessionSavedObjectMigrations, diff --git a/x-pack/plugins/data_enhanced/server/saved_objects/search_session_migration.test.ts b/x-pack/plugins/data_enhanced/server/saved_objects/search_session_migration.test.ts index 6682122c66f9c..a04a7a4d3302f 100644 --- a/x-pack/plugins/data_enhanced/server/saved_objects/search_session_migration.test.ts +++ b/x-pack/plugins/data_enhanced/server/saved_objects/search_session_migration.test.ts @@ -8,58 +8,59 @@ import { searchSessionSavedObjectMigrations, SearchSessionSavedObjectAttributesPre$7$13$0, + SearchSessionSavedObjectAttributesPre$7$14$0, } from './search_session_migration'; import { SavedObject } from '../../../../../src/core/types'; import { SEARCH_SESSION_TYPE, SearchSessionStatus } from '../../../../../src/plugins/data/common'; import { SavedObjectMigrationContext } from 'kibana/server'; -const mockCompletedSessionSavedObject: SavedObject = { - id: 'id', - type: SEARCH_SESSION_TYPE, - attributes: { - name: 'my_name', - appId: 'my_app_id', - sessionId: 'sessionId', - urlGeneratorId: 'my_url_generator_id', - initialState: {}, - restoreState: {}, - persisted: true, - idMapping: {}, - realmType: 'realmType', - realmName: 'realmName', - username: 'username', - created: '2021-03-26T00:00:00.000Z', - expires: '2021-03-30T00:00:00.000Z', - touched: '2021-03-29T00:00:00.000Z', - status: SearchSessionStatus.COMPLETE, - }, - references: [], -}; +describe('7.12.0 -> 7.13.0', () => { + const mockCompletedSessionSavedObject: SavedObject = { + id: 'id', + type: SEARCH_SESSION_TYPE, + attributes: { + name: 'my_name', + appId: 'my_app_id', + sessionId: 'sessionId', + urlGeneratorId: 'my_url_generator_id', + initialState: {}, + restoreState: {}, + persisted: true, + idMapping: {}, + realmType: 'realmType', + realmName: 'realmName', + username: 'username', + created: '2021-03-26T00:00:00.000Z', + expires: '2021-03-30T00:00:00.000Z', + touched: '2021-03-29T00:00:00.000Z', + status: SearchSessionStatus.COMPLETE, + }, + references: [], + }; -const mockInProgressSessionSavedObject: SavedObject = { - id: 'id', - type: SEARCH_SESSION_TYPE, - attributes: { - name: 'my_name', - appId: 'my_app_id', - sessionId: 'sessionId', - urlGeneratorId: 'my_url_generator_id', - initialState: {}, - restoreState: {}, - persisted: true, - idMapping: {}, - realmType: 'realmType', - realmName: 'realmName', - username: 'username', - created: '2021-03-26T00:00:00.000Z', - expires: '2021-03-30T00:00:00.000Z', - touched: '2021-03-29T00:00:00.000Z', - status: SearchSessionStatus.IN_PROGRESS, - }, - references: [], -}; + const mockInProgressSessionSavedObject: SavedObject = { + id: 'id', + type: SEARCH_SESSION_TYPE, + attributes: { + name: 'my_name', + appId: 'my_app_id', + sessionId: 'sessionId', + urlGeneratorId: 'my_url_generator_id', + initialState: {}, + restoreState: {}, + persisted: true, + idMapping: {}, + realmType: 'realmType', + realmName: 'realmName', + username: 'username', + created: '2021-03-26T00:00:00.000Z', + expires: '2021-03-30T00:00:00.000Z', + touched: '2021-03-29T00:00:00.000Z', + status: SearchSessionStatus.IN_PROGRESS, + }, + references: [], + }; -describe('7.12.0 -> 7.13.0', () => { const migration = searchSessionSavedObjectMigrations['7.13.0']; test('"completed" is populated from "touched" for completed session', () => { const migratedCompletedSession = migration( @@ -106,3 +107,58 @@ describe('7.12.0 -> 7.13.0', () => { ); }); }); + +describe('7.13.0 -> 7.14.0', () => { + const mockSessionSavedObject: SavedObject = { + id: 'id', + type: SEARCH_SESSION_TYPE, + attributes: { + name: 'my_name', + appId: 'my_app_id', + sessionId: 'sessionId', + urlGeneratorId: 'my_url_generator_id', + initialState: {}, + restoreState: {}, + persisted: true, + idMapping: {}, + realmType: 'realmType', + realmName: 'realmName', + username: 'username', + created: '2021-03-26T00:00:00.000Z', + expires: '2021-03-30T00:00:00.000Z', + touched: '2021-03-29T00:00:00.000Z', + completed: '2021-03-29T00:00:00.000Z', + status: SearchSessionStatus.COMPLETE, + }, + references: [], + }; + + const migration = searchSessionSavedObjectMigrations['7.14.0']; + test('version is populated', () => { + const migratedSession = migration(mockSessionSavedObject, {} as SavedObjectMigrationContext); + + expect(migratedSession.attributes).toHaveProperty('version'); + expect(migratedSession.attributes.version).toBe('7.13.0'); + expect(migratedSession.attributes).toMatchInlineSnapshot(` + Object { + "appId": "my_app_id", + "completed": "2021-03-29T00:00:00.000Z", + "created": "2021-03-26T00:00:00.000Z", + "expires": "2021-03-30T00:00:00.000Z", + "idMapping": Object {}, + "initialState": Object {}, + "name": "my_name", + "persisted": true, + "realmName": "realmName", + "realmType": "realmType", + "restoreState": Object {}, + "sessionId": "sessionId", + "status": "complete", + "touched": "2021-03-29T00:00:00.000Z", + "urlGeneratorId": "my_url_generator_id", + "username": "username", + "version": "7.13.0", + } + `); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/saved_objects/search_session_migration.ts b/x-pack/plugins/data_enhanced/server/saved_objects/search_session_migration.ts index 0ba8858ef525b..fa1428b3a3aad 100644 --- a/x-pack/plugins/data_enhanced/server/saved_objects/search_session_migration.ts +++ b/x-pack/plugins/data_enhanced/server/saved_objects/search_session_migration.ts @@ -17,14 +17,26 @@ import { * It is a timestamp representing the session was transitioned into "completed" status. */ export type SearchSessionSavedObjectAttributesPre$7$13$0 = Omit< - SearchSessionSavedObjectAttributesLatest, + SearchSessionSavedObjectAttributesPre$7$14$0, 'completed' >; +/** + * In 7.14.0 a `version` field was added. When search session is created it is populated with current kibana version. + * It is used to display warnings when trying to restore a session from a different version + * For saved object created before 7.14.0 we populate "7.13.0" inside the migration. + * It is less then ideal because the saved object could have actually been created in "7.12.x" or "7.13.x", + * but what is important for 7.14.0 is that the version is less then "7.14.0" + */ +export type SearchSessionSavedObjectAttributesPre$7$14$0 = Omit< + SearchSessionSavedObjectAttributesLatest, + 'version' +>; + export const searchSessionSavedObjectMigrations: SavedObjectMigrationMap = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc - ): SavedObjectUnsanitizedDoc => { + ): SavedObjectUnsanitizedDoc => { if (doc.attributes.status === SearchSessionStatus.COMPLETE) { return { ...doc, @@ -37,4 +49,15 @@ export const searchSessionSavedObjectMigrations: SavedObjectMigrationMap = { return doc; }, + '7.14.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectUnsanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + version: '7.13.0', + }, + }; + }, }; diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index dd1eafa5d60f8..4b5e1a1f86a11 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -91,7 +91,7 @@ describe('SearchSessionService', () => { warn: jest.fn(), error: jest.fn(), }; - service = new SearchSessionService(mockLogger, config); + service = new SearchSessionService(mockLogger, config, '8.0.0'); const coreStart = coreMock.createStart(); mockTaskManager = taskManagerMock.createStart(); await flushPromises(); @@ -171,7 +171,7 @@ describe('SearchSessionService', () => { warn: jest.fn(), error: jest.fn(), }; - service = new SearchSessionService(mockLogger, config); + service = new SearchSessionService(mockLogger, config, '8.0.0'); const coreStart = coreMock.createStart(); mockTaskManager = taskManagerMock.createStart(); await flushPromises(); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index af646ac3c5604..19f32860384d1 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -98,6 +98,7 @@ export class SearchSessionService constructor( private readonly logger: Logger, private readonly config: ConfigSchema, + private readonly version: string, private readonly security?: SecurityPluginSetup ) { this.sessionConfig = this.config.search.sessions; @@ -330,6 +331,7 @@ export class SearchSessionService touched: new Date().toISOString(), idMapping: {}, persisted: false, + version: this.version, realmType, realmName, username, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.test.tsx new file mode 100644 index 0000000000000..bdedc9357fa0e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiButton, EuiButtonEmpty, EuiFlyout, EuiFlyoutBody } from '@elastic/eui'; + +import { AddDomainFlyout } from './add_domain_flyout'; +import { AddDomainForm } from './add_domain_form'; +import { AddDomainFormErrors } from './add_domain_form_errors'; +import { AddDomainFormSubmitButton } from './add_domain_form_submit_button'; + +describe('AddDomainFlyout', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('is hidden by default', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFlyout)).toHaveLength(0); + }); + + it('displays the flyout when the button is pressed', () => { + const wrapper = shallow(); + + wrapper.find(EuiButton).simulate('click'); + + expect(wrapper.find(EuiFlyout)).toHaveLength(1); + }); + + describe('flyout', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + wrapper = shallow(); + + wrapper.find(EuiButton).simulate('click'); + }); + + it('displays form errors', () => { + expect(wrapper.find(EuiFlyoutBody).dive().find(AddDomainFormErrors)).toHaveLength(1); + }); + + it('contains a form to add domains', () => { + expect(wrapper.find(AddDomainForm)).toHaveLength(1); + }); + + it('contains a cancel buttonn', () => { + wrapper.find(EuiButtonEmpty).simulate('click'); + expect(wrapper.find(EuiFlyout)).toHaveLength(0); + }); + + it('contains a submit button', () => { + expect(wrapper.find(AddDomainFormSubmitButton)).toHaveLength(1); + }); + + it('hides the flyout on close', () => { + wrapper.find(EuiFlyout).simulate('close'); + + expect(wrapper.find(EuiFlyout)).toHaveLength(0); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.tsx new file mode 100644 index 0000000000000..f8511d1e2ef14 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiPortal, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { CANCEL_BUTTON_LABEL } from '../../../../../shared/constants'; + +import { AddDomainForm } from './add_domain_form'; +import { AddDomainFormErrors } from './add_domain_form_errors'; +import { AddDomainFormSubmitButton } from './add_domain_form_submit_button'; + +export const AddDomainFlyout: React.FC = () => { + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + + return ( + <> + setIsFlyoutVisible(true)} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.addDomainFlyout.openButtonLabel', + { + defaultMessage: 'Add domain', + } + )} + + + {isFlyoutVisible && ( + + setIsFlyoutVisible(false)}> + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.addDomainFlyout.title', + { + defaultMessage: 'Add a new domain', + } + )} +

+
+
+ }> + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.addDomainFlyout.description', + { + defaultMessage: + 'You can add multiple domains to this engine\'s web crawler. Add another domain here and modify the entry points and crawl rules from the "Manage" page.', + } + )} +

+ + + + + + + + setIsFlyoutVisible(false)}> + {CANCEL_BUTTON_LABEL} + + + + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.test.tsx new file mode 100644 index 0000000000000..6c869d9371f6f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.test.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiButton, EuiFieldText, EuiForm } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { rerender } from '../../../../../test_helpers'; + +import { AddDomainForm } from './add_domain_form'; + +const MOCK_VALUES = { + addDomainFormInputValue: 'https://', + entryPointValue: '/', +}; + +const MOCK_ACTIONS = { + setAddDomainFormInputValue: jest.fn(), + validateDomain: jest.fn(), +}; + +describe('AddDomainForm', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(MOCK_ACTIONS); + setMockValues(MOCK_VALUES); + wrapper = shallow(); + }); + + it('renders', () => { + expect(wrapper.find(EuiForm)).toHaveLength(1); + }); + + it('contains a submit button', () => { + expect(wrapper.find(EuiButton).prop('type')).toEqual('submit'); + }); + + it('validates domain on submit', () => { + wrapper.find(EuiForm).simulate('submit', { preventDefault: jest.fn() }); + + expect(MOCK_ACTIONS.validateDomain).toHaveBeenCalledTimes(1); + }); + + describe('url field', () => { + it('uses the value from the logic', () => { + setMockValues({ + ...MOCK_VALUES, + addDomainFormInputValue: 'test value', + }); + + rerender(wrapper); + + expect(wrapper.find(EuiFieldText).prop('value')).toEqual('test value'); + }); + + it('sets the value in the logic on change', () => { + wrapper.find(EuiFieldText).simulate('change', { target: { value: 'test value' } }); + + expect(MOCK_ACTIONS.setAddDomainFormInputValue).toHaveBeenCalledWith('test value'); + }); + }); + + describe('validate domain button', () => { + it('is enabled when the input has a value', () => { + setMockValues({ + ...MOCK_VALUES, + addDomainFormInputValue: 'https://elastic.co', + }); + + rerender(wrapper); + + expect(wrapper.find(EuiButton).prop('disabled')).toEqual(false); + }); + + it('is disabled when the input value is empty', () => { + setMockValues({ + ...MOCK_VALUES, + addDomainFormInputValue: '', + }); + + rerender(wrapper); + + expect(wrapper.find(EuiButton).prop('disabled')).toEqual(true); + }); + }); + + describe('entry point indicator', () => { + it('is hidden when the entry point is /', () => { + setMockValues({ + ...MOCK_VALUES, + entryPointValue: '/', + }); + + rerender(wrapper); + + expect(wrapper.find(FormattedMessage)).toHaveLength(0); + }); + + it('displays the entry point otherwise', () => { + setMockValues({ + ...MOCK_VALUES, + entryPointValue: '/guide', + }); + + rerender(wrapper); + + expect(wrapper.find(FormattedMessage)).toHaveLength(1); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.tsx new file mode 100644 index 0000000000000..de6a33403c2ed --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiCode, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { AddDomainLogic } from './add_domain_logic'; + +export const AddDomainForm: React.FC = () => { + const { setAddDomainFormInputValue, validateDomain } = useActions(AddDomainLogic); + + const { addDomainFormInputValue, entryPointValue } = useValues(AddDomainLogic); + + return ( + <> + { + event.preventDefault(); + validateDomain(); + }} + component="form" + > + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.addDomainForm.urlHelpText', + { + defaultMessage: 'Domain URLs require a protocol and cannot contain any paths.', + } + )} + + } + > + + + setAddDomainFormInputValue(e.target.value)} + fullWidth + /> + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.addDomainForm.validateButtonLabel', + { + defaultMessage: 'Validate Domain', + } + )} + + + + + + {entryPointValue !== '/' && ( + <> + + +

+ + {entryPointValue}, + }} + /> + +

+
+ + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.test.tsx new file mode 100644 index 0000000000000..d2c3ac37d58fa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.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 { setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { AddDomainFormErrors } from './add_domain_form_errors'; + +describe('AddDomainFormErrors', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('is empty when there are no errors', () => { + setMockValues({ + errors: [], + }); + + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('displays all the errors from the logic', () => { + setMockValues({ + errors: ['first error', 'second error'], + }); + + const wrapper = shallow(); + + expect(wrapper.find('p')).toHaveLength(2); + expect(wrapper.find('p').first().text()).toContain('first error'); + expect(wrapper.find('p').last().text()).toContain('second error'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.tsx new file mode 100644 index 0000000000000..890657d4c235a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { AddDomainLogic } from './add_domain_logic'; + +export const AddDomainFormErrors: React.FC = () => { + const { errors } = useValues(AddDomainLogic); + + if (errors.length > 0) { + return ( + + {errors.map((message, index) => ( +

{message}

+ ))} +
+ ); + } + + return null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.test.tsx new file mode 100644 index 0000000000000..a01d8c55bc87c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.test.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton } from '@elastic/eui'; + +import { AddDomainFormSubmitButton } from './add_domain_form_submit_button'; + +describe('AddDomainFormSubmitButton', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('is disabled when the domain has not been validated', () => { + setMockValues({ + hasValidationCompleted: false, + }); + + const wrapper = shallow(); + + expect(wrapper.prop('disabled')).toBe(true); + }); + + it('is enabled when the domain has been validated', () => { + setMockValues({ + hasValidationCompleted: true, + }); + + const wrapper = shallow(); + + expect(wrapper.prop('disabled')).toBe(false); + }); + + it('submits the domain on click', () => { + const submitNewDomain = jest.fn(); + + setMockActions({ + submitNewDomain, + }); + setMockValues({ + hasValidationCompleted: true, + }); + + const wrapper = shallow(); + + wrapper.find(EuiButton).simulate('click'); + + expect(submitNewDomain).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.tsx new file mode 100644 index 0000000000000..dbf5f86ca70fc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiButton } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { AddDomainLogic } from './add_domain_logic'; + +export const AddDomainFormSubmitButton: React.FC = () => { + const { submitNewDomain } = useActions(AddDomainLogic); + + const { hasValidationCompleted } = useValues(AddDomainLogic); + + return ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.addDomainForm.submitButtonLabel', { + defaultMessage: 'Add domain', + })} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts new file mode 100644 index 0000000000000..3072796b7194f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts @@ -0,0 +1,300 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, + mockKibanaValues, +} from '../../../../../__mocks__/kea_logic'; +import '../../../../__mocks__/engine_logic.mock'; + +jest.mock('../../crawler_overview_logic', () => ({ + CrawlerOverviewLogic: { + actions: { + onReceiveCrawlerData: jest.fn(), + }, + }, +})); + +import { nextTick } from '@kbn/test/jest'; + +import { CrawlerOverviewLogic } from '../../crawler_overview_logic'; +import { CrawlerDomain } from '../../types'; + +import { AddDomainLogic, AddDomainLogicValues } from './add_domain_logic'; + +const DEFAULT_VALUES: AddDomainLogicValues = { + addDomainFormInputValue: 'https://', + allowSubmit: false, + entryPointValue: '/', + hasValidationCompleted: false, + errors: [], +}; + +describe('AddDomainLogic', () => { + const { mount } = new LogicMounter(AddDomainLogic); + const { flashSuccessToast } = mockFlashMessageHelpers; + const { http } = mockHttpValues; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has default values', () => { + mount(); + expect(AddDomainLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('clearDomainFormInputValue', () => { + beforeAll(() => { + mount({ + addDomainFormInputValue: 'http://elastic.co', + entryPointValue: '/foo', + hasValidationCompleted: true, + errors: ['first error', 'second error'], + }); + + AddDomainLogic.actions.clearDomainFormInputValue(); + }); + + it('should clear the input value', () => { + expect(AddDomainLogic.values.addDomainFormInputValue).toEqual('https://'); + }); + + it('should clear the entry point value', () => { + expect(AddDomainLogic.values.entryPointValue).toEqual('/'); + }); + + it('should reset validation completion', () => { + expect(AddDomainLogic.values.hasValidationCompleted).toEqual(false); + }); + + it('should clear errors', () => { + expect(AddDomainLogic.values.errors).toEqual([]); + }); + }); + + describe('onSubmitNewDomainError', () => { + it('should set errors', () => { + mount(); + + AddDomainLogic.actions.onSubmitNewDomainError(['first error', 'second error']); + + expect(AddDomainLogic.values.errors).toEqual(['first error', 'second error']); + }); + }); + + describe('onValidateDomain', () => { + beforeAll(() => { + mount({ + addDomainFormInputValue: 'https://elastic.co', + entryPointValue: '/customers', + hasValidationCompleted: true, + errors: ['first error', 'second error'], + }); + + AddDomainLogic.actions.onValidateDomain('https://swiftype.com', '/site-search'); + }); + + it('should set the input value', () => { + expect(AddDomainLogic.values.addDomainFormInputValue).toEqual('https://swiftype.com'); + }); + + it('should set the entry point value', () => { + expect(AddDomainLogic.values.entryPointValue).toEqual('/site-search'); + }); + + it('should flag validation as being completed', () => { + expect(AddDomainLogic.values.hasValidationCompleted).toEqual(true); + }); + + it('should clear errors', () => { + expect(AddDomainLogic.values.errors).toEqual([]); + }); + }); + + describe('setAddDomainFormInputValue', () => { + beforeAll(() => { + mount({ + addDomainFormInputValue: 'https://elastic.co', + entryPointValue: '/customers', + hasValidationCompleted: true, + errors: ['first error', 'second error'], + }); + + AddDomainLogic.actions.setAddDomainFormInputValue('https://swiftype.com/site-search'); + }); + + it('should set the input value', () => { + expect(AddDomainLogic.values.addDomainFormInputValue).toEqual( + 'https://swiftype.com/site-search' + ); + }); + + it('should clear the entry point value', () => { + expect(AddDomainLogic.values.entryPointValue).toEqual('/'); + }); + + it('should reset validation completion', () => { + expect(AddDomainLogic.values.hasValidationCompleted).toEqual(false); + }); + + it('should clear errors', () => { + expect(AddDomainLogic.values.errors).toEqual([]); + }); + }); + + describe('submitNewDomain', () => { + it('should clear errors', () => { + expect(AddDomainLogic.values.errors).toEqual([]); + }); + }); + }); + + describe('listeners', () => { + describe('onSubmitNewDomainSuccess', () => { + it('should flash a success toast', () => { + const { navigateToUrl } = mockKibanaValues; + mount(); + + AddDomainLogic.actions.onSubmitNewDomainSuccess({ id: 'test-domain' } as CrawlerDomain); + + expect(flashSuccessToast).toHaveBeenCalled(); + expect(navigateToUrl).toHaveBeenCalledWith( + '/engines/some-engine/crawler/domains/test-domain' + ); + }); + }); + + describe('submitNewDomain', () => { + it('calls the domains endpoint with a JSON formatted body', async () => { + mount({ + addDomainFormInputValue: 'https://elastic.co', + entryPointValue: '/guide', + }); + http.post.mockReturnValueOnce(Promise.resolve({})); + + AddDomainLogic.actions.submitNewDomain(); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/crawler/domains', + { + query: { + respond_with: 'crawler_details', + }, + body: JSON.stringify({ + name: 'https://elastic.co', + entry_points: [{ value: '/guide' }], + }), + } + ); + }); + + describe('on success', () => { + beforeEach(() => { + mount(); + }); + + it('sets crawler data', async () => { + http.post.mockReturnValueOnce( + Promise.resolve({ + domains: [], + }) + ); + + AddDomainLogic.actions.submitNewDomain(); + await nextTick(); + + expect(CrawlerOverviewLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith({ + domains: [], + }); + }); + + it('calls the success callback with the most recent domain', async () => { + http.post.mockReturnValueOnce( + Promise.resolve({ + domains: [ + { + id: '1', + name: 'https://elastic.co/guide', + }, + { + id: '2', + name: 'https://swiftype.co/site-search', + }, + ], + }) + ); + jest.spyOn(AddDomainLogic.actions, 'onSubmitNewDomainSuccess'); + AddDomainLogic.actions.submitNewDomain(); + await nextTick(); + + expect(AddDomainLogic.actions.onSubmitNewDomainSuccess).toHaveBeenCalledWith({ + id: '2', + url: 'https://swiftype.co/site-search', + }); + }); + }); + + describe('on error', () => { + beforeEach(() => { + mount(); + jest.spyOn(AddDomainLogic.actions, 'onSubmitNewDomainError'); + }); + + it('passes error messages to the error callback', async () => { + http.post.mockReturnValueOnce( + Promise.reject({ + body: { + attributes: { + errors: ['first error', 'second error'], + }, + }, + }) + ); + + AddDomainLogic.actions.submitNewDomain(); + await nextTick(); + + expect(AddDomainLogic.actions.onSubmitNewDomainError).toHaveBeenCalledWith([ + 'first error', + 'second error', + ]); + }); + }); + }); + + describe('validateDomain', () => { + it('extracts the domain and entrypoint and passes them to the callback ', () => { + mount({ addDomainFormInputValue: 'https://swiftype.com/site-search' }); + jest.spyOn(AddDomainLogic.actions, 'onValidateDomain'); + + AddDomainLogic.actions.validateDomain(); + + expect(AddDomainLogic.actions.onValidateDomain).toHaveBeenCalledWith( + 'https://swiftype.com', + '/site-search' + ); + }); + }); + }); + + describe('selectors', () => { + describe('allowSubmit', () => { + it('gets set true when validation is completed', () => { + mount({ hasValidationCompleted: false }); + expect(AddDomainLogic.values.allowSubmit).toEqual(false); + + mount({ hasValidationCompleted: true }); + expect(AddDomainLogic.values.allowSubmit).toEqual(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.ts new file mode 100644 index 0000000000000..b05b9454fe8f8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { flashSuccessToast } from '../../../../../shared/flash_messages'; +import { getErrorsFromHttpResponse } from '../../../../../shared/flash_messages/handle_api_errors'; + +import { HttpLogic } from '../../../../../shared/http'; +import { KibanaLogic } from '../../../../../shared/kibana'; +import { ENGINE_CRAWLER_DOMAIN_PATH } from '../../../../routes'; +import { EngineLogic, generateEnginePath } from '../../../engine'; + +import { CrawlerOverviewLogic } from '../../crawler_overview_logic'; +import { CrawlerDataFromServer, CrawlerDomain } from '../../types'; +import { crawlerDataServerToClient } from '../../utils'; + +import { extractDomainAndEntryPointFromUrl } from './utils'; + +export interface AddDomainLogicValues { + addDomainFormInputValue: string; + allowSubmit: boolean; + hasValidationCompleted: boolean; + entryPointValue: string; + errors: string[]; +} + +export interface AddDomainLogicActions { + clearDomainFormInputValue(): void; + setAddDomainFormInputValue(newValue: string): string; + onSubmitNewDomainError(errors: string[]): { errors: string[] }; + onSubmitNewDomainSuccess(domain: CrawlerDomain): { domain: CrawlerDomain }; + onValidateDomain( + newValue: string, + newEntryPointValue: string + ): { newValue: string; newEntryPointValue: string }; + submitNewDomain(): void; + validateDomain(): void; +} + +const DEFAULT_SELECTOR_VALUES = { + addDomainFormInputValue: 'https://', + entryPointValue: '/', +}; + +export const AddDomainLogic = kea>({ + path: ['enterprise_search', 'app_search', 'crawler', 'add_domain'], + actions: () => ({ + clearDomainFormInputValue: true, + setAddDomainFormInputValue: (newValue) => newValue, + onSubmitNewDomainSuccess: (domain) => ({ domain }), + onSubmitNewDomainError: (errors) => ({ errors }), + onValidateDomain: (newValue, newEntryPointValue) => ({ + newValue, + newEntryPointValue, + }), + submitNewDomain: true, + validateDomain: true, + }), + reducers: () => ({ + addDomainFormInputValue: [ + DEFAULT_SELECTOR_VALUES.addDomainFormInputValue, + { + clearDomainFormInputValue: () => DEFAULT_SELECTOR_VALUES.addDomainFormInputValue, + setAddDomainFormInputValue: (_, newValue: string) => newValue, + onValidateDomain: (_, { newValue }: { newValue: string }) => newValue, + }, + ], + entryPointValue: [ + DEFAULT_SELECTOR_VALUES.entryPointValue, + { + clearDomainFormInputValue: () => DEFAULT_SELECTOR_VALUES.entryPointValue, + setAddDomainFormInputValue: () => DEFAULT_SELECTOR_VALUES.entryPointValue, + onValidateDomain: (_, { newEntryPointValue }) => newEntryPointValue, + }, + ], + // TODO When 4-step validation is added this will become a selector as + // we'll use individual step results to determine whether this is true/false + hasValidationCompleted: [ + false, + { + clearDomainFormInputValue: () => false, + setAddDomainFormInputValue: () => false, + onValidateDomain: () => true, + }, + ], + errors: [ + [], + { + clearDomainFormInputValue: () => [], + setAddDomainFormInputValue: () => [], + onValidateDomain: () => [], + submitNewDomain: () => [], + onSubmitNewDomainError: (_, { errors }) => errors, + }, + ], + }), + selectors: ({ selectors }) => ({ + // TODO include selectors.blockingFailures once 4-step validation is migrated + allowSubmit: [ + () => [selectors.hasValidationCompleted], // should eventually also contain selectors.hasBlockingFailures when that is added + (hasValidationCompleted: boolean) => hasValidationCompleted, // && !hasBlockingFailures + ], + }), + listeners: ({ actions, values }) => ({ + onSubmitNewDomainSuccess: ({ domain }) => { + flashSuccessToast( + i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.add.successMessage', + { + defaultMessage: "Successfully added domain '{domainUrl}'", + values: { + domainUrl: domain.url, + }, + } + ) + ); + KibanaLogic.values.navigateToUrl( + generateEnginePath(ENGINE_CRAWLER_DOMAIN_PATH, { domainId: domain.id }) + ); + }, + submitNewDomain: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const requestBody = JSON.stringify({ + name: values.addDomainFormInputValue.trim(), + entry_points: [{ value: values.entryPointValue }], + }); + + try { + const response = await http.post(`/api/app_search/engines/${engineName}/crawler/domains`, { + query: { + respond_with: 'crawler_details', + }, + body: requestBody, + }); + + const crawlerData = crawlerDataServerToClient(response as CrawlerDataFromServer); + CrawlerOverviewLogic.actions.onReceiveCrawlerData(crawlerData); + const newDomain = crawlerData.domains[crawlerData.domains.length - 1]; + if (newDomain) { + actions.onSubmitNewDomainSuccess(newDomain); + } + // If there is not a new domain, that means the server responded with a 200 but + // didn't actually persist the new domain to our BE, and we take no action + } catch (e) { + // we surface errors inside the form instead of in flash messages + const errorMessages = getErrorsFromHttpResponse(e); + actions.onSubmitNewDomainError(errorMessages); + } + }, + validateDomain: () => { + const { domain, entryPoint } = extractDomainAndEntryPointFromUrl( + values.addDomainFormInputValue.trim() + ); + actions.onValidateDomain(domain, entryPoint); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.test.ts new file mode 100644 index 0000000000000..446545c28ee79 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { extractDomainAndEntryPointFromUrl } from './utils'; + +describe('extractDomainAndEntryPointFromUrl', () => { + it('extracts a provided entry point and domain', () => { + expect(extractDomainAndEntryPointFromUrl('https://elastic.co/guide')).toEqual({ + domain: 'https://elastic.co', + entryPoint: '/guide', + }); + }); + + it('provides a default entry point if there is only a domain', () => { + expect(extractDomainAndEntryPointFromUrl('https://elastic.co')).toEqual({ + domain: 'https://elastic.co', + entryPoint: '/', + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.ts new file mode 100644 index 0000000000000..7ba67ae61aa2b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.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. + */ + +export const extractDomainAndEntryPointFromUrl = ( + url: string +): { domain: string; entryPoint: string } => { + let domain = url; + let entryPoint = '/'; + + const pathSlashIndex = url.search(/[^\:\/]\//); + if (pathSlashIndex !== -1) { + domain = url.substring(0, pathSlashIndex + 1); + entryPoint = url.substring(pathSlashIndex + 1); + } + + return { domain, entryPoint }; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index 3804ecfe7c67d..610ad1f571699 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -13,6 +13,7 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; +import { AddDomainFlyout } from './components/add_domain/add_domain_flyout'; import { DomainsTable } from './components/domains_table'; import { CrawlerOverview } from './crawler_overview'; @@ -44,7 +45,7 @@ describe('CrawlerOverview', () => { // TODO test for CrawlRequestsTable after it is built in a future PR - // TODO test for AddDomainForm after it is built in a future PR + expect(wrapper.find(AddDomainFlyout)).toHaveLength(1); // TODO test for empty state after it is built in a future PR }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx index 9e484df35e7a2..0daac399b7b09 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx @@ -9,9 +9,14 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + import { getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; +import { AddDomainFlyout } from './components/add_domain/add_domain_flyout'; import { DomainsTable } from './components/domains_table'; import { CRAWLER_TITLE } from './constants'; import { CrawlerOverviewLogic } from './crawler_overview_logic'; @@ -31,6 +36,21 @@ export const CrawlerOverview: React.FC = () => { pageHeader={{ pageTitle: CRAWLER_TITLE }} isLoading={dataLoading} > + + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.domainsTitle', { + defaultMessage: 'Domains', + })} +

+
+
+ + + +
+ ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx index a0145cf76908a..c5dd3907c9019 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx @@ -8,13 +8,15 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; +import { ENGINE_CRAWLER_PATH } from '../../routes'; + import { CrawlerLanding } from './crawler_landing'; import { CrawlerOverview } from './crawler_overview'; export const CrawlerRouter: React.FC = () => { return ( - + {process.env.NODE_ENV === 'development' ? : } diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts index b361e796b4f43..47cbef0bfd953 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts @@ -9,7 +9,7 @@ import '../../__mocks__/kea_logic/kibana_logic.mock'; import { FlashMessagesLogic } from './flash_messages_logic'; -import { flashAPIErrors } from './handle_api_errors'; +import { flashAPIErrors, getErrorsFromHttpResponse } from './handle_api_errors'; describe('flashAPIErrors', () => { const mockHttpError = { @@ -68,10 +68,29 @@ describe('flashAPIErrors', () => { try { flashAPIErrors(Error('whatever') as any); } catch (e) { - expect(e.message).toEqual('whatever'); expect(FlashMessagesLogic.actions.setFlashMessages).toHaveBeenCalledWith([ - { type: 'error', message: 'An unexpected error occurred' }, + { type: 'error', message: expect.any(String) }, ]); } }); }); + +describe('getErrorsFromHttpResponse', () => { + it('should return errors from the response if present', () => { + expect( + getErrorsFromHttpResponse({ + body: { attributes: { errors: ['first error', 'second error'] } }, + } as any) + ).toEqual(['first error', 'second error']); + }); + + it('should return a message from the responnse if no errors', () => { + expect(getErrorsFromHttpResponse({ body: { message: 'test message' } } as any)).toEqual([ + 'test message', + ]); + }); + + it('should return the a default message otherwise', () => { + expect(getErrorsFromHttpResponse({} as any)).toEqual([expect.any(String)]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts index 1b5dab0839663..7c82dfb971a1d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts @@ -40,13 +40,22 @@ export const defaultErrorMessage = i18n.translate( } ); +export const getErrorsFromHttpResponse = (response: HttpResponse) => { + return Array.isArray(response?.body?.attributes?.errors) + ? response.body!.attributes.errors + : [response?.body?.message || defaultErrorMessage]; +}; + /** * Converts API/HTTP errors into user-facing Flash Messages */ -export const flashAPIErrors = (error: HttpResponse, { isQueued }: Options = {}) => { - const errorFlashMessages: IFlashMessage[] = Array.isArray(error?.body?.attributes?.errors) - ? error.body!.attributes.errors.map((message) => ({ type: 'error', message })) - : [{ type: 'error', message: error?.body?.message || defaultErrorMessage }]; +export const flashAPIErrors = ( + response: HttpResponse, + { isQueued }: Options = {} +) => { + const errorFlashMessages: IFlashMessage[] = getErrorsFromHttpResponse( + response + ).map((message) => ({ type: 'error', message })); if (isQueued) { FlashMessagesLogic.actions.setQueuedMessages(errorFlashMessages); @@ -56,7 +65,7 @@ export const flashAPIErrors = (error: HttpResponse, { isQueued }: // If this was a programming error or a failed request (such as a CORS) error, // we rethrow the error so it shows up in the developer console - if (!error?.body?.message) { - throw error; + if (!response?.body?.message) { + throw response; } }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index 06a206017fbd1..fd478e35064c5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -43,6 +43,62 @@ describe('crawler routes', () => { }); }); + describe('POST /api/app_search/engines/{name}/crawler/domains', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{name}/crawler/domains', + }); + + registerCrawlerRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:name/crawler/domains', + }); + }); + + it('validates correctly with params and body', () => { + const request = { + params: { name: 'some-engine' }, + body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] }, + }; + mockRouter.shouldValidate(request); + }); + + it('accepts a query param', () => { + const request = { + params: { name: 'some-engine' }, + body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] }, + query: { respond_with: 'crawler_details' }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without a name param', () => { + const request = { + params: {}, + body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] }, + }; + mockRouter.shouldThrow(request); + }); + + it('fails validation without a body', () => { + const request = { + params: { name: 'some-engine' }, + body: {}, + }; + mockRouter.shouldThrow(request); + }); + }); + describe('DELETE /api/app_search/engines/{name}/crawler/domains/{id}', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index 6c8ed7a49c64a..35bfae763bb9a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -27,6 +27,31 @@ export function registerCrawlerRoutes({ }) ); + router.post( + { + path: '/api/app_search/engines/{name}/crawler/domains', + validate: { + params: schema.object({ + name: schema.string(), + }), + body: schema.object({ + name: schema.string(), + entry_points: schema.arrayOf( + schema.object({ + value: schema.string(), + }) + ), + }), + query: schema.object({ + respond_with: schema.maybe(schema.string()), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:name/crawler/domains', + }) + ); + router.delete( { path: '/api/app_search/engines/{name}/crawler/domains/{id}', diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index feeba68644334..4dc67a6771531 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -30,12 +30,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "isInitialized": { - "type": "boolean" - } - } + "$ref": "#/components/schemas/fleet_setup_response" } } } @@ -56,7 +51,7 @@ } } }, - "operationId": "post-setup", + "operationId": "setup", "parameters": [ { "$ref": "#/components/parameters/kbn_xsrf" @@ -64,9 +59,45 @@ ] } }, + "/settings": { + "get": { + "summary": "Settings", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/fleet_settings_response" + } + } + } + } + }, + "operationId": "get-settings" + }, + "post": { + "summary": "Settings - Update", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/fleet_settings_response" + } + } + } + } + }, + "operationId": "update-settings" + } + }, "/epm/categories": { "get": { - "summary": "Packages - Categories", + "summary": "Package categories", "tags": [], "responses": { "200": { @@ -99,7 +130,7 @@ } } }, - "operationId": "get-epm-categories" + "operationId": "get-package-categories" } }, "/epm/packages": { @@ -121,7 +152,7 @@ } } }, - "operationId": "get-epm-list" + "operationId": "list-all-packages" }, "parameters": [] }, @@ -168,7 +199,7 @@ } } }, - "operationId": "get-epm-package-pkgkey", + "operationId": "get-package", "security": [ { "basicAuth": [] @@ -205,7 +236,14 @@ "type": "string" }, "type": { - "type": "string" + "oneOf": [ + { + "$ref": "#/components/schemas/kibana_saved_object_type" + }, + { + "$ref": "#/components/schemas/elasticsearch_asset_type" + } + ] } }, "required": [ @@ -223,13 +261,27 @@ } } }, - "operationId": "post-epm-install-pkgkey", + "operationId": "install-package", "description": "", "parameters": [ { "$ref": "#/components/parameters/kbn_xsrf" } - ] + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "force": { + "type": "boolean" + } + } + } + } + } + } }, "delete": { "summary": "Packages - Delete", @@ -251,7 +303,14 @@ "type": "string" }, "type": { - "type": "string" + "oneOf": [ + { + "$ref": "#/components/schemas/kibana_saved_object_type" + }, + { + "$ref": "#/components/schemas/elasticsearch_asset_type" + } + ] } }, "required": [ @@ -269,7 +328,7 @@ } } }, - "operationId": "post-epm-delete-pkgkey", + "operationId": "delete-package", "parameters": [ { "$ref": "#/components/parameters/kbn_xsrf" @@ -301,21 +360,13 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "isInitialized": { - "type": "boolean" - } - }, - "required": [ - "isInitialized" - ] + "$ref": "#/components/schemas/fleet_status_response" } } } } }, - "operationId": "get-agents-setup", + "operationId": "get-agents-setup-status", "security": [ { "basicAuth": [] @@ -324,22 +375,14 @@ }, "post": { "summary": "Agents setup - Create", - "operationId": "post-agents-setup", + "operationId": "setup-agents", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "isInitialized": { - "type": "boolean" - } - }, - "required": [ - "isInitialized" - ] + "$ref": "#/components/schemas/fleet_setup_response" } } } @@ -425,7 +468,7 @@ } } }, - "operationId": "get-fleet-agent-status", + "operationId": "get-agent-status", "parameters": [ { "schema": { @@ -477,7 +520,7 @@ } } }, - "operationId": "get-fleet-agents", + "operationId": "get-agents", "security": [ { "basicAuth": [] @@ -495,7 +538,21 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/bulk_upgrade_agents" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "error": { + "type": "string" + } + }, + "required": [ + "success" + ] + } } } } @@ -511,7 +568,7 @@ } } }, - "operationId": "post-fleet-agents-bulk-upgrade", + "operationId": "bulk-upgrade-agents", "parameters": [ { "$ref": "#/components/parameters/kbn_xsrf" @@ -563,7 +620,7 @@ } } }, - "operationId": "get-fleet-agents-agentId" + "operationId": "get-agent" }, "put": { "summary": "Agent - Update", @@ -588,7 +645,7 @@ } } }, - "operationId": "put-fleet-agents-agentId", + "operationId": "update-agent", "parameters": [ { "$ref": "#/components/parameters/kbn_xsrf" @@ -621,7 +678,7 @@ } } }, - "operationId": "delete-fleet-agents-agentId", + "operationId": "delete-agent", "parameters": [ { "$ref": "#/components/parameters/kbn_xsrf" @@ -629,6 +686,58 @@ ] } }, + "/agents/{agentId}/reassign": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "put": { + "summary": "Agent - Reassign", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "operationId": "reassign-agent", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "policy_id": { + "type": "string" + } + }, + "required": [ + "policy_id" + ] + } + } + } + } + } + }, "/agents/{agentId}/unenroll": { "parameters": [ { @@ -679,7 +788,7 @@ } } }, - "operationId": "post-fleet-agents-unenroll", + "operationId": "unenroll-agent", "parameters": [ { "$ref": "#/components/parameters/kbn_xsrf" @@ -740,7 +849,7 @@ } } }, - "operationId": "post-fleet-agents-upgrade", + "operationId": "upgrade-agent", "parameters": [ { "$ref": "#/components/parameters/kbn_xsrf" @@ -758,6 +867,146 @@ } } }, + "/agents/bulk_reassign": { + "post": { + "summary": "Agents - Bulk reassign", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "error": { + "type": "string" + } + }, + "required": [ + "success" + ] + } + } + } + } + } + }, + "operationId": "bulk-reassign-agents", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "policy_id": { + "type": "string" + }, + "agents": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "required": [ + "policy_id", + "agents" + ] + } + } + } + } + } + }, + "/agents/bulk_unenroll": { + "post": { + "summary": "Agents - Bulk unenroll", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "error": { + "type": "string" + } + }, + "required": [ + "success" + ] + } + } + } + } + } + }, + "operationId": "bulk-unenroll-agents", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "revoke": { + "type": "boolean" + }, + "force": { + "type": "boolean" + }, + "agents": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "required": [ + "agents" + ] + } + } + } + } + } + }, "/agent_policies": { "get": { "summary": "Agent policies - List", @@ -831,7 +1080,7 @@ } } }, - "operationId": "post-agent-policy", + "operationId": "create-agent-policy", "requestBody": { "content": { "application/json": { @@ -910,7 +1159,7 @@ } } }, - "operationId": "put-agent-policy-agentPolicyId", + "operationId": "update-agent-policy", "requestBody": { "content": { "application/json": { @@ -992,29 +1241,26 @@ "/agent_policies/delete": { "post": { "summary": "Agent policy - Delete", - "operationId": "post-agent-policy-delete", + "operationId": "delete-agent-policy", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "success": { - "type": "boolean" - } + "type": "object", + "properties": { + "id": { + "type": "string" }, - "required": [ - "id", - "success" - ] - } + "success": { + "type": "boolean" + } + }, + "required": [ + "id", + "success" + ] } } } @@ -1026,13 +1272,13 @@ "schema": { "type": "object", "properties": { - "agentPolicyIds": { - "type": "array", - "items": { - "type": "string" - } + "agentPolicyId": { + "type": "string" } - } + }, + "required": [ + "agentPolicyId" + ] } } } @@ -1084,7 +1330,7 @@ } } }, - "operationId": "get-fleet-enrollment-api-keys", + "operationId": "get-enrollment-api-keys", "parameters": [] }, "post": { @@ -1113,7 +1359,7 @@ } } }, - "operationId": "post-fleet-enrollment-api-keys", + "operationId": "create-enrollment-api-keys", "parameters": [ { "$ref": "#/components/parameters/kbn_xsrf" @@ -1155,7 +1401,7 @@ } } }, - "operationId": "get-fleet-enrollment-api-keys-keyId" + "operationId": "get-enrollment-api-key" }, "delete": { "summary": "Enrollment API Key - Delete", @@ -1183,7 +1429,7 @@ } } }, - "operationId": "delete-fleet-enrollment-api-keys-keyId", + "operationId": "delete-enrollment-api-key", "parameters": [ { "$ref": "#/components/parameters/kbn_xsrf" @@ -1227,24 +1473,123 @@ } } }, - "operationId": "get-packagePolicies", + "operationId": "get-package-policies", "security": [], "parameters": [] }, "parameters": [], "post": { "summary": "Package policy - Create", - "operationId": "post-packagePolicies", + "operationId": "create-package-policy", "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/package_policy" + } + }, + "required": [ + "item" + ] + } + } + } } }, "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/new_package_policy" + "allOf": [ + { + "$ref": "#/components/schemas/new_package_policy" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + { + "type": "object", + "properties": { + "force": { + "type": "boolean" + } + } + } + ] + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/package_policies/delete": { + "post": { + "summary": "Package policy - Delete", + "operationId": "delete-package-policy", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "packagePolicyIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "force": { + "type": "boolean" + } + }, + "required": [ + "packagePolicyIds" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "id", + "success" + ] + } + } } } } @@ -1280,7 +1625,7 @@ } } }, - "operationId": "get-packagePolicies-packagePolicyId" + "operationId": "get-package-policy" }, "parameters": [ { @@ -1294,7 +1639,7 @@ ], "put": { "summary": "Package policy - Update", - "operationId": "put-packagePolicies-packagePolicyId", + "operationId": "update-package-policy", "requestBody": { "content": { "application/json": { @@ -1334,6 +1679,173 @@ } ] } + }, + "/outputs": { + "get": { + "summary": "Outputs", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/output" + } + }, + "total": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "perPage": { + "type": "integer" + } + } + } + } + } + } + }, + "operationId": "get-outputs" + } + }, + "/outputs/{outputId}": { + "get": { + "summary": "Output - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/output" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "operationId": "get-output" + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "outputId", + "in": "path", + "required": true + } + ], + "put": { + "summary": "Output - Update", + "operationId": "update-output", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "hosts": { + "type": "string" + }, + "ca_sha256": { + "type": "string" + }, + "config": { + "type": "object" + }, + "config_yaml": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/output" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/epm/packages/{pkgName}/stats": { + "get": { + "summary": "Get stats for a package", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "response": { + "$ref": "#/components/schemas/package_usage_stats" + } + }, + "required": [ + "response" + ] + } + } + } + } + }, + "operationId": "get-package-stats", + "security": [ + { + "basicAuth": [] + } + ] + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "pkgName", + "in": "path", + "required": true + } + ] } }, "components": { @@ -1393,8 +1905,76 @@ } }, "schemas": { + "fleet_setup_response": { + "title": "Fleet Setup response", + "type": "object", + "properties": { + "isInitialized": { + "type": "boolean" + }, + "nonFatalErrors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "name", + "message" + ] + } + } + }, + "required": [ + "isInitialized", + "nonFatalErrors" + ] + }, + "settings": { + "title": "Settings", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "has_seen_add_data_notice": { + "type": "boolean" + }, + "has_seen_fleet_migration_notice": { + "type": "boolean" + }, + "fleet_server_hosts": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fleet_server_hosts", + "id" + ] + }, + "fleet_settings_response": { + "title": "Fleet settings response", + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/settings" + } + }, + "required": [ + "item" + ] + }, "search_result": { - "title": "SearchResult", + "title": "Search result", "type": "object", "properties": { "description": { @@ -1441,7 +2021,7 @@ ] }, "package_info": { - "title": "PackageInfo", + "title": "Package information", "type": "object", "properties": { "name": { @@ -1618,9 +2198,61 @@ "path" ] }, + "kibana_saved_object_type": { + "title": "Kibana saved object asset type", + "type": "string", + "enum": [ + "dashboard", + "visualization", + "search", + "index-pattern", + "map", + "lens", + "ml-module", + "security-rule" + ] + }, + "elasticsearch_asset_type": { + "title": "Elasticsearch asset type", + "type": "string", + "enum": [ + "component_template", + "ingest_pipeline", + "index_template", + "ilm_policy", + "transform", + "data_stream_ilm_policy" + ] + }, + "fleet_status_response": { + "title": "Fleet status response", + "type": "object", + "properties": { + "isReady": { + "type": "boolean" + }, + "missing_requirements": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "tls_required", + "api_keys", + "fleet_admin_user", + "fleet_server", + "encrypted_saved_object_encryption_key_required" + ] + } + } + }, + "required": [ + "isReady", + "missing_requirements" + ] + }, "agent_type": { "type": "string", - "title": "AgentType", + "title": "Agent type", "enum": [ "PERMANENT", "EPHEMERAL", @@ -1628,12 +2260,12 @@ ] }, "agent_metadata": { - "title": "AgentMetadata", + "title": "Agent metadata", "type": "object" }, "agent_status": { "type": "string", - "title": "AgentStatus", + "title": "Agent status", "enum": [ "offline", "error", @@ -1708,69 +2340,36 @@ ] }, "bulk_upgrade_agents": { - "title": "BulkUpgradeAgents", - "oneOf": [ - { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "agents": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "version", - "agents" - ] + "title": "Bulk upgrade agents", + "type": "object", + "properties": { + "version": { + "type": "string" }, - { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "source_uri": { - "type": "string" - }, - "agents": { + "source_uri": { + "type": "string" + }, + "agents": { + "oneOf": [ + { "type": "array", "items": { "type": "string" } - } - }, - "required": [ - "version", - "agents" - ] - }, - { - "type": "object", - "properties": { - "version": { - "type": "string" }, - "source_uri": { - "type": "string" - }, - "agents": { + { "type": "string" } - }, - "required": [ - "version", - "agents" ] } + }, + "required": [ + "agents", + "version" ] }, "upgrade_agent": { - "title": "UpgradeAgent", + "title": "Upgrade agent", "oneOf": [ { "type": "object", @@ -1800,7 +2399,7 @@ ] }, "new_agent_policy": { - "title": "NewAgentPolicy", + "title": "New agent policy", "type": "object", "properties": { "name": { @@ -1815,7 +2414,7 @@ } }, "new_package_policy": { - "title": "NewPackagePolicy", + "title": "New package policy", "type": "object", "description": "", "properties": { @@ -1900,7 +2499,7 @@ ] }, "package_policy": { - "title": "PackagePolicy", + "title": "Package policy", "allOf": [ { "type": "object", @@ -1947,17 +2546,18 @@ "packagePolicies": { "oneOf": [ { + "type": "array", "items": { "type": "string" } }, { + "type": "array", "items": { "$ref": "#/components/schemas/package_policy" } } - ], - "type": "array" + ] }, "updated_on": { "type": "string", @@ -1981,7 +2581,7 @@ ] }, "enrollment_api_key": { - "title": "EnrollmentApiKey", + "title": "Enrollment API key", "type": "object", "properties": { "id": { @@ -2015,7 +2615,7 @@ ] }, "update_package_policy": { - "title": "UpdatePackagePolicy", + "title": "Update package policy", "allOf": [ { "type": "object", @@ -2029,6 +2629,63 @@ "$ref": "#/components/schemas/new_package_policy" } ] + }, + "output": { + "title": "Output", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "elasticsearch" + ] + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "ca_sha256": { + "type": "string" + }, + "api_key": { + "type": "string" + }, + "config": { + "type": "object" + }, + "config_yaml": { + "type": "string" + } + }, + "required": [ + "id", + "is_default", + "name", + "type" + ] + }, + "package_usage_stats": { + "title": "Package usage stats", + "type": "object", + "properties": { + "agent_policy_count": { + "type": "integer" + } + }, + "required": [ + "agent_policy_count" + ] } } }, diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 38daf60b33e0d..f2a12c0edb8a6 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -23,10 +23,7 @@ paths: content: application/json: schema: - type: object - properties: - isInitialized: - type: boolean + $ref: '#/components/schemas/fleet_setup_response' '500': description: Internal Server Error content: @@ -36,12 +33,35 @@ paths: properties: message: type: string - operationId: post-setup + operationId: setup parameters: - $ref: '#/components/parameters/kbn_xsrf' + /settings: + get: + summary: Settings + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/fleet_settings_response' + operationId: get-settings + post: + summary: Settings - Update + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/fleet_settings_response' + operationId: update-settings /epm/categories: get: - summary: Packages - Categories + summary: Package categories tags: [] responses: '200': @@ -63,7 +83,7 @@ paths: - id - title - count - operationId: get-epm-categories + operationId: get-package-categories /epm/packages: get: summary: Packages - List @@ -77,7 +97,7 @@ paths: type: array items: $ref: '#/components/schemas/search_result' - operationId: get-epm-list + operationId: list-all-packages parameters: [] '/epm/packages/{pkgkey}': get: @@ -105,7 +125,7 @@ paths: required: - status - savedObject - operationId: get-epm-package-pkgkey + operationId: get-package security: - basicAuth: [] parameters: @@ -133,16 +153,26 @@ paths: id: type: string type: - type: string + oneOf: + - $ref: '#/components/schemas/kibana_saved_object_type' + - $ref: '#/components/schemas/elasticsearch_asset_type' required: - id - type required: - response - operationId: post-epm-install-pkgkey + operationId: install-package description: '' parameters: - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: boolean delete: summary: Packages - Delete tags: [] @@ -162,13 +192,15 @@ paths: id: type: string type: - type: string + oneOf: + - $ref: '#/components/schemas/kibana_saved_object_type' + - $ref: '#/components/schemas/elasticsearch_asset_type' required: - id - type required: - response - operationId: post-epm-delete-pkgkey + operationId: delete-package parameters: - $ref: '#/components/parameters/kbn_xsrf' requestBody: @@ -189,30 +221,20 @@ paths: content: application/json: schema: - type: object - properties: - isInitialized: - type: boolean - required: - - isInitialized - operationId: get-agents-setup + $ref: '#/components/schemas/fleet_status_response' + operationId: get-agents-setup-status security: - basicAuth: [] post: summary: Agents setup - Create - operationId: post-agents-setup + operationId: setup-agents responses: '200': description: OK content: application/json: schema: - type: object - properties: - isInitialized: - type: boolean - required: - - isInitialized + $ref: '#/components/schemas/fleet_setup_response' requestBody: content: application/json: @@ -265,7 +287,7 @@ paths: - other - total - updating - operationId: get-fleet-agent-status + operationId: get-agent-status parameters: - schema: type: string @@ -299,7 +321,7 @@ paths: - total - page - perPage - operationId: get-fleet-agents + operationId: get-agents security: - basicAuth: [] /agents/bulk_upgrade: @@ -312,14 +334,23 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/bulk_upgrade_agents' + type: object + additionalProperties: + type: object + properties: + success: + type: boolean + error: + type: string + required: + - success '400': description: BAD REQUEST content: application/json: schema: $ref: '#/components/schemas/upgrade_agent' - operationId: post-fleet-agents-bulk-upgrade + operationId: bulk-upgrade-agents parameters: - $ref: '#/components/parameters/kbn_xsrf' requestBody: @@ -350,7 +381,7 @@ paths: $ref: '#/components/schemas/agent' required: - item - operationId: get-fleet-agents-agentId + operationId: get-agent put: summary: Agent - Update tags: [] @@ -366,7 +397,7 @@ paths: $ref: '#/components/schemas/agent' required: - item - operationId: put-fleet-agents-agentId + operationId: update-agent parameters: - $ref: '#/components/parameters/kbn_xsrf' delete: @@ -386,9 +417,40 @@ paths: - deleted required: - action - operationId: delete-fleet-agents-agentId + operationId: delete-agent + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/agents/{agentId}/reassign': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + put: + summary: Agent - Reassign + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + operationId: reassign-agent parameters: - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + policy_id: + type: string + required: + - policy_id '/agents/{agentId}/unenroll': parameters: - schema: @@ -421,7 +483,7 @@ paths: type: number enum: - 400 - operationId: post-fleet-agents-unenroll + operationId: unenroll-agent parameters: - $ref: '#/components/parameters/kbn_xsrf' requestBody: @@ -457,7 +519,7 @@ paths: application/json: schema: $ref: '#/components/schemas/upgrade_agent' - operationId: post-fleet-agents-upgrade + operationId: upgrade-agent parameters: - $ref: '#/components/parameters/kbn_xsrf' requestBody: @@ -466,6 +528,87 @@ paths: application/json: schema: $ref: '#/components/schemas/upgrade_agent' + /agents/bulk_reassign: + post: + summary: Agents - Bulk reassign + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + additionalProperties: + type: object + properties: + success: + type: boolean + error: + type: string + required: + - success + operationId: bulk-reassign-agents + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + policy_id: + type: string + agents: + oneOf: + - type: string + - type: array + items: + type: string + required: + - policy_id + - agents + /agents/bulk_unenroll: + post: + summary: Agents - Bulk unenroll + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + additionalProperties: + type: object + properties: + success: + type: boolean + error: + type: string + required: + - success + operationId: bulk-unenroll-agents + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + revoke: + type: boolean + force: + type: boolean + agents: + oneOf: + - type: string + - type: array + items: + type: string + required: + - agents /agent_policies: get: summary: Agent policies - List @@ -512,7 +655,7 @@ paths: properties: item: $ref: '#/components/schemas/agent_policy' - operationId: post-agent-policy + operationId: create-agent-policy requestBody: content: application/json: @@ -561,7 +704,7 @@ paths: $ref: '#/components/schemas/agent_policy' required: - item - operationId: put-agent-policy-agentPolicyId + operationId: update-agent-policy requestBody: content: application/json: @@ -609,34 +752,32 @@ paths: /agent_policies/delete: post: summary: Agent policy - Delete - operationId: post-agent-policy-delete + operationId: delete-agent-policy responses: '200': description: OK content: application/json: schema: - type: array - items: - type: object - properties: - id: - type: string - success: - type: boolean - required: - - id - - success + type: object + properties: + id: + type: string + success: + type: boolean + required: + - id + - success requestBody: content: application/json: schema: type: object properties: - agentPolicyIds: - type: array - items: - type: string + agentPolicyId: + type: string + required: + - agentPolicyId parameters: - $ref: '#/components/parameters/kbn_xsrf' parameters: [] @@ -667,7 +808,7 @@ paths: - page - perPage - total - operationId: get-fleet-enrollment-api-keys + operationId: get-enrollment-api-keys parameters: [] post: summary: Enrollment API Key - Create @@ -686,7 +827,7 @@ paths: type: string enum: - created - operationId: post-fleet-enrollment-api-keys + operationId: create-enrollment-api-keys parameters: - $ref: '#/components/parameters/kbn_xsrf' '/enrollment-api-keys/{keyId}': @@ -711,7 +852,7 @@ paths: $ref: '#/components/schemas/enrollment_api_key' required: - item - operationId: get-fleet-enrollment-api-keys-keyId + operationId: get-enrollment-api-key delete: summary: Enrollment API Key - Delete tags: [] @@ -729,7 +870,7 @@ paths: - deleted required: - action - operationId: delete-fleet-enrollment-api-keys-keyId + operationId: delete-enrollment-api-key parameters: - $ref: '#/components/parameters/kbn_xsrf' /package_policies: @@ -756,21 +897,78 @@ paths: type: number required: - items - operationId: get-packagePolicies + operationId: get-package-policies security: [] parameters: [] parameters: [] post: summary: Package policy - Create - operationId: post-packagePolicies + operationId: create-package-policy responses: '200': description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/package_policy' + required: + - item requestBody: content: application/json: schema: - $ref: '#/components/schemas/new_package_policy' + allOf: + - $ref: '#/components/schemas/new_package_policy' + - type: object + properties: + id: + type: string + - type: object + properties: + force: + type: boolean + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + /package_policies/delete: + post: + summary: Package policy - Delete + operationId: delete-package-policy + requestBody: + content: + application/json: + schema: + type: object + properties: + packagePolicyIds: + type: array + items: + type: string + force: + type: boolean + required: + - packagePolicyIds + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + success: + type: boolean + required: + - id + - success parameters: - $ref: '#/components/parameters/kbn_xsrf' '/package_policies/{packagePolicyId}': @@ -789,7 +987,7 @@ paths: $ref: '#/components/schemas/package_policy' required: - item - operationId: get-packagePolicies-packagePolicyId + operationId: get-package-policy parameters: - schema: type: string @@ -798,7 +996,7 @@ paths: required: true put: summary: Package policy - Update - operationId: put-packagePolicies-packagePolicyId + operationId: update-package-policy requestBody: content: application/json: @@ -821,6 +1019,108 @@ paths: - sucess parameters: - $ref: '#/components/parameters/kbn_xsrf' + /outputs: + get: + summary: Outputs + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/output' + total: + type: integer + page: + type: integer + perPage: + type: integer + operationId: get-outputs + '/outputs/{outputId}': + get: + summary: Output - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/output' + required: + - item + operationId: get-output + parameters: + - schema: + type: string + name: outputId + in: path + required: true + put: + summary: Output - Update + operationId: update-output + requestBody: + content: + application/json: + schema: + type: object + properties: + hosts: + type: string + ca_sha256: + type: string + config: + type: object + config_yaml: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/output' + required: + - item + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/epm/packages/{pkgName}/stats': + get: + summary: Get stats for a package + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + response: + $ref: '#/components/schemas/package_usage_stats' + required: + - response + operationId: get-package-stats + security: + - basicAuth: [] + parameters: + - schema: + type: string + name: pkgName + in: path + required: true components: securitySchemes: basicAuth: @@ -865,8 +1165,54 @@ components: schema: type: string schemas: + fleet_setup_response: + title: Fleet Setup response + type: object + properties: + isInitialized: + type: boolean + nonFatalErrors: + type: array + items: + type: object + properties: + name: + type: string + message: + type: string + required: + - name + - message + required: + - isInitialized + - nonFatalErrors + settings: + title: Settings + type: object + properties: + id: + type: string + has_seen_add_data_notice: + type: boolean + has_seen_fleet_migration_notice: + type: boolean + fleet_server_hosts: + type: array + items: + type: string + required: + - fleet_server_hosts + - id + fleet_settings_response: + title: Fleet settings response + type: object + properties: + item: + $ref: '#/components/schemas/settings' + required: + - item search_result: - title: SearchResult + title: Search result type: object properties: description: @@ -900,7 +1246,7 @@ components: - version - status package_info: - title: PackageInfo + title: Package information type: object properties: name: @@ -1018,19 +1364,60 @@ components: - format_version - download - path + kibana_saved_object_type: + title: Kibana saved object asset type + type: string + enum: + - dashboard + - visualization + - search + - index-pattern + - map + - lens + - ml-module + - security-rule + elasticsearch_asset_type: + title: Elasticsearch asset type + type: string + enum: + - component_template + - ingest_pipeline + - index_template + - ilm_policy + - transform + - data_stream_ilm_policy + fleet_status_response: + title: Fleet status response + type: object + properties: + isReady: + type: boolean + missing_requirements: + type: array + items: + type: string + enum: + - tls_required + - api_keys + - fleet_admin_user + - fleet_server + - encrypted_saved_object_encryption_key_required + required: + - isReady + - missing_requirements agent_type: type: string - title: AgentType + title: Agent type enum: - PERMANENT - EPHEMERAL - TEMPORARY agent_metadata: - title: AgentMetadata + title: Agent metadata type: object agent_status: type: string - title: AgentStatus + title: Agent status enum: - offline - error @@ -1083,45 +1470,24 @@ components: - id - status bulk_upgrade_agents: - title: BulkUpgradeAgents - oneOf: - - type: object - properties: - version: - type: string - agents: - type: array - items: - type: string - required: - - version - - agents - - type: object - properties: - version: - type: string - source_uri: - type: string - agents: - type: array + title: Bulk upgrade agents + type: object + properties: + version: + type: string + source_uri: + type: string + agents: + oneOf: + - type: array items: type: string - required: - - version - - agents - - type: object - properties: - version: - type: string - source_uri: - type: string - agents: - type: string - required: - - version - - agents + - type: string + required: + - agents + - version upgrade_agent: - title: UpgradeAgent + title: Upgrade agent oneOf: - type: object properties: @@ -1138,7 +1504,7 @@ components: required: - version new_agent_policy: - title: NewAgentPolicy + title: New agent policy type: object properties: name: @@ -1148,7 +1514,7 @@ components: description: type: string new_package_policy: - title: NewPackagePolicy + title: New package policy type: object description: '' properties: @@ -1207,7 +1573,7 @@ components: - policy_id - name package_policy: - title: PackagePolicy + title: Package policy allOf: - type: object properties: @@ -1236,11 +1602,12 @@ components: - inactive packagePolicies: oneOf: - - items: + - type: array + items: type: string - - items: + - type: array + items: $ref: '#/components/schemas/package_policy' - type: array updated_on: type: string format: date-time @@ -1254,7 +1621,7 @@ components: - id - status enrollment_api_key: - title: EnrollmentApiKey + title: Enrollment API key type: object properties: id: @@ -1278,12 +1645,51 @@ components: - active - created_at update_package_policy: - title: UpdatePackagePolicy + title: Update package policy allOf: - type: object properties: version: type: string - $ref: '#/components/schemas/new_package_policy' + output: + title: Output + type: object + properties: + id: + type: string + is_default: + type: boolean + name: + type: string + type: + type: string + enum: + - elasticsearch + hosts: + type: array + items: + type: string + ca_sha256: + type: string + api_key: + type: string + config: + type: object + config_yaml: + type: string + required: + - id + - is_default + - name + - type + package_usage_stats: + title: Package usage stats + type: object + properties: + agent_policy_count: + type: integer + required: + - agent_policy_count security: - basicAuth: [] diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/access_api_key.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/access_api_key.yaml deleted file mode 100644 index 31e2072ddefbe..0000000000000 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/access_api_key.yaml +++ /dev/null @@ -1,3 +0,0 @@ -type: string -title: AccessApiKey -format: byte diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/agent_metadata.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/agent_metadata.yaml index d37321f59a58b..5ec2d745dd14c 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/agent_metadata.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/agent_metadata.yaml @@ -1,2 +1,2 @@ -title: AgentMetadata +title: Agent metadata type: object diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml index 7395e45365ea9..7eed85eb2e3bc 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml @@ -11,11 +11,12 @@ allOf: - inactive packagePolicies: oneOf: - - items: + - type: array + items: type: string - - items: + - type: array + items: $ref: ./package_policy.yaml - type: array updated_on: type: string format: date-time diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/agent_status.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/agent_status.yaml index 076a7cc5036bb..da6df3a1b776d 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/agent_status.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/agent_status.yaml @@ -1,5 +1,5 @@ type: string -title: AgentStatus +title: Agent status enum: - offline - error diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/agent_type.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/agent_type.yaml index da42f95c9e1d9..421babbb1d5e4 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/agent_type.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/agent_type.yaml @@ -1,5 +1,5 @@ type: string -title: AgentType +title: Agent type enum: - PERMANENT - EPHEMERAL diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/bulk_upgrade_agents.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/bulk_upgrade_agents.yaml index da06aa6fa8252..31209d43fb58d 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/bulk_upgrade_agents.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/bulk_upgrade_agents.yaml @@ -1,37 +1,16 @@ -title: BulkUpgradeAgents -oneOf: - - type: object - properties: - version: - type: string - agents: - type: array +title: Bulk upgrade agents +type: object +properties: + version: + type: string + source_uri: + type: string + agents: + oneOf: + - type: array items: type: string - required: - - version - - agents - - type: object - properties: - version: - type: string - source_uri: - type: string - agents: - type: array - items: - type: string - required: - - version - - agents - - type: object - properties: - version: - type: string - source_uri: - type: string - agents: - type: string - required: - - version - - agents + - type: string +required: + - agents + - version diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/elasticsearch_asset_type.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/elasticsearch_asset_type.yaml new file mode 100644 index 0000000000000..19b3328d78346 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/elasticsearch_asset_type.yaml @@ -0,0 +1,9 @@ +title: Elasticsearch asset type +type: string +enum: + - component_template + - ingest_pipeline + - index_template + - ilm_policy + - transform + - data_stream_ilm_policy diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/enrollment_api_key.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/enrollment_api_key.yaml index e8491504d8416..7be406cf5b831 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/enrollment_api_key.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/enrollment_api_key.yaml @@ -1,4 +1,4 @@ -title: EnrollmentApiKey +title: Enrollment API key type: object properties: id: diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_settings_response.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_settings_response.yaml new file mode 100644 index 0000000000000..bb25cb54e599f --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_settings_response.yaml @@ -0,0 +1,7 @@ +title: Fleet settings response +type: object +properties: + item: + $ref: ./settings.yaml +required: + - item diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_setup_response.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_setup_response.yaml new file mode 100644 index 0000000000000..3022c394b1433 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_setup_response.yaml @@ -0,0 +1,20 @@ +title: Fleet Setup response +type: object +properties: + isInitialized: + type: boolean + nonFatalErrors: + type: array + items: + type: object + properties: + name: + type: string + message: + type: string + required: + - name + - message +required: + - isInitialized + - nonFatalErrors diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_status_response.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_status_response.yaml new file mode 100644 index 0000000000000..0d0c3db134386 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_status_response.yaml @@ -0,0 +1,18 @@ +title: Fleet status response +type: object +properties: + isReady: + type: boolean + missing_requirements: + type: array + items: + type: string + enum: + - 'tls_required' + - 'api_keys' + - 'fleet_admin_user' + - 'fleet_server' + - 'encrypted_saved_object_encryption_key_required' +required: + - isReady + - missing_requirements diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/kibana_saved_object_type.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/kibana_saved_object_type.yaml new file mode 100644 index 0000000000000..4ec82e7507166 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/kibana_saved_object_type.yaml @@ -0,0 +1,11 @@ +title: Kibana saved object asset type +type: string +enum: + - dashboard + - visualization + - search + - index-pattern + - map + - lens + - ml-module + - security-rule diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/new_agent_policy.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/new_agent_policy.yaml index 7070876cbea59..06048c81d979a 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/new_agent_policy.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/new_agent_policy.yaml @@ -1,4 +1,4 @@ -title: NewAgentPolicy +title: New agent policy type: object properties: name: diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/new_package_policy.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/new_package_policy.yaml index 61b1fa678d407..e5e4451881b57 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/new_package_policy.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/new_package_policy.yaml @@ -1,4 +1,4 @@ -title: NewPackagePolicy +title: New package policy type: object description: '' properties: diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/output.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/output.yaml new file mode 100644 index 0000000000000..b4e060ca0c151 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/output.yaml @@ -0,0 +1,29 @@ +title: Output +type: object +properties: + id: + type: string + is_default: + type: boolean + name: + type: string + type: + type: string + enum: ['elasticsearch'] + hosts: + type: array + items: + type: string + ca_sha256: + type: string + api_key: + type: string + config: + type: object + config_yaml: + type: string +required: + - id + - is_default + - name + - type diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/package_info.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/package_info.yaml index 3e0742c1879cb..ec4f18af8a223 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/package_info.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/package_info.yaml @@ -1,4 +1,4 @@ -title: PackageInfo +title: Package information type: object properties: name: diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/package_policy.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/package_policy.yaml index 99bc64f793379..4aead940ea27e 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/package_policy.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/package_policy.yaml @@ -1,4 +1,4 @@ -title: PackagePolicy +title: Package policy allOf: - type: object properties: diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/package_usage_stats.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/package_usage_stats.yaml new file mode 100644 index 0000000000000..55977e2141a63 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/package_usage_stats.yaml @@ -0,0 +1,7 @@ +title: Package usage stats +type: object +properties: + agent_policy_count: + type: integer +required: + - agent_policy_count diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/search_result.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/search_result.yaml index b67ff61c5ab60..89832f47db8cb 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/search_result.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/search_result.yaml @@ -1,4 +1,4 @@ -title: SearchResult +title: Search result type: object properties: description: diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/settings.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/settings.yaml new file mode 100644 index 0000000000000..952683400b230 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/settings.yaml @@ -0,0 +1,16 @@ +title: Settings +type: object +properties: + id: + type: string + has_seen_add_data_notice: + type: boolean + has_seen_fleet_migration_notice: + type: boolean + fleet_server_hosts: + type: array + items: + type: string +required: + - fleet_server_hosts + - id diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/update_package_policy.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/update_package_policy.yaml index 054a0e1a48be0..8f7f856a6649f 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/update_package_policy.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/update_package_policy.yaml @@ -1,4 +1,4 @@ -title: UpdatePackagePolicy +title: Update package policy allOf: - type: object properties: diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/upgrade_agent.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/upgrade_agent.yaml index 11a2b5846ba1e..19c796f8f8404 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/upgrade_agent.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/upgrade_agent.yaml @@ -1,4 +1,4 @@ -title: UpgradeAgent +title: Upgrade agent oneOf: - type: object properties: diff --git a/x-pack/plugins/fleet/common/openapi/entrypoint.yaml b/x-pack/plugins/fleet/common/openapi/entrypoint.yaml index 0f7129a2a7cec..ad8ef1408ae6b 100644 --- a/x-pack/plugins/fleet/common/openapi/entrypoint.yaml +++ b/x-pack/plugins/fleet/common/openapi/entrypoint.yaml @@ -16,6 +16,8 @@ paths: # plugin-wide endpoint(s) /setup: $ref: paths/setup.yaml + /settings: + $ref: paths/settings.yaml # EPM / integrations endpoints /epm/categories: $ref: paths/epm@categories.yaml @@ -34,10 +36,16 @@ paths: $ref: paths/agents@bulk_upgrade.yaml '/agents/{agentId}': $ref: 'paths/agents@{agent_id}.yaml' + '/agents/{agentId}/reassign': + $ref: 'paths/agents@{agent_id}@reassign.yaml' '/agents/{agentId}/unenroll': $ref: 'paths/agents@{agent_id}@unenroll.yaml' '/agents/{agentId}/upgrade': $ref: 'paths/agents@{agent_id}@upgrade.yaml' + '/agents/bulk_reassign': + $ref: 'paths/agents@bulk_reassign.yaml' + '/agents/bulk_unenroll': + $ref: 'paths/agents@bulk_unenroll.yaml' /agent_policies: $ref: paths/agent_policies.yaml '/agent_policies/{agentPolicyId}': @@ -52,8 +60,16 @@ paths: $ref: 'paths/enrollment_api_keys@{key_id}.yaml' /package_policies: $ref: paths/package_policies.yaml + /package_policies/delete: + $ref: paths/package_policies@delete.yaml '/package_policies/{packagePolicyId}': $ref: 'paths/package_policies@{package_policy_id}.yaml' + /outputs: + $ref: paths/outputs.yaml + /outputs/{outputId}: + $ref: paths/outputs@{output_id}.yaml + '/epm/packages/{pkgName}/stats': + $ref: 'paths/epm@packages@{pkg_name}@stats.yaml' components: securitySchemes: basicAuth: diff --git a/x-pack/plugins/fleet/common/openapi/paths/agent_policies.yaml b/x-pack/plugins/fleet/common/openapi/paths/agent_policies.yaml index 9c17680b6c6bd..b075d42d34af9 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agent_policies.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agent_policies.yaml @@ -43,7 +43,7 @@ post: properties: item: $ref: ../components/schemas/agent_policy.yaml - operationId: post-agent-policy + operationId: create-agent-policy requestBody: content: application/json: diff --git a/x-pack/plugins/fleet/common/openapi/paths/agent_policies@delete.yaml b/x-pack/plugins/fleet/common/openapi/paths/agent_policies@delete.yaml index ae975274d80e5..f136afb559603 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agent_policies@delete.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agent_policies@delete.yaml @@ -1,33 +1,31 @@ post: summary: Agent policy - Delete - operationId: post-agent-policy-delete + operationId: delete-agent-policy responses: '200': description: OK content: application/json: schema: - type: array - items: - type: object - properties: - id: - type: string - success: - type: boolean - required: - - id - - success + type: object + properties: + id: + type: string + success: + type: boolean + required: + - id + - success requestBody: content: application/json: schema: type: object properties: - agentPolicyIds: - type: array - items: - type: string + agentPolicyId: + type: string + required: + - agentPolicyId parameters: - $ref: ../components/headers/kbn_xsrf.yaml parameters: [] diff --git a/x-pack/plugins/fleet/common/openapi/paths/agent_policies@{agent_policy_id}.yaml b/x-pack/plugins/fleet/common/openapi/paths/agent_policies@{agent_policy_id}.yaml index 15910b0116b7f..9a60d197ed24a 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agent_policies@{agent_policy_id}.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agent_policies@{agent_policy_id}.yaml @@ -37,7 +37,7 @@ put: $ref: ../components/schemas/agent_policy.yaml required: - item - operationId: put-agent-policy-agentPolicyId + operationId: update-agent-policy requestBody: content: application/json: diff --git a/x-pack/plugins/fleet/common/openapi/paths/agent_status.yaml b/x-pack/plugins/fleet/common/openapi/paths/agent_status.yaml index 1b55cbd96733d..adc0ea79629af 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agent_status.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agent_status.yaml @@ -34,7 +34,7 @@ get: - other - total - updating - operationId: get-fleet-agent-status + operationId: get-agent-status parameters: - schema: type: string diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents.yaml index bb3905eab7c0e..4a217eda5c5ed 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents.yaml @@ -24,6 +24,6 @@ get: - total - page - perPage - operationId: get-fleet-agents + operationId: get-agents security: - basicAuth: [] diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@bulk_reassign.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@bulk_reassign.yaml new file mode 100644 index 0000000000000..a7d70f747cf92 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@bulk_reassign.yaml @@ -0,0 +1,39 @@ +post: + summary: Agents - Bulk reassign + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + additionalProperties: + type: object + properties: + success: + type: boolean + error: + type: string + required: + - success + operationId: bulk-reassign-agents + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + policy_id: + type: string + agents: + oneOf: + - type: string + - type: array + items: + type: string + required: + - policy_id + - agents diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@bulk_unenroll.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@bulk_unenroll.yaml new file mode 100644 index 0000000000000..55b10def7da7f --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@bulk_unenroll.yaml @@ -0,0 +1,40 @@ +post: + summary: Agents - Bulk unenroll + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + additionalProperties: + type: object + properties: + success: + type: boolean + error: + type: string + required: + - success + operationId: bulk-unenroll-agents + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + revoke: + type: boolean + force: + type: boolean + agents: + oneOf: + - type: string + - type: array + items: + type: string + required: + - agents diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@bulk_upgrade.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@bulk_upgrade.yaml index 37c7ad31c5b01..1467d1f98aa22 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents@bulk_upgrade.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@bulk_upgrade.yaml @@ -7,14 +7,23 @@ post: content: application/json: schema: - $ref: ../components/schemas/bulk_upgrade_agents.yaml + type: object + additionalProperties: + type: object + properties: + success: + type: boolean + error: + type: string + required: + - success '400': description: BAD REQUEST content: application/json: schema: $ref: ../components/schemas/upgrade_agent.yaml - operationId: post-fleet-agents-bulk-upgrade + operationId: bulk-upgrade-agents parameters: - $ref: ../components/headers/kbn_xsrf.yaml requestBody: diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@setup.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@setup.yaml index 87556dca0afbb..7d7f9561b2190 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents@setup.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@setup.yaml @@ -7,30 +7,20 @@ get: content: application/json: schema: - type: object - properties: - isInitialized: - type: boolean - required: - - isInitialized - operationId: get-agents-setup + $ref: ../components/schemas/fleet_status_response.yaml + operationId: get-agents-setup-status security: - basicAuth: [] post: summary: Agents setup - Create - operationId: post-agents-setup + operationId: setup-agents responses: '200': description: OK content: application/json: schema: - type: object - properties: - isInitialized: - type: boolean - required: - - isInitialized + $ref: ../components/schemas/fleet_setup_response.yaml requestBody: content: application/json: diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}.yaml index a898b9b563f17..c139fe8e7e997 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}.yaml @@ -19,7 +19,7 @@ get: $ref: ../components/schemas/agent.yaml required: - item - operationId: get-fleet-agents-agentId + operationId: get-agent put: summary: Agent - Update tags: [] @@ -35,7 +35,7 @@ put: $ref: ../components/schemas/agent.yaml required: - item - operationId: put-fleet-agents-agentId + operationId: update-agent parameters: - $ref: ../components/headers/kbn_xsrf.yaml delete: @@ -55,6 +55,6 @@ delete: - deleted required: - action - operationId: delete-fleet-agents-agentId + operationId: delete-agent parameters: - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@reassign.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@reassign.yaml new file mode 100644 index 0000000000000..6d2253be3bbc2 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@reassign.yaml @@ -0,0 +1,31 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +put: + summary: Agent - Reassign + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + operationId: reassign-agent + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + policy_id: + type: string + required: + - policy_id + diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@unenroll.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@unenroll.yaml index 5b848b715080b..b9664ae650112 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@unenroll.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@unenroll.yaml @@ -29,7 +29,7 @@ post: type: number enum: - 400 - operationId: post-fleet-agents-unenroll + operationId: unenroll-agent parameters: - $ref: ../components/headers/kbn_xsrf.yaml requestBody: diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@upgrade.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@upgrade.yaml index 14a0598ce6ecb..52489636f2fe9 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@upgrade.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@upgrade.yaml @@ -20,7 +20,7 @@ post: application/json: schema: $ref: ../components/schemas/upgrade_agent.yaml - operationId: post-fleet-agents-upgrade + operationId: upgrade-agent parameters: - $ref: ../components/headers/kbn_xsrf.yaml requestBody: diff --git a/x-pack/plugins/fleet/common/openapi/paths/enrollment_api_keys.yaml b/x-pack/plugins/fleet/common/openapi/paths/enrollment_api_keys.yaml index f00c46e7683e5..6cfbede4a7ead 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/enrollment_api_keys.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/enrollment_api_keys.yaml @@ -24,7 +24,7 @@ get: - page - perPage - total - operationId: get-fleet-enrollment-api-keys + operationId: get-enrollment-api-keys parameters: [] post: summary: Enrollment API Key - Create @@ -43,6 +43,6 @@ post: type: string enum: - created - operationId: post-fleet-enrollment-api-keys + operationId: create-enrollment-api-keys parameters: - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/fleet/common/openapi/paths/enrollment_api_keys@{key_id}.yaml b/x-pack/plugins/fleet/common/openapi/paths/enrollment_api_keys@{key_id}.yaml index 021f6582641ed..37c390897ef67 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/enrollment_api_keys@{key_id}.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/enrollment_api_keys@{key_id}.yaml @@ -19,7 +19,7 @@ get: $ref: ../components/schemas/enrollment_api_key.yaml required: - item - operationId: get-fleet-enrollment-api-keys-keyId + operationId: get-enrollment-api-key delete: summary: Enrollment API Key - Delete tags: [] @@ -37,6 +37,6 @@ delete: - deleted required: - action - operationId: delete-fleet-enrollment-api-keys-keyId + operationId: delete-enrollment-api-key parameters: - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@categories.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@categories.yaml index 567d621a2e21d..b673ea71b7786 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@categories.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@categories.yaml @@ -1,5 +1,5 @@ get: - summary: Packages - Categories + summary: Package categories tags: [] responses: '200': @@ -21,4 +21,4 @@ get: - id - title - count - operationId: get-epm-categories + operationId: get-package-categories diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml index fe79a9a4186b2..8ab62d3318070 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml @@ -10,5 +10,5 @@ get: type: array items: $ref: ../components/schemas/search_result.yaml - operationId: get-epm-list + operationId: list-all-packages parameters: [] diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@stats.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@stats.yaml new file mode 100644 index 0000000000000..9e9a4a57516dc --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@stats.yaml @@ -0,0 +1,24 @@ +get: + summary: Get stats for a package + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + response: + $ref: ../components/schemas/package_usage_stats.yaml + required: + - response + operationId: get-package-stats + security: + - basicAuth: [] +parameters: + - schema: + type: string + name: pkgName + in: path + required: true diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkgkey}.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkgkey}.yaml index 2c4567bd36ba1..1b15ddc4b22a3 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkgkey}.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkgkey}.yaml @@ -23,7 +23,7 @@ get: required: - status - savedObject - operationId: get-epm-package-pkgkey + operationId: get-package security: - basicAuth: [] parameters: @@ -51,16 +51,26 @@ post: id: type: string type: - type: string + oneOf: + - $ref: ../components/schemas/kibana_saved_object_type.yaml + - $ref: ../components/schemas/elasticsearch_asset_type.yaml required: - id - type required: - response - operationId: post-epm-install-pkgkey + operationId: install-package description: '' parameters: - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: boolean delete: summary: Packages - Delete tags: [] @@ -80,13 +90,15 @@ delete: id: type: string type: - type: string + oneOf: + - $ref: ../components/schemas/kibana_saved_object_type.yaml + - $ref: ../components/schemas/elasticsearch_asset_type.yaml required: - id - type required: - response - operationId: post-epm-delete-pkgkey + operationId: delete-package parameters: - $ref: ../components/headers/kbn_xsrf.yaml requestBody: diff --git a/x-pack/plugins/fleet/common/openapi/paths/outputs.yaml b/x-pack/plugins/fleet/common/openapi/paths/outputs.yaml new file mode 100644 index 0000000000000..94fe7c16e520d --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/paths/outputs.yaml @@ -0,0 +1,22 @@ +get: + summary: Outputs + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: ../components/schemas/output.yaml + total: + type: integer + page: + type: integer + perPage: + type: integer + operationId: get-outputs diff --git a/x-pack/plugins/fleet/common/openapi/paths/outputs@{output_id}.yaml b/x-pack/plugins/fleet/common/openapi/paths/outputs@{output_id}.yaml new file mode 100644 index 0000000000000..2f8f5e76ebaff --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/paths/outputs@{output_id}.yaml @@ -0,0 +1,53 @@ +get: + summary: Output - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/output.yaml + required: + - item + operationId: get-output +parameters: + - schema: + type: string + name: outputId + in: path + required: true +put: + summary: Output - Update + operationId: update-output + requestBody: + content: + application/json: + schema: + type: object + properties: + hosts: + type: string + ca_sha256: + type: string + config: + type: object + config_yaml: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/output.yaml + required: + - item + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/fleet/common/openapi/paths/package_policies.yaml b/x-pack/plugins/fleet/common/openapi/paths/package_policies.yaml index 24b3e091f1bc2..1d1263f16b01d 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/package_policies.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/package_policies.yaml @@ -21,20 +21,38 @@ get: type: number required: - items - operationId: get-packagePolicies + operationId: get-package-policies security: [] parameters: [] parameters: [] post: summary: Package policy - Create - operationId: post-packagePolicies + operationId: create-package-policy responses: '200': description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/package_policy.yaml + required: + - item requestBody: content: application/json: schema: - $ref: ../components/schemas/new_package_policy.yaml + allOf: + - $ref: ../components/schemas/new_package_policy.yaml + - type: object + properties: + id: + type: string + - type: object + properties: + force: + type: boolean parameters: - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/fleet/common/openapi/paths/package_policies@delete.yaml b/x-pack/plugins/fleet/common/openapi/paths/package_policies@delete.yaml new file mode 100644 index 0000000000000..ad907c6160803 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/paths/package_policies@delete.yaml @@ -0,0 +1,38 @@ +post: + summary: Package policy - Delete + operationId: delete-package-policy + requestBody: + content: + application/json: + schema: + type: object + properties: + packagePolicyIds: + type: array + items: + type: string + force: + type: boolean + required: + - packagePolicyIds + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + success: + type: boolean + required: + - id + - success + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/fleet/common/openapi/paths/package_policies@{package_policy_id}.yaml b/x-pack/plugins/fleet/common/openapi/paths/package_policies@{package_policy_id}.yaml index 9a8a1477fea78..7bd20ab17fdd3 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/package_policies@{package_policy_id}.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/package_policies@{package_policy_id}.yaml @@ -13,7 +13,7 @@ get: $ref: ../components/schemas/package_policy.yaml required: - item - operationId: get-packagePolicies-packagePolicyId + operationId: get-package-policy parameters: - schema: type: string @@ -22,7 +22,7 @@ parameters: required: true put: summary: Package policy - Update - operationId: put-packagePolicies-packagePolicyId + operationId: update-package-policy requestBody: content: application/json: diff --git a/x-pack/plugins/fleet/common/openapi/paths/settings.yaml b/x-pack/plugins/fleet/common/openapi/paths/settings.yaml new file mode 100644 index 0000000000000..b23fbc698e423 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/paths/settings.yaml @@ -0,0 +1,22 @@ +get: + summary: Settings + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: ../components/schemas/fleet_settings_response.yaml + operationId: get-settings +post: + summary: Settings - Update + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: ../components/schemas/fleet_settings_response.yaml + operationId: update-settings diff --git a/x-pack/plugins/fleet/common/openapi/paths/setup.yaml b/x-pack/plugins/fleet/common/openapi/paths/setup.yaml index a157dea8b38dc..abc17c0c9f6df 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/setup.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/setup.yaml @@ -7,10 +7,7 @@ post: content: application/json: schema: - type: object - properties: - isInitialized: - type: boolean + $ref: ../components/schemas/fleet_setup_response.yaml '500': description: Internal Server Error content: @@ -20,6 +17,6 @@ post: properties: message: type: string - operationId: post-setup + operationId: setup parameters: - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/fleet/common/types/rest_spec/fleet_setup.ts b/x-pack/plugins/fleet/common/types/rest_spec/fleet_setup.ts index 8bde56e5451c9..a637f19423a6b 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/fleet_setup.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/fleet_setup.ts @@ -5,8 +5,9 @@ * 2.0. */ -export interface CreateFleetSetupResponse { +export interface PostFleetSetupResponse { isInitialized: boolean; + nonFatalErrors: Array<{ name: string; message: string }>; } export interface GetFleetStatusResponse { diff --git a/x-pack/plugins/fleet/common/types/rest_spec/index.ts b/x-pack/plugins/fleet/common/types/rest_spec/index.ts index 870cf3f3f1b82..3ad6df3a97303 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/index.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/index.ts @@ -13,7 +13,6 @@ export * from './agent_policy'; export * from './fleet_setup'; export * from './epm'; export * from './enrollment_api_key'; -export * from './ingest_setup'; export * from './output'; export * from './settings'; export * from './app'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index ee529b6865e56..65496afc1a101 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -31,6 +31,7 @@ import { sendGetOnePackagePolicy, sendGetPackageInfoByKey, } from '../../../hooks'; +import { useBreadcrumbs as useIntegrationsBreadcrumbs } from '../../../../integrations/hooks'; import { Loading, Error, ExtensionWrapper } from '../../../components'; import { ConfirmDeployAgentPolicyModal } from '../components'; import { CreatePackagePolicyPageLayout } from '../create_package_policy_page/components'; @@ -492,6 +493,6 @@ const IntegrationsBreadcrumb = memo<{ policyName: string; pkgkey: string; }>(({ pkgTitle, policyName, pkgkey }) => { - useBreadcrumbs('integration_policy_edit', { policyName, pkgTitle, pkgkey }); + useIntegrationsBreadcrumbs('integration_policy_edit', { policyName, pkgTitle, pkgkey }); return null; }); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx index adc6ba44dbb18..178c1716d3355 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx @@ -6,41 +6,30 @@ */ import type { FunctionComponent } from 'react'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiButton, EuiCallOut, EuiSelect, EuiSpacer, EuiText } from '@elastic/eui'; import { SO_SEARCH_LIMIT } from '../../applications/fleet/constants'; -import type { GetEnrollmentAPIKeysResponse } from '../../applications/fleet/types'; +import type { + EnrollmentAPIKey, + GetEnrollmentAPIKeysResponse, +} from '../../applications/fleet/types'; import { sendGetEnrollmentAPIKeys, useStartServices, sendCreateEnrollmentAPIKey, } from '../../applications/fleet/hooks'; +import { Loading } from '../loading'; -interface Props { +const NoEnrollmentKeysCallout: React.FunctionComponent<{ agentPolicyId?: string; - selectedApiKeyId?: string; - initialAuthenticationSettingsOpen?: boolean; - onKeyChange: (key?: string) => void; -} - -export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ - agentPolicyId, - selectedApiKeyId, - initialAuthenticationSettingsOpen = false, - onKeyChange, -}) => { + onCreateEnrollmentApiKey: (key: EnrollmentAPIKey) => void; +}> = ({ agentPolicyId, onCreateEnrollmentApiKey }) => { const { notifications } = useStartServices(); - const [enrollmentAPIKeys, setEnrollmentAPIKeys] = useState( - [] - ); - const [isLoadingEnrollmentKey, setIsLoadingEnrollmentKey] = useState(false); - const [isAuthenticationSettingsOpen, setIsAuthenticationSettingsOpen] = useState( - initialAuthenticationSettingsOpen - ); + const [isLoadingEnrollmentKey, setIsLoadingEnrollmentKey] = useState(false); const onCreateEnrollmentTokenClick = async () => { setIsLoadingEnrollmentKey(true); if (agentPolicyId) { @@ -53,8 +42,7 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ if (!res.data?.item) { return; } - setEnrollmentAPIKeys([res.data.item]); - onKeyChange(res.data.item.id); + onCreateEnrollmentApiKey(res.data.item); notifications.toasts.addSuccess( i18n.translate('xpack.fleet.newEnrollmentKey.keyCreatedToasts', { defaultMessage: 'Enrollment token created', @@ -69,6 +57,69 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ } }; + return ( + +
+ +
+ + + + +
+ ); +}; + +interface Props { + agentPolicyId?: string; + selectedApiKeyId?: string; + initialAuthenticationSettingsOpen?: boolean; + onKeyChange: (key?: string) => void; +} + +export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ + agentPolicyId, + selectedApiKeyId, + initialAuthenticationSettingsOpen = false, + onKeyChange, +}) => { + const { notifications } = useStartServices(); + const [enrollmentAPIKeys, setEnrollmentAPIKeys] = useState( + [] + ); + + const [isAuthenticationSettingsOpen, setIsAuthenticationSettingsOpen] = useState( + initialAuthenticationSettingsOpen + ); + const [isLoadingEnrollmentApiKeys, setIsLoadingEnrollmentApiKeys] = useState(false); + + const onCreateEnrollmentApiKey = useCallback( + (key: EnrollmentAPIKey) => { + setEnrollmentAPIKeys([key]); + onKeyChange(key.id); + }, + [onKeyChange] + ); + useEffect( function useEnrollmentKeysForAgentPolicyEffect() { if (!agentPolicyId) { @@ -79,6 +130,7 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ async function fetchEnrollmentAPIKeys() { try { + setIsLoadingEnrollmentApiKeys(true); const res = await sendGetEnrollmentAPIKeys({ page: 1, perPage: SO_SEARCH_LIMIT, @@ -102,6 +154,8 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ notifications.toasts.addError(error, { title: 'Error', }); + } finally { + setIsLoadingEnrollmentApiKeys(false); } } fetchEnrollmentAPIKeys(); @@ -157,35 +211,13 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ onKeyChange(e.target.value); }} /> + ) : isLoadingEnrollmentApiKeys ? ( + ) : ( - -
- -
- - - - -
+ )} )} diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index f4a347c2ab3f0..0606334737a2a 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -28,7 +28,7 @@ import type { LicensingPluginSetup } from '../../licensing/public'; import type { CloudSetup } from '../../cloud/public'; import type { GlobalSearchPluginSetup } from '../../global_search/public'; import { PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, setupRouteService, appRoutesService } from '../common'; -import type { CheckPermissionsResponse, PostIngestSetupResponse } from '../common'; +import type { CheckPermissionsResponse, PostFleetSetupResponse } from '../common'; import type { FleetConfigType } from '../common/types'; @@ -223,7 +223,7 @@ export class FleetPlugin implements Plugin(setupRouteService.getSetupPath()) + .post(setupRouteService.getSetupPath()) .then(({ isInitialized }) => isInitialized ? Promise.resolve(true) diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index 809a045478b03..0e22f544ddfa3 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -7,7 +7,7 @@ import { httpServerMock } from 'src/core/server/mocks'; -import type { PostIngestSetupResponse } from '../../../common'; +import type { PostFleetSetupResponse } from '../../../common'; import { RegistryError } from '../../errors'; import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; import { appContextService } from '../../services/app_context'; @@ -53,7 +53,7 @@ describe('FleetSetupHandler', () => { ); await fleetSetupHandler(context, request, response); - const expectedBody: PostIngestSetupResponse = { isInitialized: true, nonFatalErrors: [] }; + const expectedBody: PostFleetSetupResponse = { isInitialized: true, nonFatalErrors: [] }; expect(response.customError).toHaveBeenCalledTimes(0); expect(response.ok).toHaveBeenCalledWith({ body: expectedBody }); }); diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index 370196cc202cd..fe1e30f9f05d6 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -8,7 +8,7 @@ import type { RequestHandler } from 'src/core/server'; import { appContextService } from '../../services'; -import type { GetFleetStatusResponse, PostIngestSetupResponse } from '../../../common'; +import type { GetFleetStatusResponse, PostFleetSetupResponse } from '../../../common'; import { setupIngestManager } from '../../services/setup'; import { hasFleetServers } from '../../services/fleet_server'; import { defaultIngestErrorHandler } from '../../errors'; @@ -46,21 +46,20 @@ export const fleetSetupHandler: RequestHandler = async (context, request, respon try { const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; - const body: PostIngestSetupResponse = await setupIngestManager(soClient, esClient); + const setupStatus = await setupIngestManager(soClient, esClient); + const body: PostFleetSetupResponse = { + ...setupStatus, + nonFatalErrors: setupStatus.nonFatalErrors.map((e) => { + // JSONify the error object so it can be displayed properly in the UI + const error = e.error ?? e; + return { + name: error.name, + message: error.message, + }; + }), + }; - return response.ok({ - body: { - ...body, - nonFatalErrors: body.nonFatalErrors?.map((e) => { - // JSONify the error object so it can be displayed properly in the UI - const error = e.error ?? e; - return { - name: error.name, - message: error.message, - }; - }), - }, - }); + return response.ok({ body }); } catch (error) { return defaultIngestErrorHandler({ error, response }); } diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 8302983316316..6881e0872606d 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -63,6 +63,19 @@ import { appContextService } from './app_context'; const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; +const MONITORING_DATASETS = [ + 'elastic_agent', + 'elastic_agent.elastic_agent', + 'elastic_agent.apm_server', + 'elastic_agent.filebeat', + 'elastic_agent.fleet_server', + 'elastic_agent.metricbeat', + 'elastic_agent.osquerybeat', + 'elastic_agent.packetbeat', + 'elastic_agent.endpoint_security', + 'elastic_agent.auditbeat', +]; + class AgentPolicyService { private triggerAgentPolicyUpdatedEvent = async ( soClient: SavedObjectsClientContract, @@ -762,7 +775,7 @@ class AgentPolicyService { cluster: DEFAULT_PERMISSIONS.cluster, }; - // TODO fetch this from the elastic agent package + // TODO: fetch this from the elastic agent package const monitoringOutput = fullAgentPolicy.agent?.monitoring.use_output; const monitoringNamespace = fullAgentPolicy.agent?.monitoring.namespace; if ( @@ -771,12 +784,16 @@ class AgentPolicyService { monitoringOutput && fullAgentPolicy.outputs[monitoringOutput]?.type === 'elasticsearch' ) { - const names: string[] = []; + let names: string[] = []; if (fullAgentPolicy.agent.monitoring.logs) { - names.push(`logs-elastic_agent*-${monitoringNamespace}`); + names = names.concat( + MONITORING_DATASETS.map((dataset) => `logs-${dataset}-${monitoringNamespace}`) + ); } if (fullAgentPolicy.agent.monitoring.metrics) { - names.push(`metrics-elastic_agent*-${monitoringNamespace}`); + names = names.concat( + MONITORING_DATASETS.map((dataset) => `metrics-${dataset}-${monitoringNamespace}`) + ); } permissions._elastic_agent_checks.indices = [ diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index cf06136c487e4..2361275563928 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -158,7 +158,7 @@ describe('policy preconfiguration', () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - const { policies, packages } = await ensurePreconfiguredPackagesAndPolicies( + const { policies, packages, nonFatalErrors } = await ensurePreconfiguredPackagesAndPolicies( soClient, esClient, [], @@ -168,13 +168,14 @@ describe('policy preconfiguration', () => { expect(policies.length).toBe(0); expect(packages.length).toBe(0); + expect(nonFatalErrors.length).toBe(0); }); it('should install packages successfully', async () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - const { policies, packages } = await ensurePreconfiguredPackagesAndPolicies( + const { policies, packages, nonFatalErrors } = await ensurePreconfiguredPackagesAndPolicies( soClient, esClient, [], @@ -184,13 +185,14 @@ describe('policy preconfiguration', () => { expect(policies.length).toBe(0); expect(packages).toEqual(expect.arrayContaining(['test_package-3.0.0'])); + expect(nonFatalErrors.length).toBe(0); }); it('should install packages and configure agent policies successfully', async () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - const { policies, packages } = await ensurePreconfiguredPackagesAndPolicies( + const { policies, packages, nonFatalErrors } = await ensurePreconfiguredPackagesAndPolicies( soClient, esClient, [ @@ -213,6 +215,7 @@ describe('policy preconfiguration', () => { expect(policies.length).toEqual(1); expect(policies[0].id).toBe('mocked-test-id'); expect(packages).toEqual(expect.arrayContaining(['test_package-3.0.0'])); + expect(nonFatalErrors.length).toBe(0); }); it('should throw an error when trying to install duplicate packages', async () => { @@ -235,11 +238,45 @@ describe('policy preconfiguration', () => { ); }); + it('should return nonFatalErrors', async () => { + const soClient = getPutPreconfiguredPackagesMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const policies: PreconfiguredAgentPolicy[] = [ + { + name: 'Test policy', + namespace: 'default', + id: 'test-id', + package_policies: [ + { + package: { name: 'test_package' }, + name: 'Test package', + }, + ], + }, + ]; + + const { nonFatalErrors } = await ensurePreconfiguredPackagesAndPolicies( + soClient, + esClient, + policies, + [{ name: 'CANNOT_MATCH', version: 'x.y.z' }], + mockDefaultOutput + ); + + expect(nonFatalErrors.length).toBe(1); + expect(nonFatalErrors[0].agentPolicy).toEqual({ name: 'Test policy' }); + expect(nonFatalErrors[0].error.message).toEqual( + 'Test policy could not be added. test_package is not installed, add test_package to `xpack.fleet.packages` or remove it from Test package.' + ); + }); it('should not attempt to recreate or modify an agent policy if its ID is unchanged', async () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - const { policies: policiesA } = await ensurePreconfiguredPackagesAndPolicies( + const { + policies: policiesA, + nonFatalErrors: nonFatalErrorsA, + } = await ensurePreconfiguredPackagesAndPolicies( soClient, esClient, [ @@ -256,8 +293,12 @@ describe('policy preconfiguration', () => { expect(policiesA.length).toEqual(1); expect(policiesA[0].id).toBe('mocked-test-id'); + expect(nonFatalErrorsA.length).toBe(0); - const { policies: policiesB } = await ensurePreconfiguredPackagesAndPolicies( + const { + policies: policiesB, + nonFatalErrors: nonFatalErrorsB, + } = await ensurePreconfiguredPackagesAndPolicies( soClient, esClient, [ @@ -280,6 +321,7 @@ describe('policy preconfiguration', () => { expect(policiesB.length).toEqual(1); expect(policiesB[0].id).toBe('mocked-test-id'); expect(policiesB[0].updated_at).toEqual(policiesA[0].updated_at); + expect(nonFatalErrorsB.length).toBe(0); }); }); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 0e24871628dcd..b3597ade23633 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -36,7 +36,7 @@ import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; interface PreconfigurationResult { policies: Array<{ id: string; updated_at: string }>; packages: string[]; - nonFatalErrors?: PreconfigurationError[]; + nonFatalErrors: PreconfigurationError[]; } export type InputsOverride = Partial & { diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index cfef04846d92e..1f3c3c5082b34 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -31,7 +31,7 @@ import { pkgToPkgKey } from './epm/registry'; export interface SetupStatus { isInitialized: boolean; - nonFatalErrors?: Array; + nonFatalErrors: Array; } export async function setupIngestManager( diff --git a/x-pack/plugins/infra/common/alerting/logs/log_threshold/index.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/index.ts index 3f4cbc82c405c..6cc0ccaa93a6d 100644 --- a/x-pack/plugins/infra/common/alerting/logs/log_threshold/index.ts +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export * from './rule_data'; export * from './types'; diff --git a/x-pack/plugins/infra/common/alerting/logs/log_threshold/rule_data.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/rule_data.ts deleted file mode 100644 index dd60739289756..0000000000000 --- a/x-pack/plugins/infra/common/alerting/logs/log_threshold/rule_data.ts +++ /dev/null @@ -1,19 +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 { jsonRt } from '@kbn/io-ts-utils/target/json_rt'; -import * as rt from 'io-ts'; -import { alertParamsRT as logThresholdAlertParamsRT } from './types'; - -export const serializedParamsKey = 'serialized_params'; - -export const logThresholdRuleDataNamespace = 'log_threshold_rule'; -export const logThresholdRuleDataSerializedParamsKey = `${logThresholdRuleDataNamespace}.${serializedParamsKey}` as const; - -export const logThresholdRuleDataRT = rt.type({ - [logThresholdRuleDataSerializedParamsKey]: rt.array(jsonRt.pipe(logThresholdAlertParamsRT)), -}); diff --git a/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts b/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts index 071432e4937c3..b9841abad6ab5 100644 --- a/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts +++ b/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts @@ -6,7 +6,7 @@ */ import * as rt from 'io-ts'; -import { DslQuery } from '../../../../../../src/plugins/data/common'; +import { DslQuery } from '@kbn/es-query'; import { logSourceColumnConfigurationRT } from '../../log_sources/log_source_configuration'; import { logEntryAfterCursorRT, diff --git a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx index 1f2998db4b43f..5cbd1909054af 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx @@ -32,6 +32,13 @@ export const MetricsAlertDropdown = () => { const closeFlyout = useCallback(() => setVisibleFlyoutType(null), [setVisibleFlyoutType]); + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, [setPopoverOpen]); + + const togglePopover = useCallback(() => { + setPopoverOpen(!popoverOpen); + }, [setPopoverOpen, popoverOpen]); const infrastructureAlertsPanel = useMemo( () => ({ id: 1, @@ -40,14 +47,18 @@ export const MetricsAlertDropdown = () => { }), items: [ { + 'data-test-subj': 'inventory-alerts-create-rule', name: i18n.translate('xpack.infra.alerting.createInventoryRuleButton', { defaultMessage: 'Create inventory rule', }), - onClick: () => setVisibleFlyoutType('inventory'), + onClick: () => { + closePopover(); + setVisibleFlyoutType('inventory'); + }, }, ], }), - [setVisibleFlyoutType] + [setVisibleFlyoutType, closePopover] ); const metricsAlertsPanel = useMemo( @@ -58,14 +69,18 @@ export const MetricsAlertDropdown = () => { }), items: [ { + 'data-test-subj': 'metrics-threshold-alerts-create-rule', name: i18n.translate('xpack.infra.alerting.createThresholdRuleButton', { defaultMessage: 'Create threshold rule', }), - onClick: () => setVisibleFlyoutType('threshold'), + onClick: () => { + closePopover(); + setVisibleFlyoutType('threshold'); + }, }, ], }), - [setVisibleFlyoutType] + [setVisibleFlyoutType, closePopover] ); const manageAlertsLinkProps = useLinkProps({ @@ -89,12 +104,14 @@ export const MetricsAlertDropdown = () => { canCreateAlerts ? [ { + 'data-test-subj': 'inventory-alerts-menu-option', name: i18n.translate('xpack.infra.alerting.infrastructureDropdownMenu', { defaultMessage: 'Infrastructure', }), panel: 1, }, { + 'data-test-subj': 'metrics-threshold-alerts-menu-option', name: i18n.translate('xpack.infra.alerting.metricsDropdownMenu', { defaultMessage: 'Metrics', }), @@ -120,14 +137,6 @@ export const MetricsAlertDropdown = () => { [infrastructureAlertsPanel, metricsAlertsPanel, firstPanelMenuItems, canCreateAlerts] ); - const closePopover = useCallback(() => { - setPopoverOpen(false); - }, [setPopoverOpen]); - - const openPopover = useCallback(() => { - setPopoverOpen(true); - }, [setPopoverOpen]); - return ( <> { color="text" iconSide={'right'} iconType={'arrowDown'} - onClick={openPopover} + onClick={togglePopover} + data-test-subj="infrastructure-alerts-and-rules" > { isOpen={popoverOpen} closePopover={closePopover} > - + diff --git a/x-pack/plugins/infra/public/alerting/inventory/index.ts b/x-pack/plugins/infra/public/alerting/inventory/index.ts index b66bd94553d3c..7d370c7106cb7 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/index.ts +++ b/x-pack/plugins/infra/public/alerting/inventory/index.ts @@ -12,16 +12,18 @@ import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../server/lib/alerting/inventory_metric_threshold/types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; + +import { ObservabilityRuleTypeModel } from '../../../../observability/public'; + import { AlertTypeParams } from '../../../../alerting/common'; import { validateMetricThreshold } from './components/validation'; +import { formatReason } from './rule_data_formatters'; interface InventoryMetricAlertTypeParams extends AlertTypeParams { criteria: InventoryMetricConditions[]; } -export function createInventoryMetricAlertType(): AlertTypeModel { +export function createInventoryMetricAlertType(): ObservabilityRuleTypeModel { return { id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertDescription', { @@ -44,5 +46,6 @@ Reason: } ), requiresAppContext: false, + format: formatReason, }; } diff --git a/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts b/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts new file mode 100644 index 0000000000000..1d8414d6abd23 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { ALERT_ID } from '@kbn/rule-data-utils'; +import { ObservabilityRuleTypeFormatter } from '../../../../observability/public'; + +export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { + const groupName = fields[ALERT_ID]; + const reason = i18n.translate('xpack.infra.metrics.alerting.inventory.alertReasonDescription', { + defaultMessage: 'Inventory alert for {groupName}.', // TEMP reason message, will be deleted once we index the reason field + values: { + groupName, + }, + }); + + const link = '/app/metrics/inventory'; + + return { + reason, + link, + }; +}; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx index 302de15db9f5a..ca291c49e49d1 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx @@ -54,13 +54,18 @@ export const AlertDropdown = () => { setPopoverOpen(true); }, [setPopoverOpen]); + const openFlyout = useCallback(() => { + setFlyoutVisible(true); + closePopover(); + }, [setFlyoutVisible, closePopover]); + const menuItems = useMemo(() => { return [ setFlyoutVisible(true)} + onClick={openFlyout} toolTipContent={!canCreateAlerts ? readOnlyUserTooltipContent : undefined} toolTipTitle={!canCreateAlerts ? readOnlyUserTooltipTitle : undefined} > @@ -76,7 +81,7 @@ export const AlertDropdown = () => { /> , ]; - }, [manageAlertsLinkProps, canCreateAlerts]); + }, [manageAlertsLinkProps, canCreateAlerts, openFlyout]); return ( <> diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts index 44097fd005cc7..b944b36fb9d1a 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts @@ -12,7 +12,7 @@ import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, PartialAlertParams, } from '../../../common/alerting/logs/log_threshold/types'; -import { formatReason } from './rule_data_formatters'; +import { formatRuleData } from './rule_data_formatters'; import { validateExpression } from './validation'; export function createLogThresholdAlertType(): ObservabilityRuleTypeModel { @@ -34,6 +34,6 @@ export function createLogThresholdAlertType(): ObservabilityRuleTypeModel { - const reason = pipe( - logThresholdRuleDataRT.decode(fields), - fold( - () => - i18n.translate('xpack.infra.logs.alerting.threshold.unknownReasonDescription', { - defaultMessage: 'unknown reason', - }), - (logThresholdRuleData) => { - const params = logThresholdRuleData[logThresholdRuleDataSerializedParamsKey][0]; - - const actualCount = fields[ALERT_EVALUATION_VALUE]; - const groupName = fields[ALERT_ID]; - const isGrouped = (params.groupBy?.length ?? 0) > 0; - const isRatio = ratioAlertParamsRT.is(params); - const thresholdCount = fields[ALERT_EVALUATION_THRESHOLD]; - const translatedComparator = ComparatorToi18nMap[params.count.comparator]; - - if (isRatio) { - return i18n.translate('xpack.infra.logs.alerting.threshold.ratioAlertReasonDescription', { - defaultMessage: - '{isGrouped, select, true{{groupName}: } false{}}The log entries ratio is {actualCount} ({translatedComparator} {thresholdCount}).', - values: { - actualCount, - translatedComparator, - groupName, - isGrouped, - thresholdCount, - }, - }); - } else { - return i18n.translate('xpack.infra.logs.alerting.threshold.countAlertReasonDescription', { - defaultMessage: - '{isGrouped, select, true{{groupName}: } false{}}{actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries} } ({translatedComparator} {thresholdCount}) match the configured conditions.', - values: { - actualCount, - translatedComparator, - groupName, - isGrouped, - thresholdCount, - }, - }); - } - } - ) - ); +export const formatRuleData: ObservabilityRuleTypeFormatter = ({ fields }) => { + const reason = fields[ALERT_REASON] ?? ''; const alertStartDate = fields[ALERT_START]; const timestamp = alertStartDate != null ? new Date(alertStartDate).valueOf() : null; const link = modifyUrl('/app/logs/link-to/default/logs', ({ query, ...otherUrlParts }) => ({ diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_error_boundary.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream_error_boundary.tsx index c55e6d299127b..41a9e13dcc178 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream_error_boundary.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_error_boundary.tsx @@ -8,7 +8,7 @@ import { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { KQLSyntaxError } from '../../../../../../src/plugins/data/common'; +import { KQLSyntaxError } from '@kbn/es-query'; import { RenderErrorFunc, ResettableErrorBoundary } from '../resettable_error_boundary'; export const LogStreamErrorBoundary: React.FC<{ resetOnChange: any }> = ({ diff --git a/x-pack/plugins/infra/public/components/saved_views/create_modal.tsx b/x-pack/plugins/infra/public/components/saved_views/create_modal.tsx index 654cba0721bb8..9aae695395614 100644 --- a/x-pack/plugins/infra/public/components/saved_views/create_modal.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/create_modal.tsx @@ -39,7 +39,7 @@ export const SavedViewCreateModal = ({ close, save, isInvalid }: Props) => { }, [includeTime, save, viewName]); return ( - + (props: Props) { <> @@ -163,8 +164,8 @@ export function SavedViewsToolbarControls(props: Props) { id="xpack.infra.savedView.currentView" /> - - + + {currentView ? currentView.name : i18n.translate('xpack.infra.savedView.unknownView', { @@ -182,6 +183,7 @@ export function SavedViewsToolbarControls(props: Props) { > (props: Props) { /> (props: Props) { /> (props: Props) { /> + ( <> {search} -
{list}
+
+ {list} +
)} diff --git a/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx b/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx index 78198717f8a03..df6cef5df7bea 100644 --- a/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx +++ b/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx @@ -7,9 +7,11 @@ import { useState, useCallback } from 'react'; import { SavedObjectAttributes, SavedObjectsBatchResponse } from 'src/core/public'; +import { useUiTracker } from '../../../observability/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; export const useFindSavedObject = (type: string) => { + const trackMetric = useUiTracker({ app: 'infra_metrics' }); const kibana = useKibana(); const [data, setData] = useState | null>(null); const [error, setError] = useState(null); @@ -27,10 +29,17 @@ export const useFindSavedObject = 1000) { + trackMetric({ metric: `over_1000_saved_objects_for_${type}` }); + } else { + trackMetric({ metric: `under_1000_saved_objects_for_${type}` }); + } } catch (e) { setLoading(false); setError(e); @@ -38,7 +47,7 @@ export const useFindSavedObject = { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx index 35265f0a462cf..c178532c13b92 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx @@ -97,7 +97,7 @@ export const MetricsExplorerChart = ({ : dataDomain; return ( -
+
{options.groupBy ? ( @@ -133,7 +133,7 @@ export const MetricsExplorerChart = ({ )} -
+
{metrics.length && series.rows.length > 0 ? ( {metrics.map((metric, id) => ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx index a239d607a17d2..aa051bc3ff442 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx @@ -39,7 +39,12 @@ export const MetricsExplorerChartOptions = ({ chartOptions, onChange }: Props) = }, []); const button = ( - + return ( { return ( diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index d5951d9ec9915..0eaeea60c63bf 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -34,12 +34,15 @@ export class Plugin implements InfraClientPluginClass { registerFeatures(pluginsSetup.home); } - pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createInventoryMetricAlertType()); + pluginsSetup.observability.observabilityRuleTypeRegistry.register( + createInventoryMetricAlertType() + ); + pluginsSetup.observability.observabilityRuleTypeRegistry.register( createLogThresholdAlertType() ); - pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType()); + pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType()); pluginsSetup.observability.dashboard.register({ appName: 'infra_logs', hasData: getLogsHasDataFetcher(core.getStartServices), diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 7a890ac14482a..025bc54e11cc9 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -12,12 +12,13 @@ import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_m import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertStates, InventoryMetricConditions } from './types'; import { + ActionGroupIdsOf, ActionGroup, AlertInstanceContext, AlertInstanceState, RecoveredActionGroup, } from '../../../../../alerting/common'; -import { AlertExecutorOptions } from '../../../../../alerting/server'; +import { AlertInstance, AlertTypeState } from '../../../../../alerting/server'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; import { InfraBackendLibs } from '../../infra_types'; import { METRIC_FORMATTERS } from '../../../../common/formatters/snapshot_metric_formats'; @@ -30,7 +31,6 @@ import { stateToAlertMessage, } from '../common/messages'; import { evaluateCondition } from './evaluate_condition'; -import { InventoryMetricThresholdAllowedActionGroups } from './register_inventory_metric_threshold_alert_type'; interface InventoryMetricThresholdParams { criteria: InventoryMetricConditions[]; @@ -40,145 +40,163 @@ interface InventoryMetricThresholdParams { alertOnNoData?: boolean; } -export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) => async ({ - services, - params, -}: AlertExecutorOptions< - /** - * TODO: Remove this use of `any` by utilizing a proper type - */ - Record, - Record, - AlertInstanceState, - AlertInstanceContext, +type InventoryMetricThresholdAllowedActionGroups = ActionGroupIdsOf< + typeof FIRED_ACTIONS | typeof WARNING_ACTIONS +>; + +export type InventoryMetricThresholdAlertTypeParams = Record; +export type InventoryMetricThresholdAlertTypeState = AlertTypeState; // no specific state used +export type InventoryMetricThresholdAlertInstanceState = AlertInstanceState; // no specific state used +export type InventoryMetricThresholdAlertInstanceContext = AlertInstanceContext; // no specific instance context used + +type InventoryMetricThresholdAlertInstance = AlertInstance< + InventoryMetricThresholdAlertInstanceState, + InventoryMetricThresholdAlertInstanceContext, InventoryMetricThresholdAllowedActionGroups ->) => { - const { - criteria, - filterQuery, - sourceId, - nodeType, - alertOnNoData, - } = params as InventoryMetricThresholdParams; - - if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); - - const source = await libs.sources.getSourceConfiguration( - services.savedObjectsClient, - sourceId || 'default' - ); - - const logQueryFields = await libs - .getLogQueryFields( - sourceId || 'default', - services.savedObjectsClient, - services.scopedClusterClient.asCurrentUser - ) - .catch(() => undefined); - - const compositeSize = libs.configuration.inventory.compositeSize; - - const results = await Promise.all( - criteria.map((condition) => - evaluateCondition({ - condition, - nodeType, - source, - logQueryFields, - esClient: services.scopedClusterClient.asCurrentUser, - compositeSize, - filterQuery, - }) - ) - ); - - const inventoryItems = Object.keys(first(results)!); - for (const item of inventoryItems) { - // AND logic; all criteria must be across the threshold - const shouldAlertFire = results.every((result) => - // Grab the result of the most recent bucket - last(result[item].shouldFire) +>; +type InventoryMetricThresholdAlertInstanceFactory = ( + id: string, + threshold?: number | undefined, + value?: number | undefined +) => InventoryMetricThresholdAlertInstance; + +export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) => + libs.metricsRules.createLifecycleRuleExecutor< + InventoryMetricThresholdAlertTypeParams, + InventoryMetricThresholdAlertTypeState, + InventoryMetricThresholdAlertInstanceState, + InventoryMetricThresholdAlertInstanceContext, + InventoryMetricThresholdAllowedActionGroups + >(async ({ services, params }) => { + const { + criteria, + filterQuery, + sourceId, + nodeType, + alertOnNoData, + } = params as InventoryMetricThresholdParams; + if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); + const { alertWithLifecycle, savedObjectsClient } = services; + const alertInstanceFactory: InventoryMetricThresholdAlertInstanceFactory = (id) => + alertWithLifecycle({ + id, + fields: {}, + }); + + const source = await libs.sources.getSourceConfiguration( + savedObjectsClient, + sourceId || 'default' ); - const shouldAlertWarn = results.every((result) => last(result[item].shouldWarn)); - - // AND logic; because we need to evaluate all criteria, if one of them reports no data then the - // whole alert is in a No Data/Error state - const isNoData = results.some((result) => last(result[item].isNoData)); - const isError = results.some((result) => result[item].isError); - - const nextState = isError - ? AlertStates.ERROR - : isNoData - ? AlertStates.NO_DATA - : shouldAlertFire - ? AlertStates.ALERT - : shouldAlertWarn - ? AlertStates.WARNING - : AlertStates.OK; - - let reason; - if (nextState === AlertStates.ALERT || nextState === AlertStates.WARNING) { - reason = results - .map((result) => - buildReasonWithVerboseMetricName( - result[item], - buildFiredAlertReason, - nextState === AlertStates.WARNING - ) - ) - .join('\n'); - /* - * Custom recovery actions aren't yet available in the alerting framework - * Uncomment the code below once they've been implemented - * Reference: https://github.com/elastic/kibana/issues/87048 - */ - // } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { - // reason = results - // .map((result) => buildReasonWithVerboseMetricName(result[item], buildRecoveredAlertReason)) - // .join('\n'); - } - if (alertOnNoData) { - if (nextState === AlertStates.NO_DATA) { - reason = results - .filter((result) => result[item].isNoData) - .map((result) => buildReasonWithVerboseMetricName(result[item], buildNoDataAlertReason)) - .join('\n'); - } else if (nextState === AlertStates.ERROR) { + + const logQueryFields = await libs + .getLogQueryFields( + sourceId || 'default', + services.savedObjectsClient, + services.scopedClusterClient.asCurrentUser + ) + .catch(() => undefined); + + const compositeSize = libs.configuration.inventory.compositeSize; + const results = await Promise.all( + criteria.map((condition) => + evaluateCondition({ + condition, + nodeType, + source, + logQueryFields, + esClient: services.scopedClusterClient.asCurrentUser, + compositeSize, + filterQuery, + }) + ) + ); + const inventoryItems = Object.keys(first(results)!); + for (const item of inventoryItems) { + // AND logic; all criteria must be across the threshold + const shouldAlertFire = results.every((result) => { + // Grab the result of the most recent bucket + return last(result[item].shouldFire); + }); + const shouldAlertWarn = results.every((result) => last(result[item].shouldWarn)); + + // AND logic; because we need to evaluate all criteria, if one of them reports no data then the + // whole alert is in a No Data/Error state + const isNoData = results.some((result) => last(result[item].isNoData)); + const isError = results.some((result) => result[item].isError); + + const nextState = isError + ? AlertStates.ERROR + : isNoData + ? AlertStates.NO_DATA + : shouldAlertFire + ? AlertStates.ALERT + : shouldAlertWarn + ? AlertStates.WARNING + : AlertStates.OK; + let reason; + if (nextState === AlertStates.ALERT || nextState === AlertStates.WARNING) { reason = results - .filter((result) => result[item].isError) - .map((result) => buildReasonWithVerboseMetricName(result[item], buildErrorAlertReason)) + .map((result) => + buildReasonWithVerboseMetricName( + result[item], + buildFiredAlertReason, + nextState === AlertStates.WARNING + ) + ) .join('\n'); - } - } - if (reason) { - const actionGroupId = - nextState === AlertStates.OK - ? RecoveredActionGroup.id - : nextState === AlertStates.WARNING - ? WARNING_ACTIONS.id - : FIRED_ACTIONS.id; - const alertInstance = services.alertInstanceFactory(`${item}`); - alertInstance.scheduleActions( - /** - * TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on - * the RecoveredActionGroup isn't allowed + /* + * Custom recovery actions aren't yet available in the alerting framework + * Uncomment the code below once they've been implemented + * Reference: https://github.com/elastic/kibana/issues/87048 */ - (actionGroupId as unknown) as InventoryMetricThresholdAllowedActionGroups, - { - group: item, - alertState: stateToAlertMessage[nextState], - reason, - timestamp: moment().toISOString(), - value: mapToConditionsLookup(results, (result) => - formatMetric(result[item].metric, result[item].currentValue) - ), - threshold: mapToConditionsLookup(criteria, (c) => c.threshold), - metric: mapToConditionsLookup(criteria, (c) => c.metric), + // } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { + // reason = results + // .map((result) => buildReasonWithVerboseMetricName(result[item], buildRecoveredAlertReason)) + // .join('\n'); + } + if (alertOnNoData) { + if (nextState === AlertStates.NO_DATA) { + reason = results + .filter((result) => result[item].isNoData) + .map((result) => buildReasonWithVerboseMetricName(result[item], buildNoDataAlertReason)) + .join('\n'); + } else if (nextState === AlertStates.ERROR) { + reason = results + .filter((result) => result[item].isError) + .map((result) => buildReasonWithVerboseMetricName(result[item], buildErrorAlertReason)) + .join('\n'); } - ); + } + if (reason) { + const actionGroupId = + nextState === AlertStates.OK + ? RecoveredActionGroup.id + : nextState === AlertStates.WARNING + ? WARNING_ACTIONS.id + : FIRED_ACTIONS.id; + + const alertInstance = alertInstanceFactory(`${item}`); + alertInstance.scheduleActions( + /** + * TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on + * the RecoveredActionGroup isn't allowed + */ + (actionGroupId as unknown) as InventoryMetricThresholdAllowedActionGroups, + { + group: item, + alertState: stateToAlertMessage[nextState], + reason, + timestamp: moment().toISOString(), + value: mapToConditionsLookup(results, (result) => + formatMetric(result[item].metric, result[item].currentValue) + ), + threshold: mapToConditionsLookup(criteria, (c) => c.threshold), + metric: mapToConditionsLookup(criteria, (c) => c.metric), + } + ); + } } - } -}; + }); const buildReasonWithVerboseMetricName = ( resultItem: any, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index 5410353ac46a0..5d516f3591419 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -7,12 +7,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { - AlertType, - AlertInstanceState, - AlertInstanceContext, - ActionGroupIdsOf, -} from '../../../../../alerting/server'; +import { PluginSetupContract } from '../../../../../alerting/server'; import { createInventoryMetricThresholdExecutor, FIRED_ACTIONS, @@ -51,56 +46,45 @@ const condition = schema.object({ ), }); -export type InventoryMetricThresholdAllowedActionGroups = ActionGroupIdsOf< - typeof FIRED_ACTIONS | typeof WARNING_ACTIONS ->; - -export const registerMetricInventoryThresholdAlertType = ( +export async function registerMetricInventoryThresholdAlertType( + alertingPlugin: PluginSetupContract, libs: InfraBackendLibs -): AlertType< - /** - * TODO: Remove this use of `any` by utilizing a proper type - */ - Record, - never, // Only use if defining useSavedObjectReferences hook - Record, - AlertInstanceState, - AlertInstanceContext, - InventoryMetricThresholdAllowedActionGroups -> => ({ - id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, - name: i18n.translate('xpack.infra.metrics.inventory.alertName', { - defaultMessage: 'Inventory', - }), - validate: { - params: schema.object( - { - criteria: schema.arrayOf(condition), - nodeType: schema.string(), - filterQuery: schema.maybe( - schema.string({ validate: validateIsStringElasticsearchJSONFilter }) - ), - sourceId: schema.string(), - alertOnNoData: schema.maybe(schema.boolean()), - }, - { unknowns: 'allow' } - ), - }, - defaultActionGroupId: FIRED_ACTIONS_ID, - actionGroups: [FIRED_ACTIONS, WARNING_ACTIONS], - producer: 'infrastructure', - minimumLicenseRequired: 'basic', - isExportable: true, - executor: createInventoryMetricThresholdExecutor(libs), - actionVariables: { - context: [ - { name: 'group', description: groupActionVariableDescription }, - { name: 'alertState', description: alertStateActionVariableDescription }, - { name: 'reason', description: reasonActionVariableDescription }, - { name: 'timestamp', description: timestampActionVariableDescription }, - { name: 'value', description: valueActionVariableDescription }, - { name: 'metric', description: metricActionVariableDescription }, - { name: 'threshold', description: thresholdActionVariableDescription }, - ], - }, -}); +) { + alertingPlugin.registerType({ + id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + name: i18n.translate('xpack.infra.metrics.inventory.alertName', { + defaultMessage: 'Inventory', + }), + validate: { + params: schema.object( + { + criteria: schema.arrayOf(condition), + nodeType: schema.string(), + filterQuery: schema.maybe( + schema.string({ validate: validateIsStringElasticsearchJSONFilter }) + ), + sourceId: schema.string(), + alertOnNoData: schema.maybe(schema.boolean()), + }, + { unknowns: 'allow' } + ), + }, + defaultActionGroupId: FIRED_ACTIONS_ID, + actionGroups: [FIRED_ACTIONS, WARNING_ACTIONS], + producer: 'infrastructure', + minimumLicenseRequired: 'basic', + isExportable: true, + executor: createInventoryMetricThresholdExecutor(libs), + actionVariables: { + context: [ + { name: 'group', description: groupActionVariableDescription }, + { name: 'alertState', description: alertStateActionVariableDescription }, + { name: 'reason', description: reasonActionVariableDescription }, + { name: 'timestamp', description: timestampActionVariableDescription }, + { name: 'value', description: valueActionVariableDescription }, + { name: 'metric', description: metricActionVariableDescription }, + { name: 'threshold', description: thresholdActionVariableDescription }, + ], + }, + }); +} diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index 55c66f0aabbfb..1f0521070a1e5 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -432,7 +432,7 @@ describe('Log threshold executor', () => { { actionGroup: 'logs.threshold.fired', context: { - conditions: ' numericField more than 10', + conditions: 'numericField more than 10', group: null, matchingDocuments: 10, isRatio: false, @@ -497,7 +497,7 @@ describe('Log threshold executor', () => { { actionGroup: 'logs.threshold.fired', context: { - conditions: ' numericField more than 10', + conditions: 'numericField more than 10', group: 'i-am-a-host-name-1, i-am-a-dataset-1', matchingDocuments: 10, isRatio: false, @@ -512,7 +512,7 @@ describe('Log threshold executor', () => { { actionGroup: 'logs.threshold.fired', context: { - conditions: ' numericField more than 10', + conditions: 'numericField more than 10', group: 'i-am-a-host-name-3, i-am-a-dataset-3', matchingDocuments: 20, isRatio: false, diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 241931e610af0..a1bdbdfd14c12 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -7,7 +7,11 @@ import { estypes } from '@elastic/elasticsearch'; import { i18n } from '@kbn/i18n'; -import { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE } from '@kbn/rule-data-utils'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_REASON, +} from '@kbn/rule-data-utils'; import { ElasticsearchClient } from 'kibana/server'; import { ActionGroup, @@ -17,10 +21,6 @@ import { AlertInstanceState, AlertTypeState, } from '../../../../../alerting/server'; -import { - logThresholdRuleDataRT, - logThresholdRuleDataSerializedParamsKey, -} from '../../../../common/alerting/logs/log_threshold'; import { AlertParams, alertParamsRT, @@ -46,6 +46,12 @@ import { decodeOrThrow } from '../../../../common/runtime_types'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { InfraBackendLibs } from '../../infra_types'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; +import { + getReasonMessageForGroupedCountAlert, + getReasonMessageForGroupedRatioAlert, + getReasonMessageForUngroupedCountAlert, + getReasonMessageForUngroupedRatioAlert, +} from './reason_formatters'; export type LogThresholdActionGroups = ActionGroupIdsOf; export type LogThresholdAlertTypeParams = AlertParams; @@ -60,6 +66,7 @@ type LogThresholdAlertInstance = AlertInstance< >; type LogThresholdAlertInstanceFactory = ( id: string, + reason: string, threshold: number, value: number ) => LogThresholdAlertInstance; @@ -89,15 +96,13 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => >(async ({ services, params }) => { const { alertWithLifecycle, savedObjectsClient, scopedClusterClient } = services; const { sources } = libs; - const alertInstanceFactory: LogThresholdAlertInstanceFactory = (id, threshold, value) => + const alertInstanceFactory: LogThresholdAlertInstanceFactory = (id, reason, threshold, value) => alertWithLifecycle({ id, fields: { [ALERT_EVALUATION_THRESHOLD]: threshold, [ALERT_EVALUATION_VALUE]: value, - ...logThresholdRuleDataRT.encode({ - [logThresholdRuleDataSerializedParamsKey]: [params], - }), + [ALERT_REASON]: reason, }, }); @@ -243,7 +248,12 @@ export const processUngroupedResults = ( const documentCount = results.hits.total.value; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY, count.value, documentCount); + const alertInstance = alertInstanceFactory( + UNGROUPED_FACTORY_KEY, + getReasonMessageForUngroupedCountAlert(documentCount, count.value, count.comparator), + count.value, + documentCount + ); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -272,7 +282,12 @@ export const processUngroupedRatioResults = ( const ratio = getRatio(numeratorCount, denominatorCount); if (ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value)) { - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY, count.value, ratio); + const alertInstance = alertInstanceFactory( + UNGROUPED_FACTORY_KEY, + getReasonMessageForUngroupedRatioAlert(ratio, count.value, count.comparator), + count.value, + ratio + ); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -341,7 +356,17 @@ export const processGroupByResults = ( const documentCount = group.documentCount; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { - const alertInstance = alertInstanceFactory(group.name, count.value, documentCount); + const alertInstance = alertInstanceFactory( + group.name, + getReasonMessageForGroupedCountAlert( + documentCount, + count.value, + count.comparator, + group.name + ), + count.value, + documentCount + ); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -382,7 +407,17 @@ export const processGroupByRatioResults = ( ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value) ) { - const alertInstance = alertInstanceFactory(numeratorGroup.name, count.value, ratio); + const alertInstance = alertInstanceFactory( + numeratorGroup.name, + getReasonMessageForGroupedRatioAlert( + ratio, + count.value, + count.comparator, + numeratorGroup.name + ), + count.value, + ratio + ); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -748,13 +783,13 @@ const getGroupedResults = async (query: object, esClient: ElasticsearchClient) = return compositeGroupBuckets; }; -const createConditionsMessageForCriteria = (criteria: CountCriteria) => { - const parts = criteria.map((criterion, index) => { - const { field, comparator, value } = criterion; - return `${index === 0 ? '' : 'and'} ${field} ${comparator} ${value}`; - }); - return parts.join(' '); -}; +const createConditionsMessageForCriteria = (criteria: CountCriteria) => + criteria + .map((criterion) => { + const { field, comparator, value } = criterion; + return `${field} ${comparator} ${value}`; + }) + .join(' and '); // When the Alerting plugin implements support for multiple action groups, add additional // action groups here to send different messages, e.g. a recovery notification diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts new file mode 100644 index 0000000000000..cd579b9965b66 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.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 { i18n } from '@kbn/i18n'; +import { + Comparator, + ComparatorToi18nMap, +} from '../../../../common/alerting/logs/log_threshold/types'; + +export const getReasonMessageForUngroupedCountAlert = ( + actualCount: number, + expectedCount: number, + comparator: Comparator +) => + i18n.translate('xpack.infra.logs.alerting.threshold.ungroupedCountAlertReasonDescription', { + defaultMessage: + '{actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries} } ({translatedComparator} {expectedCount}) match the conditions.', + values: { + actualCount, + expectedCount, + translatedComparator: ComparatorToi18nMap[comparator], + }, + }); + +export const getReasonMessageForGroupedCountAlert = ( + actualCount: number, + expectedCount: number, + comparator: Comparator, + groupName: string +) => + i18n.translate('xpack.infra.logs.alerting.threshold.groupedCountAlertReasonDescription', { + defaultMessage: + '{groupName}: {actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries} } ({translatedComparator} {expectedCount}) match the conditions.', + values: { + actualCount, + expectedCount, + groupName, + translatedComparator: ComparatorToi18nMap[comparator], + }, + }); + +export const getReasonMessageForUngroupedRatioAlert = ( + actualRatio: number, + expectedRatio: number, + comparator: Comparator +) => + i18n.translate('xpack.infra.logs.alerting.threshold.ungroupedRatioAlertReasonDescription', { + defaultMessage: + 'The log entries ratio is {actualRatio} ({translatedComparator} {expectedRatio}).', + values: { + actualRatio, + expectedRatio, + translatedComparator: ComparatorToi18nMap[comparator], + }, + }); + +export const getReasonMessageForGroupedRatioAlert = ( + actualRatio: number, + expectedRatio: number, + comparator: Comparator, + groupName: string +) => + i18n.translate('xpack.infra.logs.alerting.threshold.groupedRatioAlertReasonDescription', { + defaultMessage: + '{groupName}: The log entries ratio is {actualRatio} ({translatedComparator} {expectedRatio}).', + values: { + actualRatio, + expectedRatio, + groupName, + translatedComparator: ComparatorToi18nMap[comparator], + }, + }); diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts index b5e6f714de77e..d7df2afd8038b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -21,10 +21,9 @@ const registerAlertTypes = ( ) => { if (alertingPlugin) { alertingPlugin.registerType(registerMetricThresholdAlertType(libs)); - alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs)); alertingPlugin.registerType(registerMetricAnomalyAlertType(libs, ml)); - const registerFns = [registerLogThresholdAlertType]; + const registerFns = [registerLogThresholdAlertType, registerMetricInventoryThresholdAlertType]; registerFns.forEach((fn) => { fn(alertingPlugin, libs); }); diff --git a/x-pack/plugins/infra/server/services/rules/rule_data_client.ts b/x-pack/plugins/infra/server/services/rules/rule_data_client.ts index d693be40f10d0..f67837cff0df1 100644 --- a/x-pack/plugins/infra/server/services/rules/rule_data_client.ts +++ b/x-pack/plugins/infra/server/services/rules/rule_data_client.ts @@ -9,7 +9,6 @@ import { once } from 'lodash'; import { CoreSetup, Logger } from 'src/core/server'; import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../../../rule_registry/common/assets'; import { RuleRegistryPluginSetupContract } from '../../../../rule_registry/server'; -import { logThresholdRuleDataNamespace } from '../../../common/alerting/logs/log_threshold'; import type { InfraFeatureId } from '../../../common/constants'; import { RuleRegistrationContext, RulesServiceStartDeps } from './types'; @@ -44,18 +43,7 @@ export const createRuleDataClient = ({ settings: { number_of_shards: 1, }, - mappings: { - properties: { - [logThresholdRuleDataNamespace]: { - properties: { - serialized_params: { - type: 'keyword', - index: false, - }, - }, - }, - }, - }, + mappings: {}, }, }, }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/community_id.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/community_id.test.tsx new file mode 100644 index 0000000000000..0338cb8e04348 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/community_id.test.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setup, SetupResult, getProcessorValue } from './processor.helpers'; + +const COMMUNITY_ID_TYPE = 'community_id'; + +describe('Processor: Community id', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + + await act(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + testBed.component.update(); + + // Open flyout to add new processor + testBed.actions.addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await testBed.actions.addProcessorType(COMMUNITY_ID_TYPE); + }); + + test('can submit if no fields are filled', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Click submit button with no fields filled + await saveNewProcessor(); + + // Expect no form errors + expect(form.getErrorsMessages()).toHaveLength(0); + }); + + test('allows to set either iana_number or transport', async () => { + const { find, form } = testBed; + + expect(find('ianaField.input').exists()).toBe(true); + expect(find('transportField.input').exists()).toBe(true); + + form.setInputValue('ianaField.input', 'iana_number'); + expect(find('transportField.input').props().disabled).toBe(true); + + form.setInputValue('ianaField.input', ''); + form.setInputValue('transportField.input', 'transport'); + expect(find('ianaField.input').props().disabled).toBe(true); + }); + + test('allows optional parameters to be set', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + form.toggleEuiSwitch('ignoreMissingSwitch.input'); + form.toggleEuiSwitch('ignoreFailureSwitch.input'); + form.setInputValue('sourceIpField.input', 'source.ip'); + form.setInputValue('sourcePortField.input', 'source.port'); + form.setInputValue('targetField.input', 'target_field'); + form.setInputValue('destinationIpField.input', 'destination.ip'); + form.setInputValue('destinationPortField.input', 'destination.port'); + form.setInputValue('icmpTypeField.input', 'icmp_type'); + form.setInputValue('icmpCodeField.input', 'icmp_code'); + form.setInputValue('ianaField.input', 'iana'); + form.setInputValue('seedField.input', '10'); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, COMMUNITY_ID_TYPE); + expect(processors[0][COMMUNITY_ID_TYPE]).toEqual({ + ignore_failure: true, + ignore_missing: false, + target_field: 'target_field', + source_ip: 'source.ip', + source_port: 'source.port', + destination_ip: 'destination.ip', + destination_port: 'destination.port', + icmp_type: 'icmp_type', + icmp_code: 'icmp_code', + iana_number: 'iana', + seed: 10, + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx index e4024e4ec67f4..183777ca765b4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx @@ -180,4 +180,13 @@ type TestSubject = | 'fieldsValueField.input' | 'saltValueField.input' | 'methodsValueField' + | 'sourceIpField.input' + | 'sourcePortField.input' + | 'destinationIpField.input' + | 'destinationPortField.input' + | 'icmpTypeField.input' + | 'icmpCodeField.input' + | 'ianaField.input' + | 'transportField.input' + | 'seedField.input' | 'trimSwitch.input'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/community_id.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/community_id.tsx new file mode 100644 index 0000000000000..cd6f97d0a299e --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/community_id.tsx @@ -0,0 +1,307 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiCode, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { FieldsConfig, from } from './shared'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { + Field, + UseField, + useFormData, + FIELD_TYPES, + NumericField, + SerializerFunc, + fieldFormatters, + fieldValidators, +} from '../../../../../../shared_imports'; + +const SEED_MIN_VALUE = 0; +const SEED_MAX_VALUE = 65535; + +const seedValidator = { + max: fieldValidators.numberSmallerThanField({ + than: SEED_MAX_VALUE, + allowEquality: true, + message: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.seedMaxNumberError', { + defaultMessage: `This number must be equal or less than {maxValue}.`, + values: { maxValue: SEED_MAX_VALUE }, + }), + }), + min: fieldValidators.numberGreaterThanField({ + than: SEED_MIN_VALUE, + allowEquality: true, + message: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.seedMinNumberError', { + defaultMessage: `This number must be equal or greater than {minValue}.`, + values: { minValue: SEED_MIN_VALUE }, + }), + }), +}; + +const fieldsConfig: FieldsConfig = { + source_ip: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.sourceIpLabel', { + defaultMessage: 'Source IP (optional)', + }), + helpText: ( + {'source.ip'} }} + /> + ), + }, + source_port: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.sourcePortLabel', { + defaultMessage: 'Source port (optional)', + }), + helpText: ( + {'source.port'} }} + /> + ), + }, + destination_ip: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.destinationIpLabel', { + defaultMessage: 'Destination IP (optional)', + }), + helpText: ( + {'destination.ip'} }} + /> + ), + }, + destination_port: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.destinationPortLabel', { + defaultMessage: 'Destination port (optional)', + }), + helpText: ( + {'destination.port'} }} + /> + ), + }, + icmp_type: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.icmpTypeLabel', { + defaultMessage: 'ICMP type (optional)', + }), + helpText: ( + {'icmp.type'} }} + /> + ), + }, + icmp_code: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.icmpCodeLabel', { + defaultMessage: 'ICMP code (optional)', + }), + helpText: ( + {'icmp.code'} }} + /> + ), + }, + iana_number: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.ianaLabel', { + defaultMessage: 'IANA number (optional)', + }), + helpText: ( + {'Transport'}, + defaultValue: {'network.iana_number'}, + }} + /> + ), + }, + transport: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.transportLabel', { + defaultMessage: 'Transport (optional)', + }), + helpText: ( + {'IANA number'}, + defaultValue: {'network.transport'}, + }} + /> + ), + }, + seed: { + type: FIELD_TYPES.NUMBER, + formatters: [fieldFormatters.toInt], + serializer: from.undefinedIfValue(''), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.seedLabel', { + defaultMessage: 'Seed (optional)', + }), + helpText: ( + {'0'} }} + /> + ), + validations: [ + { + validator: (field) => { + if (field.value) { + return seedValidator.max(field) ?? seedValidator.min(field); + } + }, + }, + ], + }, +}; + +export const CommunityId: FunctionComponent = () => { + const [{ fields }] = useFormData({ watch: ['fields.iana_number', 'fields.transport'] }); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {'network.community_id'}, + }} + /> + } + /> + + } + /> + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts index f5eb1ab3ec59b..1a2422b40d0b0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts @@ -8,6 +8,7 @@ export { Append } from './append'; export { Bytes } from './bytes'; export { Circle } from './circle'; +export { CommunityId } from './community_id'; export { Convert } from './convert'; export { CSV } from './csv'; export { DateProcessor } from './date'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx index e6ca465bf1a02..2a7067be512ae 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx @@ -14,6 +14,7 @@ import { Append, Bytes, Circle, + CommunityId, Convert, CSV, DateProcessor, @@ -126,6 +127,20 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { }, }), }, + community_id: { + FieldsComponent: CommunityId, + docLinkPath: '/community-id-processor.html', + label: i18n.translate('xpack.ingestPipelines.processors.label.communityId', { + defaultMessage: 'Community ID', + }), + typeDescription: i18n.translate('xpack.ingestPipelines.processors.description.communityId', { + defaultMessage: 'Computes the Community ID for network flow data.', + }), + getDefaultDescription: () => + i18n.translate('xpack.ingestPipelines.processors.defaultDescription.communityId', { + defaultMessage: 'Computes the Community ID for network flow data.', + }), + }, convert: { FieldsComponent: Convert, docLinkPath: '/convert-processor.html', diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index 4a34bd030429e..ae51f7d42312f 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -11,6 +11,7 @@ import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test/jest'; import { EuiDataGrid } from '@elastic/eui'; import { IAggType, IFieldFormat } from 'src/plugins/data/public'; +import { VisualizationContainer } from '../../visualization_container'; import { EmptyPlaceholder } from '../../shared_components'; import { LensIconChartDatatable } from '../../assets/chart_datatable'; import { DataContext, DatatableComponent } from './table_basic'; @@ -357,6 +358,7 @@ describe('DatatableComponent', () => { uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> ); + expect(component.find(VisualizationContainer)).toHaveLength(1); expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDatatable); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx index 25a7d3a35050c..8ef64e4acdccc 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx @@ -329,7 +329,15 @@ export const DatatableComponent = (props: DatatableRenderProps) => { }, [columnConfig.columns, alignments, firstTable, columns]); if (isEmpty) { - return ; + return ( + + + + ); } const dataGridAriaLabel = diff --git a/x-pack/plugins/lens/public/editor_frame_service/error_helper.test.ts b/x-pack/plugins/lens/public/editor_frame_service/error_helper.test.ts index 41974149b9c03..7fae35057421c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/error_helper.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/error_helper.test.ts @@ -141,6 +141,17 @@ const scriptedFieldError = { }, }; +const networkError = { + stack: 'Error: Batch request failed with status 0', + message: '[lens_merge_tables] > [esaggs] > Batch request failed with status 0', + name: 'Error', + original: { + name: 'Error', + message: 'Batch request failed with status 0', + stack: 'Error: Batch request failed with status 0', + }, +}; + // EsAggs will report an internal error when user attempts to use a runtime field on an indexpattern he has no access to const indexpatternAccessError = { stack: "TypeError: Cannot read property 'values' of undefined\n", @@ -175,5 +186,11 @@ describe('lens_error_helpers', () => { indexpatternAccessError.message, ]); }); + + it("should report a network custom message when there's a network/connection problem", () => { + expect(getOriginalRequestErrorMessages(networkError as Error)).toEqual([ + 'Network error, try again later or contact your administrator.', + ]); + }); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts b/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts index 42470e5cb6162..b19a295b68407 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts @@ -28,6 +28,10 @@ interface EsAggError { stack: string; } +const isNetworkError = (e: Error): boolean => { + return e.message === 'Batch request failed with status 0'; // Note: 0 here means Network error +}; + const isRequestError = (e: Error | RequestError): e is RequestError => { if ('body' in e) { return e.body?.attributes?.error?.caused_by !== undefined; @@ -101,7 +105,15 @@ export function getOriginalRequestErrorMessages(error?: ExpressionRenderError | const errorMessages = []; if (error && 'original' in error && error.original) { if (isEsAggError(error.original)) { - errorMessages.push(error.message); + if (isNetworkError(error.original)) { + errorMessages.push( + i18n.translate('xpack.lens.editorFrame.networkErrorMessage', { + defaultMessage: 'Network error, try again later or contact your administrator.', + }) + ); + } else { + errorMessages.push(error.message); + } } else { const rootErrors = uniqWith(getErrorSources(error.original), isEqual); for (const rootError of rootErrors) { diff --git a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx index 865ebb417a4ca..3e8e9d184ed8a 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx @@ -314,8 +314,6 @@ export const HeatmapComponent: FC = ({ return ; } - // const colorPalette = euiPaletteForTemperature(5); - return ( { setState({ isReady: true }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index a0a6b30e541a7..ec1421577dc2b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -23,9 +23,10 @@ import { EuiButtonIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { EsQueryConfig, Query, Filter } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart } from 'kibana/public'; -import { DataPublicPluginStart, EsQueryConfig, Query, Filter } from 'src/plugins/data/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { htmlIdGenerator } from '@elastic/eui'; import { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx index b31125a1912ef..27a7659b1c817 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx @@ -249,10 +249,16 @@ describe('metric_expression', () => { /> ) ).toMatchInlineSnapshot(` - - `); + + + + `); }); test('it renders an EmptyPlaceholder when null value is passed as data', () => { @@ -269,9 +275,15 @@ describe('metric_expression', () => { /> ) ).toMatchInlineSnapshot(` - + + + `); }); diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.tsx index e21fa08b97410..cf6921b2ca579 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.tsx @@ -113,32 +113,32 @@ export function MetricChart({ }: MetricChartProps & { formatFactory: FormatFactory }) { const { metricTitle, title, description, accessor, mode } = args; const firstTable = Object.values(data.tables)[0]; - if (!accessor) { - return ( - - ); - } - if (!firstTable) { - return ; + const getEmptyState = () => ( + + + + ); + + if (!accessor || !firstTable) { + return getEmptyState(); } const column = firstTable.columns.find(({ id }) => id === accessor); const row = firstTable.rows[0]; if (!column || !row) { - return ; + return getEmptyState(); } // NOTE: Cardinality and Sum never receives "null" as value, but always 0, even for empty dataset. // Mind falsy values here as 0! const shouldShowResults = row[accessor] != null; - if (!shouldShowResults) { - return ; + return getEmptyState(); } const value = diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index a9e7e4adb9ca7..a3a10b803fcd3 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -19,6 +19,7 @@ import { shallow } from 'enzyme'; import { LensMultiTable } from '../types'; import { PieComponent } from './render_function'; import { PieExpressionArgs } from './types'; +import { VisualizationContainer } from '../visualization_container'; import { EmptyPlaceholder } from '../shared_components'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { LensIconChartDonut } from '../assets/chart_donut'; @@ -311,6 +312,7 @@ describe('PieVisualization component', () => { const component = shallow( ); + expect(component.find(VisualizationContainer)).toHaveLength(1); expect(component.find(EmptyPlaceholder)).toHaveLength(1); }); @@ -331,6 +333,7 @@ describe('PieVisualization component', () => { ); + expect(component.find(VisualizationContainer)).toHaveLength(1); expect(component.find(EmptyPlaceholder)).toHaveLength(0); expect(component.find(Chart)).toHaveLength(1); }); @@ -353,6 +356,7 @@ describe('PieVisualization component', () => { const component = shallow( ); + expect(component.find(VisualizationContainer)).toHaveLength(1); expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDonut); }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 2e5a06b4f705f..b161a81a835f1 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -233,7 +233,15 @@ export function PieComponent( isMetricEmpty; if (isEmpty) { - return ; + return ( + + ; + + ); } if (hasNegative) { 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 9a996f8f1ac46..feee231f232b0 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 @@ -611,114 +611,115 @@ describe('build_exceptions_filter', () => { getEntryExistsExcludedMock(), ], }); - - expect(exceptionItemFilter).toEqual({ - bool: { - filter: [ - { - nested: { - path: 'parent.field', - query: { - bool: { - filter: [ - { - bool: { - minimum_should_match: 1, - should: [ - { - match_phrase: { - 'parent.field.host.name': 'some host name', + expect(exceptionItemFilter).toEqual([ + { + bool: { + filter: [ + { + nested: { + path: 'parent.field', + query: { + bool: { + filter: [ + { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'parent.field.host.name': 'some host name', + }, }, - }, - ], + ], + }, }, - }, - { - bool: { - must_not: { - bool: { - minimum_should_match: 1, - should: [ - { - bool: { - minimum_should_match: 1, - should: [ - { - match_phrase: { - 'parent.field.host.name': 'some host name', + { + bool: { + must_not: { + bool: { + minimum_should_match: 1, + should: [ + { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'parent.field.host.name': 'some host name', + }, }, - }, - ], + ], + }, }, - }, - { - bool: { - minimum_should_match: 1, - should: [ - { - match_phrase: { - 'parent.field.host.name': 'some other host name', + { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'parent.field.host.name': 'some other host name', + }, }, - }, - ], + ], + }, }, - }, - ], + ], + }, }, }, }, - }, - { - bool: { - minimum_should_match: 1, - should: [{ exists: { field: 'parent.field.host.name' } }], + { + bool: { + minimum_should_match: 1, + should: [{ exists: { field: 'parent.field.host.name' } }], + }, }, - }, - ], + ], + }, }, + score_mode: 'none', }, - score_mode: 'none', }, - }, - { - bool: { - minimum_should_match: 1, - should: [ - { - bool: { - minimum_should_match: 1, - should: [{ match_phrase: { 'host.name': 'some "host" name' } }], + { + bool: { + minimum_should_match: 1, + should: [ + { + bool: { + minimum_should_match: 1, + should: [{ match_phrase: { 'host.name': 'some "host" name' } }], + }, }, - }, - { + { + bool: { + minimum_should_match: 1, + should: [{ match_phrase: { 'host.name': 'some other host name' } }], + }, + }, + ], + }, + }, + { + bool: { + must_not: { bool: { minimum_should_match: 1, - should: [{ match_phrase: { 'host.name': 'some other host name' } }], + should: [{ match_phrase: { 'host.name': 'some host name' } }], }, }, - ], - }, - }, - { - bool: { - must_not: { - bool: { - minimum_should_match: 1, - should: [{ match_phrase: { 'host.name': 'some host name' } }], - }, }, }, - }, - { - bool: { - must_not: { - bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] }, + { + bool: { + must_not: { + bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] }, + }, }, }, - }, - ], + ], + }, }, - }); + ]); }); }); diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts index 22a176da222d6..d04080e8a56c0 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -39,7 +39,7 @@ export const getExceptionListItemSchemaMock = ( meta: META, name: NAME, namespace_type: NAMESPACE_TYPE, - os_types: ['linux'], + os_types: [], tags: ['user added string for a tag', 'malware'], tie_breaker_id: TIE_BREAKER, type: ITEM_TYPE, diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx deleted file mode 100644 index 416852b469a79..0000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.test.tsx +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; -import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; - -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; - -import { FieldComponent } from './field'; - -describe('FieldComponent', () => { - test('it renders disabled if "isDisabled" is true', () => { - const wrapper = mount( - - ); - - expect( - wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] input`).prop('disabled') - ).toBeTruthy(); - }); - - test('it renders loading if "isLoading" is true', () => { - const wrapper = mount( - - ); - wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] button`).at(0).simulate('click'); - expect( - wrapper - .find(`EuiComboBoxOptionsList[data-test-subj="fieldAutocompleteComboBox-optionsList"]`) - .prop('isLoading') - ).toBeTruthy(); - }); - - test('it allows user to clear values if "isClearable" is true', () => { - const wrapper = mount( - - ); - - expect( - wrapper - .find(`[data-test-subj="comboBoxInput"]`) - .hasClass('euiComboBox__inputWrap-isClearable') - ).toBeTruthy(); - }); - - test('it correctly displays selected field', () => { - const wrapper = mount( - - ); - - expect( - wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] EuiComboBoxPill`).at(0).text() - ).toEqual('machine.os.raw'); - }); - - test('it invokes "onChange" when option selected', () => { - const mockOnChange = jest.fn(); - const wrapper = mount( - - ); - - ((wrapper.find(EuiComboBox).props() as unknown) as { - onChange: (a: EuiComboBoxOptionOption[]) => void; - }).onChange([{ label: 'machine.os' }]); - - expect(mockOnChange).toHaveBeenCalledWith([ - { - aggregatable: true, - count: 0, - esTypes: ['text'], - name: 'machine.os', - readFromDocValues: false, - scripted: false, - searchable: true, - type: 'string', - }, - ]); - }); -}); diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts deleted file mode 100644 index 21764c6f459d8..0000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts +++ /dev/null @@ -1,388 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { - EXCEPTION_OPERATORS, - doesNotExistOperator, - existsOperator, - isNotOperator, - isOperator, -} from '@kbn/securitysolution-list-utils'; - -import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; - -import * as i18n from './translations'; -import { - checkEmptyValue, - filterFieldToList, - getGenericComboBoxProps, - getOperators, - paramIsValid, - typeMatch, -} from './helpers'; - -describe('helpers', () => { - // @ts-ignore - moment.suppressDeprecationWarnings = true; - describe('#getOperators', () => { - test('it returns "isOperator" if passed in field is "undefined"', () => { - const operator = getOperators(undefined); - - expect(operator).toEqual([isOperator]); - }); - - test('it returns expected operators when field type is "boolean"', () => { - const operator = getOperators(getField('ssl')); - - expect(operator).toEqual([isOperator, isNotOperator, existsOperator, doesNotExistOperator]); - }); - - test('it returns "isOperator" when field type is "nested"', () => { - const operator = getOperators({ - aggregatable: false, - count: 0, - esTypes: ['text'], - name: 'nestedField', - readFromDocValues: false, - scripted: false, - searchable: true, - subType: { nested: { path: 'nestedField' } }, - type: 'nested', - }); - - expect(operator).toEqual([isOperator]); - }); - - test('it returns all operator types when field type is not null, boolean, or nested', () => { - const operator = getOperators(getField('machine.os.raw')); - - expect(operator).toEqual(EXCEPTION_OPERATORS); - }); - }); - - describe('#checkEmptyValue', () => { - test('returns no errors if no field has been selected', () => { - const isValid = checkEmptyValue('', undefined, true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns error string if user has touched a required input and left empty', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, true); - - expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); - }); - - test('returns no errors if required input is empty but user has not yet touched it', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty string', () => { - const isValid = checkEmptyValue('', getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns null if input value is not empty string or undefined', () => { - const isValid = checkEmptyValue('hellooo', getField('@timestamp'), false, true); - - expect(isValid).toBeNull(); - }); - }); - - describe('#paramIsValid', () => { - test('returns no errors if no field has been selected', () => { - const isValid = paramIsValid('', undefined, true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns error string if user has touched a required input and left empty', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), true, true); - - expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); - }); - - test('returns no errors if required input is empty but user has not yet touched it', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty string', () => { - const isValid = paramIsValid('', getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type date and value is valid', () => { - const isValid = paramIsValid( - '1994-11-05T08:15:30-05:00', - getField('@timestamp'), - false, - true - ); - - expect(isValid).toBeUndefined(); - }); - - test('returns errors if filed is of type date and value is not valid', () => { - const isValid = paramIsValid('1593478826', getField('@timestamp'), false, true); - - expect(isValid).toEqual(i18n.DATE_ERR); - }); - - test('returns no errors if field is of type number and value is an integer', () => { - const isValid = paramIsValid('4', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type number and value is a float', () => { - const isValid = paramIsValid('4.3', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type number and value is a long', () => { - const isValid = paramIsValid('-9223372036854775808', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns errors if field is of type number and value is "hello"', () => { - const isValid = paramIsValid('hello', getField('bytes'), true, true); - - expect(isValid).toEqual(i18n.NUMBER_ERR); - }); - - test('returns errors if field is of type number and value is "123abc"', () => { - const isValid = paramIsValid('123abc', getField('bytes'), true, true); - - expect(isValid).toEqual(i18n.NUMBER_ERR); - }); - }); - - describe('#getGenericComboBoxProps', () => { - test('it returns empty arrays if "options" is empty array', () => { - const result = getGenericComboBoxProps({ - getLabel: (t: string) => t, - options: [], - selectedOptions: ['option1'], - }); - - expect(result).toEqual({ comboOptions: [], labels: [], selectedComboOptions: [] }); - }); - - test('it returns formatted props if "options" array is not empty', () => { - const result = getGenericComboBoxProps({ - getLabel: (t: string) => t, - options: ['option1', 'option2', 'option3'], - selectedOptions: [], - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [], - }); - }); - - test('it does not return "selectedOptions" items that do not appear in "options"', () => { - const result = getGenericComboBoxProps({ - getLabel: (t: string) => t, - options: ['option1', 'option2', 'option3'], - selectedOptions: ['option4'], - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [], - }); - }); - - test('it return "selectedOptions" items that do appear in "options"', () => { - const result = getGenericComboBoxProps({ - getLabel: (t: string) => t, - options: ['option1', 'option2', 'option3'], - selectedOptions: ['option2'], - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [ - { - label: 'option2', - }, - ], - }); - }); - }); - - describe('#typeMatch', () => { - test('ip -> ip is true', () => { - expect(typeMatch('ip', 'ip')).toEqual(true); - }); - - test('keyword -> keyword is true', () => { - expect(typeMatch('keyword', 'keyword')).toEqual(true); - }); - - test('text -> text is true', () => { - expect(typeMatch('text', 'text')).toEqual(true); - }); - - test('ip_range -> ip is true', () => { - expect(typeMatch('ip_range', 'ip')).toEqual(true); - }); - - test('date_range -> date is true', () => { - expect(typeMatch('date_range', 'date')).toEqual(true); - }); - - test('double_range -> double is true', () => { - expect(typeMatch('double_range', 'double')).toEqual(true); - }); - - test('float_range -> float is true', () => { - expect(typeMatch('float_range', 'float')).toEqual(true); - }); - - test('integer_range -> integer is true', () => { - expect(typeMatch('integer_range', 'integer')).toEqual(true); - }); - - test('long_range -> long is true', () => { - expect(typeMatch('long_range', 'long')).toEqual(true); - }); - - test('ip -> date is false', () => { - expect(typeMatch('ip', 'date')).toEqual(false); - }); - - test('long -> float is false', () => { - expect(typeMatch('long', 'float')).toEqual(false); - }); - - test('integer -> long is false', () => { - expect(typeMatch('integer', 'long')).toEqual(false); - }); - }); - - describe('#filterFieldToList', () => { - test('it returns empty array if given a undefined for field', () => { - const filter = filterFieldToList([], undefined); - expect(filter).toEqual([]); - }); - - test('it returns empty array if filed does not contain esTypes', () => { - const field: IFieldType = { name: 'some-name', type: 'some-type' }; - const filter = filterFieldToList([], field); - expect(filter).toEqual([]); - }); - - test('it returns single filtered list of ip_range -> ip', () => { - const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; - const listItem: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; - const filter = filterFieldToList([listItem], field); - const expected: ListSchema[] = [listItem]; - expect(filter).toEqual(expected); - }); - - test('it returns single filtered list of ip -> ip', () => { - const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; - const listItem: ListSchema = { ...getListResponseMock(), type: 'ip' }; - const filter = filterFieldToList([listItem], field); - const expected: ListSchema[] = [listItem]; - expect(filter).toEqual(expected); - }); - - test('it returns single filtered list of keyword -> keyword', () => { - const field: IFieldType = { esTypes: ['keyword'], name: 'some-name', type: 'keyword' }; - const listItem: ListSchema = { ...getListResponseMock(), type: 'keyword' }; - const filter = filterFieldToList([listItem], field); - const expected: ListSchema[] = [listItem]; - expect(filter).toEqual(expected); - }); - - test('it returns single filtered list of text -> text', () => { - const field: IFieldType = { esTypes: ['text'], name: 'some-name', type: 'text' }; - const listItem: ListSchema = { ...getListResponseMock(), type: 'text' }; - const filter = filterFieldToList([listItem], field); - const expected: ListSchema[] = [listItem]; - expect(filter).toEqual(expected); - }); - - test('it returns 2 filtered lists of ip_range -> ip', () => { - const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; - const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; - const listItem2: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; - const filter = filterFieldToList([listItem1, listItem2], field); - const expected: ListSchema[] = [listItem1, listItem2]; - expect(filter).toEqual(expected); - }); - - test('it returns 1 filtered lists of ip_range -> ip if the 2nd is not compatible type', () => { - const field: IFieldType = { esTypes: ['ip'], name: 'some-name', type: 'ip' }; - const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; - const listItem2: ListSchema = { ...getListResponseMock(), type: 'text' }; - const filter = filterFieldToList([listItem1, listItem2], field); - const expected: ListSchema[] = [listItem1]; - expect(filter).toEqual(expected); - }); - }); -}); diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts deleted file mode 100644 index 975416e272227..0000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts +++ /dev/null @@ -1,183 +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 dateMath from '@elastic/datemath'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; -import type { ListSchema, Type } from '@kbn/securitysolution-io-ts-list-types'; -import { - EXCEPTION_OPERATORS, - OperatorOption, - doesNotExistOperator, - existsOperator, - isNotOperator, - isOperator, -} from '@kbn/securitysolution-list-utils'; - -import { IFieldType } from '../../../../../../../src/plugins/data/common'; - -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; - -/** - * Returns the appropriate operators given a field type - * - * @param field IFieldType selected field - * - */ -export const getOperators = (field: IFieldType | undefined): OperatorOption[] => { - if (field == null) { - return [isOperator]; - } else if (field.type === 'boolean') { - return [isOperator, isNotOperator, existsOperator, doesNotExistOperator]; - } else if (field.type === 'nested') { - return [isOperator]; - } else { - return EXCEPTION_OPERATORS; - } -}; - -/** - * Determines if empty value is ok - * - * @param param the value being checked - * @param field the selected field - * @param isRequired whether or not an empty value is allowed - * @param touched has field been touched by user - * @returns undefined if valid, string with error message if invalid, - * null if no checks matched - */ -export const checkEmptyValue = ( - param: string | undefined, - field: IFieldType | undefined, - isRequired: boolean, - touched: boolean -): string | undefined | null => { - if (isRequired && touched && (param == null || param.trim() === '')) { - return i18n.FIELD_REQUIRED_ERR; - } - - if ( - field == null || - (isRequired && !touched) || - (!isRequired && (param == null || param === '')) - ) { - return undefined; - } - - return null; -}; - -/** - * Very basic validation for values - * There is a copy within: - * x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * - * @param param the value being checked - * @param field the selected field - * @param isRequired whether or not an empty value is allowed - * @param touched has field been touched by user - * @returns undefined if valid, string with error message if invalid - */ -export const paramIsValid = ( - param: string | undefined, - field: IFieldType | undefined, - isRequired: boolean, - touched: boolean -): string | undefined => { - if (field == null) { - return undefined; - } - - const emptyValueError = checkEmptyValue(param, field, isRequired, touched); - if (emptyValueError !== null) { - return emptyValueError; - } - - switch (field.type) { - case 'date': - const moment = dateMath.parse(param ?? ''); - const isDate = Boolean(moment && moment.isValid()); - return isDate ? undefined : i18n.DATE_ERR; - case 'number': - const isNum = param != null && param.trim() !== '' && !isNaN(+param); - return isNum ? undefined : i18n.NUMBER_ERR; - default: - return undefined; - } -}; - -/** - * Determines the options, selected values and option labels for EUI combo box - * There is a copy within: - * x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * @param options options user can select from - * @param selectedOptions user selection if any - * @param getLabel helper function to know which property to use for labels - */ -export const getGenericComboBoxProps = ({ - getLabel, - options, - selectedOptions, -}: { - getLabel: (value: T) => string; - options: T[]; - selectedOptions: T[]; -}): GetGenericComboBoxPropsReturn => { - const newLabels = options.map(getLabel); - const newComboOptions: EuiComboBoxOptionOption[] = newLabels.map((label) => ({ label })); - const newSelectedComboOptions = selectedOptions - .map(getLabel) - .filter((option) => { - return newLabels.indexOf(option) !== -1; - }) - .map((option) => { - return newComboOptions[newLabels.indexOf(option)]; - }); - - return { - comboOptions: newComboOptions, - labels: newLabels, - selectedComboOptions: newSelectedComboOptions, - }; -}; - -/** - * Given an array of lists and optionally a field this will return all - * the lists that match against the field based on the types from the field - * @param lists The lists to match against the field - * @param field The field to check against the list to see if they are compatible - */ -export const filterFieldToList = (lists: ListSchema[], field?: IFieldType): ListSchema[] => { - if (field != null) { - const { esTypes = [] } = field; - return lists.filter(({ type }) => esTypes.some((esType) => typeMatch(type, esType))); - } else { - return []; - } -}; - -/** - * Given an input list type and a string based ES type this will match - * if they're exact or if they are compatible with a range - * @param type The type to match against the esType - * @param esType The ES type to match with - */ -export const typeMatch = (type: Type, esType: string): boolean => { - return ( - type === esType || - (type === 'ip_range' && esType === 'ip') || - (type === 'date_range' && esType === 'date') || - (type === 'double_range' && esType === 'double') || - (type === 'float_range' && esType === 'float') || - (type === 'integer_range' && esType === 'integer') || - (type === 'long_range' && esType === 'long') - ); -}; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx deleted file mode 100644 index 1623683f25ed5..0000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/index.tsx +++ /dev/null @@ -1,13 +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 { AutocompleteFieldExistsComponent } from './field_value_exists'; -export { AutocompleteFieldListsComponent } from './field_value_lists'; -export { AutocompleteFieldMatchAnyComponent } from './field_value_match_any'; -export { AutocompleteFieldMatchComponent } from './field_value_match'; -export { FieldComponent } from './field'; -export { OperatorComponent } from './operator'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/translations.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/translations.ts deleted file mode 100644 index 065239246d329..0000000000000 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/translations.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const LOADING = i18n.translate('xpack.lists.autocomplete.loadingDescription', { - defaultMessage: 'Loading...', -}); - -export const SELECT_FIELD_FIRST = i18n.translate('xpack.lists.autocomplete.selectField', { - defaultMessage: 'Please select a field first...', -}); - -export const FIELD_REQUIRED_ERR = i18n.translate('xpack.lists.autocomplete.fieldRequiredError', { - defaultMessage: 'Value cannot be empty', -}); - -export const NUMBER_ERR = i18n.translate('xpack.lists.autocomplete.invalidNumberError', { - defaultMessage: 'Not a valid number', -}); - -export const DATE_ERR = i18n.translate('xpack.lists.autocomplete.invalidDateError', { - defaultMessage: 'Not a valid date', -}); 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 c54da89766d76..d7741b3fe0ff1 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 @@ -27,16 +27,18 @@ import { getFilteredIndexPatterns, getOperatorOptions, } from '@kbn/securitysolution-list-utils'; +import { + AutocompleteFieldExistsComponent, + AutocompleteFieldListsComponent, + AutocompleteFieldMatchAnyComponent, + AutocompleteFieldMatchComponent, + FieldComponent, + OperatorComponent, +} from '@kbn/securitysolution-autocomplete'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { HttpStart } from '../../../../../../../src/core/public'; -import { FieldComponent } from '../autocomplete/field'; -import { OperatorComponent } from '../autocomplete/operator'; -import { AutocompleteFieldExistsComponent } from '../autocomplete/field_value_exists'; -import { AutocompleteFieldMatchComponent } from '../autocomplete/field_value_match'; -import { AutocompleteFieldMatchAnyComponent } from '../autocomplete/field_value_match_any'; -import { AutocompleteFieldListsComponent } from '../autocomplete/field_value_lists'; import { getEmptyValue } from '../../../common/empty_value'; import * as i18n from './translations'; diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter.ts index 25c8f9880063f..ff0e42870ab30 100644 --- a/x-pack/plugins/lists/server/services/utils/get_query_filter.ts +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DslQuery, EsQueryConfig } from 'src/plugins/data/common'; +import { DslQuery, EsQueryConfig } from '@kbn/es-query'; import { Filter, Query, esQuery } from '../../../../../../src/plugins/data/server'; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts index 9a2b2c21136df..0e2c38bf7afe4 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { Feature, Geometry, Polygon, Position } from 'geojson'; // @ts-expect-error import turfCircle from '@turf/circle'; -import { FilterMeta, FILTERS } from '../../../../../src/plugins/data/common'; +import { FilterMeta, FILTERS } from '@kbn/es-query'; import { MapExtent } from '../descriptor_types'; import { ES_SPATIAL_RELATIONS } from '../constants'; import { getEsSpatialRelationLabel } from '../i18n_getters'; diff --git a/x-pack/plugins/ml/common/constants/alerts.ts b/x-pack/plugins/ml/common/constants/alerts.ts index 30daf0d45c3ac..604d5ea8c4fc9 100644 --- a/x-pack/plugins/ml/common/constants/alerts.ts +++ b/x-pack/plugins/ml/common/constants/alerts.ts @@ -6,46 +6,22 @@ */ import { i18n } from '@kbn/i18n'; -import { ActionGroup } from '../../../alerting/common'; -import { MINIMUM_FULL_LICENSE } from '../license'; -import { PLUGIN_ID } from './app'; export const ML_ALERT_TYPES = { ANOMALY_DETECTION: 'xpack.ml.anomaly_detection_alert', + AD_JOBS_HEALTH: 'xpack.ml.anomaly_detection_jobs_health', } as const; export type MlAlertType = typeof ML_ALERT_TYPES[keyof typeof ML_ALERT_TYPES]; -export const ANOMALY_SCORE_MATCH_GROUP_ID = 'anomaly_score_match'; -export type AnomalyScoreMatchGroupId = typeof ANOMALY_SCORE_MATCH_GROUP_ID; -export const THRESHOLD_MET_GROUP: ActionGroup = { - id: ANOMALY_SCORE_MATCH_GROUP_ID, - name: i18n.translate('xpack.ml.anomalyDetectionAlert.actionGroupName', { - defaultMessage: 'Anomaly score matched the condition', - }), -}; - -export const ML_ALERT_TYPES_CONFIG: Record< - MlAlertType, - { - name: string; - actionGroups: Array>; - defaultActionGroupId: AnomalyScoreMatchGroupId; - minimumLicenseRequired: string; - producer: string; - } -> = { - [ML_ALERT_TYPES.ANOMALY_DETECTION]: { - name: i18n.translate('xpack.ml.anomalyDetectionAlert.name', { - defaultMessage: 'Anomaly detection alert', - }), - actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: ANOMALY_SCORE_MATCH_GROUP_ID, - minimumLicenseRequired: MINIMUM_FULL_LICENSE, - producer: PLUGIN_ID, - }, -}; - export const ALERT_PREVIEW_SAMPLE_SIZE = 5; export const TOP_N_BUCKETS_COUNT = 1; + +export const ALL_JOBS_SELECTION = '*'; + +export const HEALTH_CHECK_NAMES = { + datafeed: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedCheckName', { + defaultMessage: 'Datafeed is not started', + }), +}; diff --git a/x-pack/plugins/ml/common/types/alerts.ts b/x-pack/plugins/ml/common/types/alerts.ts index 1677a766544a1..877bb2d293365 100644 --- a/x-pack/plugins/ml/common/types/alerts.ts +++ b/x-pack/plugins/ml/common/types/alerts.ts @@ -108,3 +108,38 @@ export type MlAnomalyDetectionAlertRule = Omit; diff --git a/x-pack/plugins/ml/common/types/es_client.ts b/x-pack/plugins/ml/common/types/es_client.ts index 29a7a81aa5693..433deac02bc9c 100644 --- a/x-pack/plugins/ml/common/types/es_client.ts +++ b/x-pack/plugins/ml/common/types/es_client.ts @@ -8,8 +8,7 @@ import { estypes } from '@elastic/elasticsearch'; import { JsonObject } from '@kbn/common-utils'; -import { buildEsQuery } from '../../../../../src/plugins/data/common/es_query/es_query'; -import type { DslQuery } from '../../../../../src/plugins/data/common/es_query/kuery'; +import { buildEsQuery, DslQuery } from '@kbn/es-query'; import { isPopulatedObject } from '../util/object_utils'; diff --git a/x-pack/plugins/ml/common/types/job_service.ts b/x-pack/plugins/ml/common/types/job_service.ts index 44d9d8df5b1ab..e846635ee5380 100644 --- a/x-pack/plugins/ml/common/types/job_service.ts +++ b/x-pack/plugins/ml/common/types/job_service.ts @@ -8,6 +8,7 @@ import { Job, JobStats, IndicesOptions } from './anomaly_detection_jobs'; import { RuntimeMappings } from './fields'; import { ES_AGGREGATION } from '../constants/aggregation_types'; +import { ErrorType } from '../util/errors'; export interface MlJobsResponse { jobs: Job[]; @@ -40,3 +41,10 @@ export interface BucketSpanEstimatorData { runtimeMappings?: RuntimeMappings; indicesOptions?: IndicesOptions; } + +export interface BulkCreateResults { + [id: string]: { + job: { success: boolean; error?: ErrorType }; + datafeed: { success: boolean; error?: ErrorType }; + }; +} diff --git a/x-pack/plugins/ml/common/util/alerts.test.ts b/x-pack/plugins/ml/common/util/alerts.test.ts index d9896c967165b..430e10cc8ffa8 100644 --- a/x-pack/plugins/ml/common/util/alerts.test.ts +++ b/x-pack/plugins/ml/common/util/alerts.test.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { getLookbackInterval, resolveLookbackInterval } from './alerts'; +import { + getLookbackInterval, + getResultJobsHealthRuleConfig, + resolveLookbackInterval, +} from './alerts'; import type { CombinedJobWithStats, Datafeed, Job } from '../types/anomaly_detection_jobs'; describe('resolveLookbackInterval', () => { @@ -76,3 +80,49 @@ describe('getLookbackInterval', () => { expect(getLookbackInterval(testJobs)).toBe('32m'); }); }); + +describe('getResultJobsHealthRuleConfig', () => { + test('returns default config for empty configuration', () => { + expect(getResultJobsHealthRuleConfig(null)).toEqual({ + datafeed: { + enabled: true, + }, + mml: { + enabled: true, + }, + delayedData: { + enabled: true, + }, + behindRealtime: { + enabled: true, + }, + errorMessages: { + enabled: true, + }, + }); + }); + test('returns config with overridden values based on provided configuration', () => { + expect( + getResultJobsHealthRuleConfig({ + mml: { enabled: false }, + errorMessages: { enabled: true }, + }) + ).toEqual({ + datafeed: { + enabled: true, + }, + mml: { + enabled: false, + }, + delayedData: { + enabled: true, + }, + behindRealtime: { + enabled: true, + }, + errorMessages: { + enabled: true, + }, + }); + }); +}); diff --git a/x-pack/plugins/ml/common/util/alerts.ts b/x-pack/plugins/ml/common/util/alerts.ts index 5d68677d4fb97..b211423e65062 100644 --- a/x-pack/plugins/ml/common/util/alerts.ts +++ b/x-pack/plugins/ml/common/util/alerts.ts @@ -9,6 +9,7 @@ import { CombinedJobWithStats, Datafeed, Job } from '../types/anomaly_detection_ import { resolveMaxTimeInterval } from './job_utils'; import { isDefined } from '../types/guards'; import { parseInterval } from './parse_interval'; +import { JobsHealthRuleTestsConfig } from '../types/alerts'; const narrowBucketLength = 60; @@ -51,3 +52,27 @@ export function getTopNBuckets(job: Job): number { return Math.ceil(narrowBucketLength / bucketSpan.asSeconds()); } + +/** + * Returns tests configuration combined with default values. + * @param config + */ +export function getResultJobsHealthRuleConfig(config: JobsHealthRuleTestsConfig) { + return { + datafeed: { + enabled: config?.datafeed?.enabled ?? true, + }, + mml: { + enabled: config?.mml?.enabled ?? true, + }, + delayedData: { + enabled: config?.delayedData?.enabled ?? true, + }, + behindRealtime: { + enabled: config?.behindRealtime?.enabled ?? true, + }, + errorMessages: { + enabled: config?.errorMessages?.enabled ?? true, + }, + }; +} diff --git a/x-pack/plugins/ml/public/alerting/job_selector.tsx b/x-pack/plugins/ml/public/alerting/job_selector.tsx index d00d4efc25b8d..0ef7bba0ddbc5 100644 --- a/x-pack/plugins/ml/public/alerting/job_selector.tsx +++ b/x-pack/plugins/ml/public/alerting/job_selector.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { FC, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiComboBox, EuiComboBoxOptionOption, EuiComboBoxProps, EuiFormRow } from '@elastic/eui'; import { JobId } from '../../common/types/anomaly_detection_jobs'; import { MlApiServices } from '../application/services/ml_api_service'; +import { ALL_JOBS_SELECTION } from '../../common/constants/alerts'; interface JobSelection { jobIds?: JobId[]; @@ -25,6 +26,17 @@ export interface JobSelectorControlProps { * Validation is handled by alerting framework */ errors: string[]; + /** Enables multiple selection of jobs and groups */ + multiSelect?: boolean; + label?: ReactNode; + /** + * Allows selecting all jobs, even those created afterward. + */ + allowSelectAll?: boolean; + /** + * Available options to select. By default suggest all existing jobs. + */ + options?: Array>; } export const JobSelectorControl: FC = ({ @@ -32,6 +44,10 @@ export const JobSelectorControl: FC = ({ onChange, adJobsApiService, errors, + multiSelect = false, + label, + allowSelectAll = false, + options: defaultOptions, }) => { const [options, setOptions] = useState>>([]); const jobIds = useMemo(() => new Set(), []); @@ -60,12 +76,39 @@ export const JobSelectorControl: FC = ({ }); setOptions([ + ...(allowSelectAll + ? [ + { + label: i18n.translate('xpack.ml.jobSelector.selectAllGroupLabel', { + defaultMessage: 'Select all', + }), + options: [ + { + label: i18n.translate('xpack.ml.jobSelector.selectAllOptionLabel', { + defaultMessage: '*', + }), + value: ALL_JOBS_SELECTION, + }, + ], + }, + ] + : []), { label: i18n.translate('xpack.ml.jobSelector.jobOptionsLabel', { defaultMessage: 'Jobs', }), options: jobIdOptions.map((v) => ({ label: v })), }, + ...(multiSelect + ? [ + { + label: i18n.translate('xpack.ml.jobSelector.groupOptionsLabel', { + defaultMessage: 'Groups', + }), + options: groupIdOptions.map((v) => ({ label: v })), + }, + ] + : []), ]); } catch (e) { // TODO add error handling @@ -73,25 +116,33 @@ export const JobSelectorControl: FC = ({ }, [adJobsApiService]); const onSelectionChange: EuiComboBoxProps['onChange'] = useCallback( - (selectionUpdate) => { + ((selectionUpdate) => { + if (selectionUpdate.some((selectedOption) => selectedOption.value === ALL_JOBS_SELECTION)) { + onChange({ jobIds: [ALL_JOBS_SELECTION] }); + return; + } + const selectedJobIds: JobId[] = []; const selectedGroupIds: string[] = []; - selectionUpdate.forEach(({ label }: { label: string }) => { - if (jobIds.has(label)) { - selectedJobIds.push(label); - } else if (groupIds.has(label)) { - selectedGroupIds.push(label); + selectionUpdate.forEach(({ label: selectedLabel }: { label: string }) => { + if (jobIds.has(selectedLabel)) { + selectedJobIds.push(selectedLabel); + } else if (groupIds.has(selectedLabel)) { + selectedGroupIds.push(selectedLabel); + } else if (defaultOptions?.some((v) => v.options?.some((o) => o.label === selectedLabel))) { + selectedJobIds.push(selectedLabel); } }); onChange({ ...(selectedJobIds.length > 0 ? { jobIds: selectedJobIds } : {}), ...(selectedGroupIds.length > 0 ? { groupIds: selectedGroupIds } : {}), }); - }, - [jobIds, groupIds] + }) as Exclude['onChange'], undefined>, + [jobIds, groupIds, defaultOptions] ); useEffect(() => { + if (defaultOptions) return; fetchOptions(); }, []); @@ -99,15 +150,20 @@ export const JobSelectorControl: FC = ({ + label ?? ( + + ) } isInvalid={!!errors?.length} error={errors} > - singleSelection + singleSelection={!multiSelect} selectedOptions={selectedOptions} - options={options} + options={defaultOptions ?? options} onChange={onSelectionChange} fullWidth data-test-subj={'mlAnomalyAlertJobSelection'} diff --git a/x-pack/plugins/ml/public/alerting/jobs_health_rule/anomaly_detection_jobs_health_rule_trigger.tsx b/x-pack/plugins/ml/public/alerting/jobs_health_rule/anomaly_detection_jobs_health_rule_trigger.tsx new file mode 100644 index 0000000000000..7c75817e4029f --- /dev/null +++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/anomaly_detection_jobs_health_rule_trigger.tsx @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useCallback, useMemo, useState } from 'react'; +import { EuiComboBoxOptionOption, EuiForm, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useDebounce from 'react-use/lib/useDebounce'; +import { AlertTypeParamsExpressionProps } from '../../../../triggers_actions_ui/public'; +import { MlAnomalyDetectionJobsHealthRuleParams } from '../../../common/types/alerts'; +import { JobSelectorControl } from '../job_selector'; +import { jobsApiProvider } from '../../application/services/ml_api_service/jobs'; +import { HttpService } from '../../application/services/http_service'; +import { useMlKibana } from '../../application/contexts/kibana'; +import { TestsSelectionControl } from './tests_selection_control'; +import { isPopulatedObject } from '../../../common'; +import { ALL_JOBS_SELECTION } from '../../../common/constants/alerts'; + +export type MlAnomalyAlertTriggerProps = AlertTypeParamsExpressionProps; + +const AnomalyDetectionJobsHealthRuleTrigger: FC = ({ + alertParams, + setAlertParams, + errors, +}) => { + const { + services: { http }, + } = useMlKibana(); + const mlHttpService = useMemo(() => new HttpService(http), [http]); + const adJobsApiService = useMemo(() => jobsApiProvider(mlHttpService), [mlHttpService]); + const [excludeJobsOptions, setExcludeJobsOptions] = useState< + Array> + >([]); + + const includeJobsAndGroupIds: string[] = useMemo( + () => (Object.values(alertParams.includeJobs ?? {}) as string[][]).flat(), + [alertParams.includeJobs] + ); + + const excludeJobsAndGroupIds: string[] = useMemo( + () => (Object.values(alertParams.excludeJobs ?? {}) as string[][]).flat(), + [alertParams.excludeJobs] + ); + + const onAlertParamChange = useCallback( + (param: T) => ( + update: MlAnomalyDetectionJobsHealthRuleParams[T] + ) => { + setAlertParams(param, update); + }, + [] + ); + + const formErrors = Object.values(errors).flat(); + const isFormInvalid = formErrors.length > 0; + + useDebounce( + function updateExcludeJobsOptions() { + const areAllJobsSelected = alertParams.includeJobs?.jobIds?.[0] === ALL_JOBS_SELECTION; + + if (!areAllJobsSelected && !alertParams.includeJobs?.groupIds?.length) { + // It only makes sense to suggest excluded jobs options when at least one group or all jobs are selected + setExcludeJobsOptions([]); + return; + } + + adJobsApiService + .jobs(areAllJobsSelected ? [] : (alertParams.includeJobs.groupIds as string[])) + .then((jobs) => { + setExcludeJobsOptions([ + { + label: i18n.translate('xpack.ml.jobSelector.jobOptionsLabel', { + defaultMessage: 'Jobs', + }), + options: jobs.map((v) => ({ label: v.job_id })), + }, + ]); + }); + }, + 500, + [alertParams.includeJobs] + ); + + return ( + + + } + /> + + + + { + const callback = onAlertParamChange('excludeJobs'); + if (isPopulatedObject(update)) { + callback(update); + } else { + callback(null); + } + }, [])} + errors={Array.isArray(errors.excludeJobs) ? errors.excludeJobs : []} + multiSelect + label={ + + } + options={excludeJobsOptions} + /> + + + + + + ); +}; + +// Default export is required for React.lazy loading + +// eslint-disable-next-line import/no-default-export +export default AnomalyDetectionJobsHealthRuleTrigger; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts b/x-pack/plugins/ml/public/alerting/jobs_health_rule/index.ts similarity index 68% rename from x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts rename to x-pack/plugins/ml/public/alerting/jobs_health_rule/index.ts index 91a1915c4c518..f26b38a1370ec 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts +++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/index.ts @@ -5,7 +5,4 @@ * 2.0. */ -export interface PostIngestSetupResponse { - isInitialized: boolean; - nonFatalErrors?: Array<{ error: Error }>; -} +export { registerJobsHealthAlertingRule } from './register_jobs_health_alerting_rule'; diff --git a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts new file mode 100644 index 0000000000000..ef20b51df2600 --- /dev/null +++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../../../triggers_actions_ui/public'; +import { PluginSetupContract as AlertingSetup } from '../../../../alerting/public'; +import { ML_ALERT_TYPES } from '../../../common/constants/alerts'; +import { MlAnomalyDetectionJobsHealthRuleParams } from '../../../common/types/alerts'; + +export function registerJobsHealthAlertingRule( + triggersActionsUi: TriggersAndActionsUIPublicPluginSetup, + alerting?: AlertingSetup +) { + triggersActionsUi.alertTypeRegistry.register({ + id: ML_ALERT_TYPES.AD_JOBS_HEALTH, + description: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.description', { + defaultMessage: 'Alert when anomaly detection jobs experience operational issues.', + }), + iconClass: 'bell', + documentationUrl(docLinks) { + return docLinks.links.ml.alertingRules; + }, + alertParamsExpression: lazy(() => import('./anomaly_detection_jobs_health_rule_trigger')), + validate: (alertParams: MlAnomalyDetectionJobsHealthRuleParams) => { + const validationResult = { + errors: { + includeJobs: new Array(), + testsConfig: new Array(), + } as Record, + }; + + if (!alertParams.includeJobs?.jobIds?.length && !alertParams.includeJobs?.groupIds?.length) { + validationResult.errors.includeJobs.push( + i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.includeJobs.errorMessage', { + defaultMessage: 'Job selection is required', + }) + ); + } + + if ( + alertParams.testsConfig && + Object.values(alertParams.testsConfig).every((v) => v?.enabled === false) + ) { + validationResult.errors.testsConfig.push( + i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.testsConfig.errorMessage', { + defaultMessage: 'At least one health check must be enabled.', + }) + ); + } + + return validationResult; + }, + requiresAppContext: false, + defaultActionMessage: i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.defaultActionMessage', + { + defaultMessage: `Anomaly detection jobs health check result: +\\{\\{context.message\\}\\} +- Job IDs: \\{\\{context.jobIds\\}\\} +`, + } + ), + }); +} diff --git a/x-pack/plugins/ml/public/alerting/jobs_health_rule/tests_selection_control.tsx b/x-pack/plugins/ml/public/alerting/jobs_health_rule/tests_selection_control.tsx new file mode 100644 index 0000000000000..8c033fe141222 --- /dev/null +++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/tests_selection_control.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormFieldset, EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { JobsHealthRuleTestsConfig } from '../../../common/types/alerts'; +import { getResultJobsHealthRuleConfig } from '../../../common/util/alerts'; +import { HEALTH_CHECK_NAMES } from '../../../common/constants/alerts'; + +interface TestsSelectionControlProps { + config: JobsHealthRuleTestsConfig; + onChange: (update: JobsHealthRuleTestsConfig) => void; + errors?: string[]; +} + +export const TestsSelectionControl: FC = ({ + config, + onChange, + errors, +}) => { + const uiConfig = getResultJobsHealthRuleConfig(config); + + const updateCallback = useCallback( + (update: Partial>) => { + onChange({ + ...(config ?? {}), + ...update, + }); + }, + [onChange, config] + ); + + return ( + + + + + + + + {false && ( + <> + + } + onChange={updateCallback.bind(null, { mml: { enabled: !uiConfig.mml.enabled } })} + checked={uiConfig.mml.enabled} + /> + + + + + } + onChange={updateCallback.bind(null, { + delayedData: { enabled: !uiConfig.delayedData.enabled }, + })} + checked={uiConfig.delayedData.enabled} + /> + + + + + } + onChange={updateCallback.bind(null, { + behindRealtime: { enabled: !uiConfig.behindRealtime.enabled }, + })} + checked={uiConfig.behindRealtime.enabled} + /> + + + + + } + onChange={updateCallback.bind(null, { + errorMessages: { enabled: !uiConfig.errorMessages.enabled }, + })} + checked={uiConfig.errorMessages.enabled} + /> + + + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts b/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts index b1640ab7aba7d..99ba61f3d9154 100644 --- a/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts +++ b/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts @@ -14,6 +14,7 @@ import type { PluginSetupContract as AlertingSetup } from '../../../alerting/pub import { PLUGIN_ID } from '../../common/constants/app'; import { formatExplorerUrl } from '../locator/formatters/anomaly_detection'; import { validateLookbackInterval, validateTopNBucket } from './validators'; +import { registerJobsHealthAlertingRule } from './jobs_health_rule'; export function registerMlAlerts( triggersActionsUi: TriggersAndActionsUIPublicPluginSetup, @@ -26,7 +27,7 @@ export function registerMlAlerts( }), iconClass: 'bell', documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/machine-learning/${docLinks.DOC_LINK_VERSION}/ml-configuring-alerts.html`; + return docLinks.links.ml.alertingRules; }, alertParamsExpression: lazy(() => import('./ml_anomaly_alert_trigger')), validate: (alertParams: MlAnomalyDetectionAlertParams) => { @@ -137,6 +138,8 @@ export function registerMlAlerts( ), }); + registerJobsHealthAlertingRule(triggersActionsUi, alerting); + if (alerting) { registerNavigation(alerting); } diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx new file mode 100644 index 0000000000000..cd9ecc70e553a --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx @@ -0,0 +1,373 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useState, useEffect, useMemo, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyout, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiFlyoutBody, + EuiTitle, + EuiSpacer, + EuiCheckbox, + EuiTabs, + EuiTab, + EuiLoadingSpinner, + EuiConfirmModal, +} from '@elastic/eui'; + +import { useMlApiContext, useMlKibana } from '../../../contexts/kibana'; +import { JobsExportService } from './jobs_export_service'; +import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; +import type { JobType } from '../../../../../common/types/saved_objects'; + +interface Props { + isDisabled: boolean; + currentTab: JobType; +} + +export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { + const mlApiServices = useMlApiContext(); + const { + getJobs, + dataFrameAnalytics: { getDataFrameAnalytics }, + } = mlApiServices; + + const { + services: { + notifications: { toasts }, + }, + } = useMlKibana(); + + const jobsExportService = useMemo(() => new JobsExportService(mlApiServices), []); + + const [loadingADJobs, setLoadingADJobs] = useState(true); + const [loadingDFAJobs, setLoadingDFAJobs] = useState(true); + const [showFlyout, setShowFlyout] = useState(false); + const [adJobIds, setAdJobIds] = useState([]); + const [dfaJobIds, setDfaJobIds] = useState([]); + const [selectedJobIds, setSelectedJobIds] = useState([]); + const [exporting, setExporting] = useState(false); + const [selectedJobType, setSelectedJobType] = useState(currentTab); + const [switchTabConfirmVisible, setSwitchTabConfirmVisible] = useState(false); + const { displayErrorToast, displaySuccessToast } = useMemo( + () => toastNotificationServiceProvider(toasts), + [toasts] + ); + + useEffect( + function onFlyoutChange() { + setLoadingADJobs(true); + setLoadingDFAJobs(true); + setAdJobIds([]); + setSelectedJobIds([]); + setExporting(false); + setSelectedJobType(currentTab); + setSwitchTabConfirmVisible(false); + + if (showFlyout) { + getJobs() + .then(({ jobs }) => { + setLoadingADJobs(false); + setAdJobIds(jobs.map((j) => j.job_id)); + }) + .catch((error) => { + const errorTitle = i18n.translate('xpack.ml.importExport.exportFlyout.adJobsError', { + defaultMessage: 'Could not load anomaly detection jobs', + }); + displayErrorToast(error, errorTitle); + }); + getDataFrameAnalytics() + .then(({ data_frame_analytics: dataFrameAnalytics }) => { + setLoadingDFAJobs(false); + setDfaJobIds(dataFrameAnalytics.map((j) => j.id)); + }) + .catch((error) => { + const errorTitle = i18n.translate('xpack.ml.importExport.exportFlyout.dfaJobsError', { + defaultMessage: 'Could not load data frame analytics jobs', + }); + displayErrorToast(error, errorTitle); + }); + } + }, + [showFlyout] + ); + + function toggleFlyout() { + setShowFlyout(!showFlyout); + } + + async function onExport() { + setExporting(true); + const title = i18n.translate('xpack.ml.importExport.exportFlyout.exportDownloading', { + defaultMessage: 'Your file is downloading in the background', + }); + displaySuccessToast(title); + + try { + if (selectedJobType === 'anomaly-detector') { + await jobsExportService.exportAnomalyDetectionJobs(selectedJobIds); + } else { + await jobsExportService.exportDataframeAnalyticsJobs(selectedJobIds); + } + + setExporting(false); + setShowFlyout(false); + } catch (error) { + const errorTitle = i18n.translate('xpack.ml.importExport.exportFlyout.exportError', { + defaultMessage: 'Could not export selected jobs', + }); + displayErrorToast(error, errorTitle); + setExporting(false); + } + } + + function toggleSelectedJob(checked: boolean, id: string) { + if (checked) { + setSelectedJobIds([...selectedJobIds, id]); + } else { + setSelectedJobIds(selectedJobIds.filter((id2) => id2 !== id)); + } + } + + const attemptTabSwitch = useCallback(() => { + // if the user has already selected some jobs, open a confirm modal + // rather than changing tabs + if (selectedJobIds.length > 0) { + setSwitchTabConfirmVisible(true); + return; + } + + switchTab(); + }, [selectedJobIds]); + + function switchTab() { + const jobType = + selectedJobType === 'anomaly-detector' ? 'data-frame-analytics' : 'anomaly-detector'; + + setSwitchTabConfirmVisible(false); + setSelectedJobIds([]); + setSelectedJobType(jobType); + } + + function onSelectAll() { + const ids = selectedJobType === 'anomaly-detector' ? adJobIds : dfaJobIds; + if (selectedJobIds.length === ids.length) { + setSelectedJobIds([]); + } else { + setSelectedJobIds([...ids]); + } + } + + return ( + <> + + + {showFlyout === true && isDisabled === false && ( + <> + setShowFlyout(false)} hideCloseButton size="s"> + + +

+ +

+
+
+ + + + + + + + + + + <> + {selectedJobType === 'anomaly-detector' && ( + <> + {loadingADJobs === true ? ( + + ) : ( + <> + + + + + + + {adJobIds.map((id) => ( +
+ toggleSelectedJob(e.target.checked, id)} + /> + +
+ ))} + + )} + + )} + {selectedJobType === 'data-frame-analytics' && ( + <> + {loadingDFAJobs === true ? ( + + ) : ( + <> + + + + + + + {dfaJobIds.map((id) => ( +
+ toggleSelectedJob(e.target.checked, id)} + /> + +
+ ))} + + )} + + )} + +
+ + + + + + + + + + + + + + +
+ + {switchTabConfirmVisible === true ? ( + + ) : null} + + )} + + ); +}; + +const FlyoutButton: FC<{ isDisabled: boolean; onClick(): void }> = ({ isDisabled, onClick }) => { + return ( + + + + ); +}; + +const LoadingSpinner: FC = () => ( + <> + + + + + + + +); + +const SwitchTabsConfirm: FC<{ onCancel: () => void; onConfirm: () => void }> = ({ + onCancel, + onConfirm, +}) => ( + +

+ +

+
+); diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/index.ts b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/index.ts new file mode 100644 index 0000000000000..270da6f35c100 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ExportJobsFlyout } from './export_jobs_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/jobs_export_service.ts b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/jobs_export_service.ts new file mode 100644 index 0000000000000..d44c59fff938e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/jobs_export_service.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// @ts-expect-error +import { saveAs } from '@elastic/filesaver'; +import type { MlApiServices } from '../../../services/ml_api_service'; +import type { JobType } from '../../../../../common/types/saved_objects'; +import type { Job, Datafeed } from '../../../../../common/types/anomaly_detection_jobs'; +import type { DataFrameAnalyticsConfig } from '../../../../../common/types/data_frame_analytics'; + +type ExportableConfigs = + | Array< + | { + job?: Job; + datafeed?: Datafeed; + } + | undefined + > + | DataFrameAnalyticsConfig[]; + +export class JobsExportService { + constructor(private _mlApiServices: MlApiServices) {} + + public async exportAnomalyDetectionJobs(jobIds: string[]) { + const configs = await Promise.all(jobIds.map(this._mlApiServices.jobs.jobForCloning)); + this._export(configs, 'anomaly-detector'); + } + + public async exportDataframeAnalyticsJobs(jobIds: string[]) { + const { + data_frame_analytics: configs, + } = await this._mlApiServices.dataFrameAnalytics.getDataFrameAnalytics(jobIds.join(','), true); + this._export(configs, 'data-frame-analytics'); + } + + private _export(configs: ExportableConfigs, jobType: JobType) { + const configsForExport = configs.length === 1 ? configs[0] : configs; + const blob = new Blob([JSON.stringify(configsForExport, null, 2)], { + type: 'application/json', + }); + const fileName = this._createFileName(jobType); + saveAs(blob, fileName); + } + + private _createFileName(jobType: JobType) { + return ( + (jobType === 'anomaly-detector' ? 'anomaly_detection' : 'data_frame_analytics') + '_jobs.json' + ); + } +} diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx new file mode 100644 index 0000000000000..7d573462a6c8f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { EuiCallOut, EuiText, EuiAccordion, EuiSpacer } from '@elastic/eui'; +import type { SkippedJobs } from './jobs_import_service'; + +interface Props { + jobs: SkippedJobs[]; + autoExpand?: boolean; +} + +export const CannotImportJobsCallout: FC = ({ jobs, autoExpand = false }) => { + if (jobs.length === 0) { + return null; + } + + return ( + <> + + {autoExpand ? ( + + ) : ( + + } + > + + + )} + + + + + ); +}; + +const SkippedJobList: FC<{ jobs: SkippedJobs[] }> = ({ jobs }) => ( + <> + {jobs.length > 0 && ( + <> + {jobs.map(({ jobId, missingIndices }) => ( + +
{jobId}
+ +
+ ))} + + )} + +); diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx new file mode 100644 index 0000000000000..4c7a2471db9d6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx @@ -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 React, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; + +export const CannotReadFileCallout: FC = () => { + return ( + <> + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx new file mode 100644 index 0000000000000..c156a41150420 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx @@ -0,0 +1,513 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useState, useEffect, useCallback, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useDebounce from 'react-use/lib/useDebounce'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlyout, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiButtonIcon, + EuiFlyoutBody, + EuiTitle, + EuiText, + EuiFilePicker, + EuiSpacer, + EuiPanel, + EuiFormRow, + EuiFieldText, +} from '@elastic/eui'; + +import type { DataFrameAnalyticsConfig } from '../../../data_frame_analytics/common'; +import type { JobType } from '../../../../../common/types/saved_objects'; +import { useMlApiContext, useMlKibana } from '../../../contexts/kibana'; +import { CannotImportJobsCallout } from './cannot_import_jobs_callout'; +import { CannotReadFileCallout } from './cannot_read_file_callout'; +import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; +import { JobImportService } from './jobs_import_service'; +import { useValidateIds } from './validate'; +import type { ImportedAdJob, JobIdObject, SkippedJobs } from './jobs_import_service'; +import { ErrorType, extractErrorProperties } from '../../../../../common/util/errors'; + +interface Props { + isDisabled: boolean; +} +export const ImportJobsFlyout: FC = ({ isDisabled }) => { + const { + jobs: { bulkCreateJobs }, + dataFrameAnalytics: { createDataFrameAnalytics }, + } = useMlApiContext(); + const { + services: { + data: { + indexPatterns: { getTitles: getIndexPatternTitles }, + }, + notifications: { toasts }, + }, + } = useMlKibana(); + + const jobImportService = useMemo(() => new JobImportService(), []); + + const [showFlyout, setShowFlyout] = useState(false); + const [adJobs, setAdJobs] = useState([]); + const [dfaJobs, setDfaJobs] = useState([]); + const [jobIdObjects, setJobIdObjects] = useState([]); + const [skippedJobs, setSkippedJobs] = useState([]); + const [importing, setImporting] = useState(false); + const [jobType, setJobType] = useState(null); + const [totalJobsRead, setTotalJobsRead] = useState(0); + const [importDisabled, setImportDisabled] = useState(true); + const [deleteDisabled, setDeleteDisabled] = useState(true); + const [idsMash, setIdsMash] = useState(''); + const [validatingJobs, setValidatingJobs] = useState(false); + const [showFileReadError, setShowFileReadError] = useState(false); + const { displayErrorToast, displaySuccessToast } = useMemo( + () => toastNotificationServiceProvider(toasts), + [toasts] + ); + + const [validateIds] = useValidateIds( + jobType, + jobIdObjects, + idsMash, + setJobIdObjects, + setValidatingJobs + ); + useDebounce(validateIds, 400, [idsMash]); + + const reset = useCallback((showFileError = false) => { + setAdJobs([]); + setDfaJobs([]); + setJobIdObjects([]); + setIdsMash(''); + setImporting(false); + setJobType(null); + setTotalJobsRead(0); + setValidatingJobs(false); + setShowFileReadError(showFileError); + }, []); + + useEffect( + function onFlyoutChange() { + reset(); + }, + [showFlyout] + ); + + function toggleFlyout() { + setShowFlyout(!showFlyout); + } + + const onFilePickerChange = useCallback(async (files: any) => { + setShowFileReadError(false); + + if (files.length === 0) { + reset(); + return; + } + + try { + const loadedFile = await jobImportService.readJobConfigs(files[0]); + if (loadedFile.jobType === null) { + reset(true); + return; + } + + setTotalJobsRead(loadedFile.jobs.length); + + const validatedJobs = await jobImportService.validateJobs( + loadedFile.jobs, + loadedFile.jobType, + getIndexPatternTitles + ); + + if (loadedFile.jobType === 'anomaly-detector') { + const tempJobs = (loadedFile.jobs as ImportedAdJob[]).filter((j) => + validatedJobs.jobs.map(({ jobId }) => jobId).includes(j.job.job_id) + ); + setAdJobs(tempJobs); + } else if (loadedFile.jobType === 'data-frame-analytics') { + const tempJobs = (loadedFile.jobs as DataFrameAnalyticsConfig[]).filter((j) => + validatedJobs.jobs.map(({ jobId }) => jobId).includes(j.id) + ); + setDfaJobs(tempJobs); + } + + setJobType(loadedFile.jobType); + setJobIdObjects( + validatedJobs.jobs.map(({ jobId, destIndex }) => ({ + jobId, + originalId: jobId, + jobIdValid: true, + jobIdInvalidMessage: '', + jobIdValidated: false, + destIndex, + originalDestIndex: destIndex, + destIndexValid: true, + destIndexInvalidMessage: '', + destIndexValidated: false, + })) + ); + + const ids = createIdsMash(validatedJobs.jobs as JobIdObject[], loadedFile.jobType); + setIdsMash(ids); + setValidatingJobs(true); + setSkippedJobs(validatedJobs.skippedJobs); + } catch (error) { + displayErrorToast(error); + } + }, []); + + const onImport = useCallback(async () => { + setImporting(true); + if (jobType === 'anomaly-detector') { + const renamedJobs = jobImportService.renameAdJobs(jobIdObjects, adJobs); + try { + await bulkCreateADJobs(renamedJobs); + } catch (error) { + // display unexpected error + displayErrorToast(error); + } + } else if (jobType === 'data-frame-analytics') { + const renamedJobs = jobImportService.renameDfaJobs(jobIdObjects, dfaJobs); + await bulkCreateDfaJobs(renamedJobs); + } + + setImporting(false); + setShowFlyout(false); + }, [jobType, jobIdObjects, adJobs, dfaJobs]); + + const bulkCreateADJobs = useCallback(async (jobs: ImportedAdJob[]) => { + const results = await bulkCreateJobs(jobs); + let successCount = 0; + const errors: ErrorType[] = []; + Object.entries(results).forEach(([jobId, { job, datafeed }]) => { + if (job.error || datafeed.error) { + if (job.error) { + errors.push(job.error); + } + if (datafeed.error) { + errors.push(datafeed.error); + } + } else { + successCount++; + } + }); + + if (successCount > 0) { + displayImportSuccessToast(successCount); + } + if (errors.length > 0) { + displayImportErrorToast(errors); + } + }, []); + + const bulkCreateDfaJobs = useCallback(async (jobs: DataFrameAnalyticsConfig[]) => { + const errors: ErrorType[] = []; + const results = await Promise.all( + jobs.map(async ({ id, ...config }) => { + try { + return await createDataFrameAnalytics(id, config); + } catch (error) { + errors.push(error); + } + }) + ); + const successCount = Object.values(results).filter((job) => job !== undefined).length; + if (successCount > 0) { + displayImportSuccessToast(successCount); + } + if (errors.length > 0) { + displayImportErrorToast(errors); + } + }, []); + + const displayImportSuccessToast = useCallback((count: number) => { + const title = i18n.translate('xpack.ml.importExport.importFlyout.importJobSuccessToast', { + defaultMessage: '{count, plural, one {# job} other {# jobs}} successfully imported', + values: { count }, + }); + displaySuccessToast(title); + }, []); + + const displayImportErrorToast = useCallback((errors: ErrorType[]) => { + const title = i18n.translate('xpack.ml.importExport.importFlyout.importJobErrorToast', { + defaultMessage: '{count, plural, one {# job} other {# jobs}} failed to import correctly', + values: { count: errors.length }, + }); + + const errorList = errors.map(extractErrorProperties); + displayErrorToast((errorList as unknown) as ErrorType, title); + }, []); + + const deleteJob = useCallback( + (index: number) => { + if (jobType === 'anomaly-detector') { + const js = [...adJobs]; + js.splice(index, 1); + setAdJobs(js); + } else if (jobType === 'data-frame-analytics') { + const js = [...dfaJobs]; + js.splice(index, 1); + setDfaJobs(js); + } + const js = [...jobIdObjects]; + js.splice(index, 1); + setJobIdObjects(js); + + const ids = createIdsMash(js, jobType); + setIdsMash(ids); + setValidatingJobs(true); + }, + [jobIdObjects, adJobs, dfaJobs] + ); + + useEffect(() => { + const disabled = + jobIdObjects.length === 0 || + importing === true || + validatingJobs === true || + jobIdObjects.some( + ({ jobIdValid, destIndexValid }) => jobIdValid === false || destIndexValid === false + ); + setImportDisabled(disabled); + + setDeleteDisabled(importing === true || validatingJobs === true); + }, [jobIdObjects, idsMash, validatingJobs, importing]); + + const renameJob = useCallback( + (id: string, i: number) => { + jobIdObjects[i].jobId = id; + jobIdObjects[i].jobIdValid = false; + jobIdObjects[i].jobIdValidated = false; + setJobIdObjects([...jobIdObjects]); + + const ids = createIdsMash(jobIdObjects, jobType); + setIdsMash(ids); + setValidatingJobs(true); + }, + [jobIdObjects] + ); + + const renameDestIndex = useCallback( + (id: string, i: number) => { + jobIdObjects[i].destIndex = id; + jobIdObjects[i].destIndexValid = false; + jobIdObjects[i].destIndexValidated = false; + jobIdObjects[i].destIndexInvalidMessage = ''; + setJobIdObjects([...jobIdObjects]); + + const ids = createIdsMash(jobIdObjects, jobType); + setIdsMash(ids); + setValidatingJobs(true); + }, + [jobIdObjects] + ); + + const DeleteJobButton: FC<{ index: number }> = ({ index }) => ( + deleteJob(index)} + /> + ); + + return ( + <> + + + {showFlyout === true && isDisabled === false && ( + + + +

+ +

+
+
+ + <> + + + + + {showFileReadError ? : null} + + {totalJobsRead > 0 && jobType !== null && ( + <> + + {jobType === 'anomaly-detector' && ( + + )} + + {jobType === 'data-frame-analytics' && ( + + )} + + + + + + + + + {jobIdObjects.map((jobId, i) => ( +
+ + + + + renameJob(e.target.value, i)} + isInvalid={jobId.jobIdValid === false} + /> + + + {jobType === 'data-frame-analytics' && ( + + renameDestIndex(e.target.value, i)} + isInvalid={jobId.destIndexValid === false} + /> + + )} + + + + + + + +
+ ))} + + )} + +
+ + + + + + + + + + + + + + +
+ )} + + ); +}; + +const FlyoutButton: FC<{ isDisabled: boolean; onClick(): void }> = ({ isDisabled, onClick }) => { + return ( + + + + ); +}; + +function createIdsMash(jobIdObjects: JobIdObject[], jobType: JobType | null) { + return ( + jobIdObjects.map(({ jobId }) => jobId).join('') + + (jobType === 'data-frame-analytics' + ? jobIdObjects.map(({ destIndex }) => destIndex).join('') + : '') + ); +} diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/index.ts b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/index.ts new file mode 100644 index 0000000000000..873ba9573f46f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ImportJobsFlyout } from './import_jobs_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/jobs_import_service.ts b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/jobs_import_service.ts new file mode 100644 index 0000000000000..85028426fa23d --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/jobs_import_service.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { JobType } from '../../../../../common/types/saved_objects'; +import type { Job, Datafeed } from '../../../../../common/types/anomaly_detection_jobs'; +import type { DataFrameAnalyticsConfig } from '../../../data_frame_analytics/common'; + +export interface ImportedAdJob { + job: Job; + datafeed: Datafeed; +} + +export interface JobIdObject { + jobId: string; + originalId: string; + jobIdValid: boolean; + jobIdInvalidMessage: string; + + jobIdValidated: boolean; + + destIndex?: string; + originalDestIndex?: string; + destIndexValid: boolean; + destIndexInvalidMessage: string; + + destIndexValidated: boolean; +} + +export interface SkippedJobs { + jobId: string; + missingIndices: string[]; +} + +function isImportedAdJobs(obj: any): obj is ImportedAdJob[] { + return Array.isArray(obj) && obj.some((o) => o.job && o.datafeed); +} + +function isDataFrameAnalyticsConfigs(obj: any): obj is DataFrameAnalyticsConfig[] { + return Array.isArray(obj) && obj.some((o) => o.dest && o.analysis); +} + +export class JobImportService { + private _readFile(file: File) { + return new Promise((resolve, reject) => { + if (file && file.size) { + const reader = new FileReader(); + reader.readAsText(file); + + reader.onload = (() => { + return () => { + const data = reader.result; + if (typeof data === 'string') { + try { + const json = JSON.parse(data); + resolve(json); + } catch (error) { + reject(); + } + } else { + reject(); + } + }; + })(); + } else { + reject(); + } + }); + } + public async readJobConfigs( + file: File + ): Promise<{ + jobs: ImportedAdJob[] | DataFrameAnalyticsConfig[]; + jobIds: string[]; + jobType: JobType | null; + }> { + try { + const json = await this._readFile(file); + const jobs = Array.isArray(json) ? json : [json]; + + if (isImportedAdJobs(jobs)) { + const jobIds = jobs.map((j) => j.job.job_id); + return { jobs, jobIds, jobType: 'anomaly-detector' }; + } else if (isDataFrameAnalyticsConfigs(jobs)) { + const jobIds = jobs.map((j) => j.id); + return { jobs, jobIds, jobType: 'data-frame-analytics' }; + } else { + return { jobs: [], jobIds: [], jobType: null }; + } + } catch (error) { + return { jobs: [], jobIds: [], jobType: null }; + } + } + + public renameAdJobs(jobIds: JobIdObject[], jobs: ImportedAdJob[]) { + if (jobs.length !== jobs.length) { + return jobs; + } + + return jobs.map((j, i) => { + const { jobId } = jobIds[i]; + j.job.job_id = jobId; + j.datafeed.job_id = jobId; + j.datafeed.datafeed_id = `datafeed-${jobId}`; + return j; + }); + } + + public renameDfaJobs(jobIds: JobIdObject[], jobs: DataFrameAnalyticsConfig[]) { + if (jobs.length !== jobs.length) { + return jobs; + } + + return jobs.map((j, i) => { + const { jobId, destIndex } = jobIds[i]; + j.id = jobId; + if (destIndex !== undefined) { + j.dest.index = destIndex; + } + return j; + }); + } + + public async validateJobs( + jobs: ImportedAdJob[] | DataFrameAnalyticsConfig[], + type: JobType, + getIndexPatternTitles: (refresh?: boolean) => Promise + ) { + const existingIndexPatterns = new Set(await getIndexPatternTitles()); + const tempJobs: Array<{ jobId: string; destIndex?: string }> = []; + const tempSkippedJobIds: SkippedJobs[] = []; + + const commonJobs: Array<{ jobId: string; indices: string[]; destIndex?: string }> = + type === 'anomaly-detector' + ? (jobs as ImportedAdJob[]).map((j) => ({ + jobId: j.job.job_id, + indices: j.datafeed.indices, + })) + : (jobs as DataFrameAnalyticsConfig[]).map((j) => ({ + jobId: j.id, + destIndex: j.dest.index, + indices: Array.isArray(j.source.index) ? j.source.index : [j.source.index], + })); + + commonJobs.forEach(({ jobId, indices, destIndex }) => { + const missingIndices = indices.filter((i) => existingIndexPatterns.has(i) === false); + if (missingIndices.length === 0) { + tempJobs.push({ + jobId, + ...(type === 'data-frame-analytics' ? { destIndex } : {}), + }); + } else { + tempSkippedJobIds.push({ + jobId, + missingIndices, + }); + } + }); + + return { + jobs: tempJobs, + skippedJobs: tempSkippedJobIds, + }; + } +} diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/validate.ts b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/validate.ts new file mode 100644 index 0000000000000..4c8ebe4e017aa --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/validate.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { JobType } from '../../../../../common/types/saved_objects'; +import { isValidIndexName } from '../../../../../common/util/es_utils'; +import { isJobIdValid } from '../../../../../common/util/job_utils'; +import { JOB_ID_MAX_LENGTH } from '../../../../../common/constants/validation'; +import type { JobIdObject } from './jobs_import_service'; +import { useMlApiContext } from '../../../contexts/kibana'; + +export const useValidateIds = ( + jobType: JobType | null, + jobIdObjects: JobIdObject[], + idsMash: string, + setJobIdObjects: (j: JobIdObject[]) => void, + setValidatingJobs: (b: boolean) => void +) => { + const { + jobs: { jobsExist: adJobsExist }, + dataFrameAnalytics: { jobsExist: dfaJobsExist }, + checkIndicesExists, + } = useMlApiContext(); + + const validateIds = useCallback(async () => { + const jobIdExistsChecks: string[] = []; + const destIndexExistsChecks: string[] = []; + + const skipDestIndexCheck = jobType === 'anomaly-detector'; + + jobIdObjects + .filter(({ jobIdValidated }) => jobIdValidated === false) + .forEach((j) => { + j.jobIdValid = true; + j.jobIdInvalidMessage = ''; + + if (j.jobId === '') { + j.jobIdValid = false; + j.jobIdInvalidMessage = jobEmpty; + j.jobIdValidated = skipDestIndexCheck; + } else if (j.jobId.length > JOB_ID_MAX_LENGTH) { + j.jobIdValid = false; + j.jobIdInvalidMessage = jobInvalidLength; + j.jobIdValidated = skipDestIndexCheck; + } else if (isJobIdValid(j.jobId) === false) { + j.jobIdValid = false; + j.jobIdInvalidMessage = jobInvalid; + j.jobIdValidated = skipDestIndexCheck; + } + + if (j.jobIdValid === true) { + jobIdExistsChecks.push(j.jobId); + } + }); + + if (jobType === 'data-frame-analytics') { + jobIdObjects + .filter(({ destIndexValidated }) => destIndexValidated === false) + .forEach((j) => { + if (j.destIndex === undefined) { + return; + } + j.destIndexValid = true; + j.destIndexInvalidMessage = ''; + + if (j.destIndex === '') { + j.destIndexValid = false; + j.destIndexInvalidMessage = destIndexEmpty; + j.destIndexValidated = true; + } else if (isValidIndexName(j.destIndex) === false) { + j.destIndexValid = false; + j.destIndexInvalidMessage = destIndexInvalid; + j.destIndexValidated = true; + } + + if (j.destIndexValid === true) { + destIndexExistsChecks.push(j.destIndex); + } + }); + } + + if (jobType !== null) { + const jobsExist = jobType === 'anomaly-detector' ? adJobsExist : dfaJobsExist; + const resp = await jobsExist(jobIdExistsChecks, true); + jobIdObjects.forEach((j) => { + const jobResp = resp[j.jobId]; + if (jobResp) { + const { exists } = jobResp; + j.jobIdValid = !exists; + j.jobIdInvalidMessage = exists ? jobExists : ''; + j.jobIdValidated = true; + } + }); + + if (jobType === 'data-frame-analytics') { + const resp2 = await checkIndicesExists({ indices: destIndexExistsChecks }); + jobIdObjects.forEach((j) => { + if (j.destIndex !== undefined && j.destIndexValidated === false) { + const exists = resp2[j.destIndex]?.exists === true; + j.destIndexInvalidMessage = exists ? destIndexExists : ''; + j.destIndexValidated = true; + } + }); + } + + setJobIdObjects([...jobIdObjects]); + setValidatingJobs(false); + } + }, [idsMash, jobIdObjects]); + + return [validateIds]; +}; + +const jobEmpty = i18n.translate('xpack.ml.importExport.importFlyout.validateJobId.jobNameEmpty', { + defaultMessage: 'Enter a valid job ID', +}); +const jobInvalid = i18n.translate( + 'xpack.ml.importExport.importFlyout.validateJobId.jobNameAllowedCharacters', + { + defaultMessage: + 'Job ID can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' + + 'must start and end with an alphanumeric character', + } +); +const jobInvalidLength = i18n.translate( + 'xpack.ml.importExport.importFlyout.validateJobId.jobIdInvalidMaxLengthErrorMessage', + { + defaultMessage: + 'Job ID must be no more than {maxLength, plural, one {# character} other {# characters}} long.', + values: { + maxLength: JOB_ID_MAX_LENGTH, + }, + } +); +const jobExists = i18n.translate( + 'xpack.ml.importExport.importFlyout.validateJobId.jobNameAlreadyExists', + { + defaultMessage: + 'Job ID already exists. A job ID cannot be the same as an existing job or group.', + } +); + +const destIndexEmpty = i18n.translate( + 'xpack.ml.importExport.importFlyout.validateDestIndex.destIndexEmpty', + { + defaultMessage: 'Enter a valid destination index', + } +); + +const destIndexInvalid = i18n.translate( + 'xpack.ml.importExport.importFlyout.validateDestIndex.destIndexInvalid', + { + defaultMessage: 'Invalid destination index name.', + } +); + +const destIndexExists = i18n.translate( + 'xpack.ml.importExport.importFlyout.validateDestIndex.destIndexExists', + { + defaultMessage: + 'An index with this name already exists. Be aware that running this analytics job will modify this destination index.', + } +); diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/index.ts b/x-pack/plugins/ml/public/application/components/import_export_jobs/index.ts new file mode 100644 index 0000000000000..3e9e0db4ea1e4 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ImportJobsFlyout } from './import_jobs_flyout'; +export { ExportJobsFlyout } from './export_jobs_flyout'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx index 5b36a3a1ccb96..478f3b0056de1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx @@ -56,8 +56,8 @@ export const CreateAnalyticsAdvancedEditor: FC = (prop () => debounce(async () => { try { - const { results } = await ml.dataFrameAnalytics.jobsExists([jobId], true); - setFormState({ jobIdExists: results[jobId] }); + const results = await ml.dataFrameAnalytics.jobsExist([jobId], true); + setFormState({ jobIdExists: results[jobId].exists }); } catch (e) { toasts.addDanger( i18n.translate( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index 5129fbb64c0ec..746b02d934002 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -83,8 +83,8 @@ export const DetailsStepForm: FC = ({ const debouncedIndexCheck = debounce(async () => { try { - const { exists } = await ml.checkIndexExists({ index: destinationIndex }); - setFormState({ destinationIndexNameExists: exists }); + const resp = await ml.checkIndicesExists({ indices: [destinationIndex] }); + setFormState({ destinationIndexNameExists: resp[destinationIndex].exists }); } catch (e) { notifications.toasts.addDanger( i18n.translate('xpack.ml.dataframe.analytics.create.errorCheckingIndexExists', { @@ -99,8 +99,8 @@ export const DetailsStepForm: FC = ({ () => debounce(async () => { try { - const { results } = await ml.dataFrameAnalytics.jobsExists([jobId], true); - setFormState({ jobIdExists: results[jobId] }); + const results = await ml.dataFrameAnalytics.jobsExist([jobId], true); + setFormState({ jobIdExists: results[jobId].exists }); } catch (e) { notifications.toasts.addDanger( i18n.translate('xpack.ml.dataframe.analytics.create.errorCheckingJobIdExists', { diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 77f0e6123dfad..6a76b1e207ec5 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -18,9 +18,12 @@ import { EuiSpacer, EuiTabbedContent, EuiTabbedContentTab, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import type { SpacesContextProps } from 'src/plugins/spaces_oss/public'; +import type { DataPublicPluginStart } from 'src/plugins/data/public'; import { PLUGIN_ID } from '../../../../../../common/constants/app'; import { ManagementAppMountParams } from '../../../../../../../../../src/plugins/management/public/'; @@ -43,6 +46,8 @@ import { getDefaultAnomalyDetectionJobsListState } from '../../../../jobs/jobs_l import { getMlGlobalServices } from '../../../../app'; import { ListingPageUrlState } from '../../../../../../common/types/common'; import { getDefaultDFAListState } from '../../../../data_frame_analytics/pages/analytics_management/page'; +import { ExportJobsFlyout, ImportJobsFlyout } from '../../../../components/import_export_jobs'; +import { JobType } from '../../../../../../common/types/saved_objects'; interface Tab extends EuiTabbedContentTab { 'data-test-subj': string; @@ -76,7 +81,7 @@ function useTabs(isMlEnabledInSpace: boolean, spacesApi: SpacesPluginStart | und () => [ { 'data-test-subj': 'mlStackManagementJobsListAnomalyDetectionTab', - id: 'anomaly_detection_jobs', + id: 'anomaly-detector', name: i18n.translate('xpack.ml.management.jobsList.anomalyDetectionTab', { defaultMessage: 'Anomaly detection', }), @@ -95,7 +100,7 @@ function useTabs(isMlEnabledInSpace: boolean, spacesApi: SpacesPluginStart | und }, { 'data-test-subj': 'mlStackManagementJobsListAnalyticsTab', - id: 'analytics_jobs', + id: 'data-frame-analytics', name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', { defaultMessage: 'Analytics', }), @@ -122,7 +127,8 @@ export const JobsListPage: FC<{ share: SharePluginStart; history: ManagementAppMountParams['history']; spacesApi?: SpacesPluginStart; -}> = ({ coreStart, share, history, spacesApi }) => { + data: DataPublicPluginStart; +}> = ({ coreStart, share, history, spacesApi, data }) => { const spacesEnabled = spacesApi !== undefined; const [initialized, setInitialized] = useState(false); const [accessDenied, setAccessDenied] = useState(false); @@ -130,7 +136,7 @@ export const JobsListPage: FC<{ const [showSyncFlyout, setShowSyncFlyout] = useState(false); const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false); const tabs = useTabs(isMlEnabledInSpace, spacesApi); - const [currentTabId, setCurrentTabId] = useState(tabs[0].id); + const [currentTabId, setCurrentTabId] = useState('anomaly-detector'); const I18nContext = coreStart.i18n.Context; const check = async () => { @@ -175,14 +181,12 @@ export const JobsListPage: FC<{ const docsLink = ( - {currentTabId === 'anomaly_detection_jobs' ? anomalyDetectionDocsLabel : analyticsDocsLabel} + {currentTabId === 'anomaly-detector' ? anomalyDetectionDocsLabel : analyticsDocsLabel} ); @@ -190,7 +194,7 @@ export const JobsListPage: FC<{ return ( { - setCurrentTabId(id); + setCurrentTabId(id as JobType); }} size="s" tabs={tabs} @@ -215,7 +219,7 @@ export const JobsListPage: FC<{ @@ -242,17 +246,27 @@ export const JobsListPage: FC<{ id="kibanaManagementMLSection" data-test-subj="mlPageStackManagementJobsList" > - {spacesEnabled && ( - <> - setShowSyncFlyout(true)}> - {i18n.translate('xpack.ml.management.jobsList.syncFlyoutButton', { - defaultMessage: 'Synchronize saved objects', - })} - - {showSyncFlyout && } - - - )} + + + {spacesEnabled && ( + <> + setShowSyncFlyout(true)}> + {i18n.translate('xpack.ml.management.jobsList.syncFlyoutButton', { + defaultMessage: 'Synchronize saved objects', + })} + + {showSyncFlyout && } + + + )} + + + + + + + + {renderTabs()} diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts index dde543ac6ac9c..039653af0d095 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts +++ b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts @@ -8,6 +8,7 @@ import ReactDOM, { unmountComponentAtNode } from 'react-dom'; import React from 'react'; import { CoreSetup, CoreStart } from 'kibana/public'; +import type { DataPublicPluginStart } from 'src/plugins/data/public'; import { ManagementAppMountParams } from '../../../../../../../src/plugins/management/public/'; import { MlStartDependencies } from '../../../plugin'; import { JobsListPage } from './components'; @@ -22,10 +23,11 @@ const renderApp = ( history: ManagementAppMountParams['history'], coreStart: CoreStart, share: SharePluginStart, + data: DataPublicPluginStart, spacesApi?: SpacesPluginStart ) => { ReactDOM.render( - React.createElement(JobsListPage, { coreStart, history, share, spacesApi }), + React.createElement(JobsListPage, { coreStart, history, share, data, spacesApi }), element ); return () => { @@ -53,6 +55,7 @@ export async function mountApp( params.history, coreStart, pluginsStart.share, + pluginsStart.data, pluginsStart.spaces ); } diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index 39662cfedd901..8aba633970a78 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -50,9 +50,7 @@ export interface DeleteDataFrameAnalyticsWithIndexResponse { } export interface JobsExistsResponse { - results: { - [jobId: string]: boolean; - }; + [jobId: string]: { exists: boolean }; } export const dataFrameAnalytics = { @@ -108,7 +106,7 @@ export const dataFrameAnalytics = { query: { treatAsRoot, type }, }); }, - jobsExists(analyticsIds: string[], allSpaces: boolean = false) { + jobsExist(analyticsIds: string[], allSpaces: boolean = false) { const body = JSON.stringify({ analyticsIds, allSpaces }); return http({ path: `${basePath()}/data_frame/analytics/jobs_exist`, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 1bfc597ba0b10..81a86e5a7f980 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -366,10 +366,10 @@ export function mlApiServicesProvider(httpService: HttpService) { }); }, - checkIndexExists({ index }: { index: string }) { - const body = JSON.stringify({ index }); + checkIndicesExists({ indices }: { indices: string[] }) { + const body = JSON.stringify({ indices }); - return httpService.http<{ exists: boolean }>({ + return httpService.http>({ path: `${basePath()}/index_exists`, method: 'POST', body, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index 5695e3d830890..a982b78d59914 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -27,7 +27,7 @@ import type { } from '../../../../common/types/categories'; import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/categorization_job'; import type { Category } from '../../../../common/types/categories'; -import type { JobsExistResponse } from '../../../../common/types/job_service'; +import type { JobsExistResponse, BulkCreateResults } from '../../../../common/types/job_service'; import { ML_BASE_PATH } from '../../../../common/constants/app'; export const jobsApiProvider = (httpService: HttpService) => ({ @@ -364,4 +364,13 @@ export const jobsApiProvider = (httpService: HttpService) => ({ body, }); }, + + bulkCreateJobs(jobs: { job: Job; datafeed: Datafeed } | Array<{ job: Job; datafeed: Datafeed }>) { + const body = JSON.stringify(jobs); + return httpService.http({ + path: `${ML_BASE_PATH}/jobs/bulk_create`, + method: 'POST', + body, + }); + }, }); diff --git a/x-pack/plugins/ml/public/embeddables/common/process_filters.ts b/x-pack/plugins/ml/public/embeddables/common/process_filters.ts index 8ff75205b4d48..e054df09bd95b 100644 --- a/x-pack/plugins/ml/public/embeddables/common/process_filters.ts +++ b/x-pack/plugins/ml/public/embeddables/common/process_filters.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { Filter } from '../../../../../../src/plugins/data/common/es_query/filters'; -import { Query } from '../../../../../../src/plugins/data/common/query'; +import { Filter, Query } from '@kbn/es-query'; import { esKuery, esQuery } from '../../../../../../src/plugins/data/public'; export function processFilters(filters: Filter[], query: Query, controlledBy?: string) { diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index 60355dae5baf4..436eee0698708 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -6,14 +6,10 @@ */ import type { CoreStart } from 'kibana/public'; +import type { Filter, Query } from '@kbn/es-query'; import type { JobId } from '../../common/types/anomaly_detection_jobs'; import type { SwimlaneType } from '../application/explorer/explorer_constants'; -import type { Filter } from '../../../../../src/plugins/data/common/es_query/filters'; -import type { - Query, - RefreshInterval, - TimeRange, -} from '../../../../../src/plugins/data/common/query'; +import type { RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common'; import type { EmbeddableInput, EmbeddableOutput, diff --git a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts index e7d3ef97a301b..e4c1e0fe53f01 100644 --- a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts @@ -436,7 +436,7 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da const jobIds = jobsResponse.map((v) => v.job_id); - const dataFeeds = await datafeedsService.getDatafeedByJobId(jobIds); + const datafeeds = await datafeedsService.getDatafeedByJobId(jobIds); const maxBucketInSeconds = resolveMaxTimeInterval( jobsResponse.map((v) => v.analysis_config.bucket_span) @@ -448,7 +448,7 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da } const lookBackTimeInterval: string = - params.lookbackInterval ?? resolveLookbackInterval(jobsResponse, dataFeeds ?? []); + params.lookbackInterval ?? resolveLookbackInterval(jobsResponse, datafeeds ?? []); const topNBuckets: number = params.topNBuckets ?? getTopNBuckets(jobsResponse[0]); diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts new file mode 100644 index 0000000000000..59213a7cf6ab1 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { JobsHealthService, jobsHealthServiceProvider } from './jobs_health_service'; +import type { DatafeedsService } from '../../models/job_service/datafeeds'; +import type { Logger } from 'kibana/server'; +import { MlClient } from '../ml_client'; +import { MlJob, MlJobStats } from '@elastic/elasticsearch/api/types'; + +describe('JobsHealthService', () => { + const mlClient = ({ + getJobs: jest.fn().mockImplementation(({ job_id: jobIds = [] }) => { + let jobs: MlJob[] = []; + + if (jobIds.some((v: string) => v === 'test_group')) { + jobs = [ + ({ + job_id: 'test_job_01', + } as unknown) as MlJob, + ({ + job_id: 'test_job_02', + } as unknown) as MlJob, + ({ + job_id: 'test_job_03', + } as unknown) as MlJob, + ]; + } + + if (jobIds[0]?.startsWith('test_job_')) { + jobs = [ + ({ + job_id: jobIds[0], + } as unknown) as MlJob, + ]; + } + + return Promise.resolve({ + body: { + jobs, + }, + }); + }), + getJobStats: jest.fn().mockImplementation(({ job_id: jobIdsStr }) => { + const jobsIds = jobIdsStr.split(','); + return Promise.resolve({ + body: { + jobs: jobsIds.map((j: string) => { + return { + job_id: j, + state: j === 'test_job_02' ? 'opened' : 'closed', + }; + }) as MlJobStats, + }, + }); + }), + getDatafeedStats: jest.fn().mockImplementation(({ datafeed_id: datafeedIdsStr }) => { + const datafeedIds = datafeedIdsStr.split(','); + return Promise.resolve({ + body: { + datafeeds: datafeedIds.map((d: string) => { + return { + datafeed_id: d, + state: d === 'test_datafeed_02' ? 'stopped' : 'started', + timing_stats: { + job_id: d.replace('datafeed', 'job'), + }, + }; + }) as MlJobStats, + }, + }); + }), + } as unknown) as jest.Mocked; + + const datafeedsService = ({ + getDatafeedByJobId: jest.fn().mockImplementation((jobIds: string[]) => { + return Promise.resolve( + jobIds.map((j) => { + return { + datafeed_id: j.replace('job', 'datafeed'), + }; + }) + ); + }), + } as unknown) as jest.Mocked; + + const logger = ({ + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + } as unknown) as jest.Mocked; + + const jobHealthService: JobsHealthService = jobsHealthServiceProvider( + mlClient, + datafeedsService, + logger + ); + + beforeEach(() => {}); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('returns empty results when no jobs provided', async () => { + // act + const executionResult = await jobHealthService.getTestsResults('testRule', { + testsConfig: null, + includeJobs: { + jobIds: ['*'], + groupIds: [], + }, + excludeJobs: null, + }); + expect(logger.warn).toHaveBeenCalledWith('Rule "testRule" does not have associated jobs.'); + expect(datafeedsService.getDatafeedByJobId).not.toHaveBeenCalled(); + expect(executionResult).toEqual([]); + }); + + test('returns empty results and does not perform datafeed check when test is disabled', async () => { + const executionResult = await jobHealthService.getTestsResults('testRule', { + testsConfig: { + datafeed: { + enabled: false, + }, + behindRealtime: null, + delayedData: null, + errorMessages: null, + mml: null, + }, + includeJobs: { + jobIds: ['test_job_01'], + groupIds: [], + }, + excludeJobs: null, + }); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith(`Performing health checks for job IDs: test_job_01`); + expect(datafeedsService.getDatafeedByJobId).not.toHaveBeenCalled(); + expect(executionResult).toEqual([]); + }); + + test('returns results based on provided selection', async () => { + const executionResult = await jobHealthService.getTestsResults('testRule_03', { + testsConfig: null, + includeJobs: { + jobIds: [], + groupIds: ['test_group'], + }, + excludeJobs: { + jobIds: ['test_job_03'], + groupIds: [], + }, + }); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + `Performing health checks for job IDs: test_job_01, test_job_02` + ); + expect(datafeedsService.getDatafeedByJobId).toHaveBeenCalledWith([ + 'test_job_01', + 'test_job_02', + ]); + expect(mlClient.getJobStats).toHaveBeenCalledWith({ job_id: 'test_job_01,test_job_02' }); + expect(mlClient.getDatafeedStats).toHaveBeenCalledWith({ + datafeed_id: 'test_datafeed_01,test_datafeed_02', + }); + expect(executionResult).toEqual([ + { + name: 'Datafeed is not started', + context: { + jobIds: ['test_job_02'], + message: 'Datafeed is not started for the following jobs:', + }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts new file mode 100644 index 0000000000000..db4907decc3f0 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts @@ -0,0 +1,185 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, SavedObjectsClientContract } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { Logger } from 'kibana/server'; +import { MlJobState } from '@elastic/elasticsearch/api/types'; +import { MlClient } from '../ml_client'; +import { + AnomalyDetectionJobsHealthRuleParams, + JobSelection, +} from '../../routes/schemas/alerting_schema'; +import { datafeedsProvider, DatafeedsService } from '../../models/job_service/datafeeds'; +import { ALL_JOBS_SELECTION, HEALTH_CHECK_NAMES } from '../../../common/constants/alerts'; +import { DatafeedStats } from '../../../common/types/anomaly_detection_jobs'; +import { GetGuards } from '../../shared_services/shared_services'; +import { AnomalyDetectionJobsHealthAlertContext } from './register_jobs_monitoring_rule_type'; +import { getResultJobsHealthRuleConfig } from '../../../common/util/alerts'; + +interface TestResult { + name: string; + context: AnomalyDetectionJobsHealthAlertContext; +} + +type TestsResults = TestResult[]; + +type NotStartedDatafeedResponse = Array; + +export function jobsHealthServiceProvider( + mlClient: MlClient, + datafeedsService: DatafeedsService, + logger: Logger +) { + /** + * Extracts result list of job ids based on included and excluded selection of jobs and groups. + * @param includeJobs + * @param excludeJobs + */ + const getResultJobIds = async (includeJobs: JobSelection, excludeJobs?: JobSelection | null) => { + const jobAndGroupIds = [...(includeJobs.jobIds ?? []), ...(includeJobs.groupIds ?? [])]; + + const includeAllJobs = jobAndGroupIds.some((id) => id === ALL_JOBS_SELECTION); + + // Extract jobs from group ids and make sure provided jobs assigned to a current space + const jobsResponse = ( + await mlClient.getJobs({ + ...(includeAllJobs ? {} : { job_id: jobAndGroupIds }), + }) + ).body.jobs; + + let resultJobIds = jobsResponse.map((v) => v.job_id); + + if (excludeJobs && (!!excludeJobs.jobIds.length || !!excludeJobs?.groupIds.length)) { + const excludedJobAndGroupIds = [ + ...(excludeJobs?.jobIds ?? []), + ...(excludeJobs?.groupIds ?? []), + ]; + const excludedJobsResponse = ( + await mlClient.getJobs({ + job_id: excludedJobAndGroupIds, + }) + ).body.jobs; + + const excludedJobsIds: Set = new Set(excludedJobsResponse.map((v) => v.job_id)); + + resultJobIds = resultJobIds.filter((v) => !excludedJobsIds.has(v)); + } + + return resultJobIds; + }; + + return { + /** + * Gets not started datafeeds for opened jobs. + * @param jobIds + */ + async getNotStartedDatafeeds(jobIds: string[]): Promise { + const datafeeds = await datafeedsService.getDatafeedByJobId(jobIds); + + if (datafeeds) { + const { + body: { jobs: jobsStats }, + } = await mlClient.getJobStats({ job_id: jobIds.join(',') }); + + const { + body: { datafeeds: datafeedsStats }, + } = await mlClient.getDatafeedStats({ + datafeed_id: datafeeds.map((d) => d.datafeed_id).join(','), + }); + + // match datafeed stats with the job ids + return (datafeedsStats as DatafeedStats[]) + .map((datafeedStats) => { + const jobId = datafeedStats.timing_stats.job_id; + const jobState = + jobsStats.find((jobStats) => jobStats.job_id === jobId)?.state ?? 'failed'; + return { + ...datafeedStats, + job_id: jobId, + job_state: jobState, + }; + }) + .filter((datafeedStat) => { + // Find opened jobs with not started datafeeds + return datafeedStat.job_state === 'opened' && datafeedStat.state !== 'started'; + }); + } + }, + /** + * Retrieves report grouped by test. + */ + async getTestsResults( + ruleInstanceName: string, + { testsConfig, includeJobs, excludeJobs }: AnomalyDetectionJobsHealthRuleParams + ): Promise { + const config = getResultJobsHealthRuleConfig(testsConfig); + + const results: TestsResults = []; + + const jobIds = await getResultJobIds(includeJobs, excludeJobs); + + if (jobIds.length === 0) { + logger.warn(`Rule "${ruleInstanceName}" does not have associated jobs.`); + return results; + } + + logger.debug(`Performing health checks for job IDs: ${jobIds.join(', ')}`); + + if (config.datafeed.enabled) { + const response = await this.getNotStartedDatafeeds(jobIds); + if (response && response.length > 0) { + results.push({ + name: HEALTH_CHECK_NAMES.datafeed, + context: { + jobIds: [...new Set(response.map((v) => v.job_id))], + message: i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedStateMessage', + { + defaultMessage: 'Datafeed is not started for the following jobs:', + } + ), + }, + }); + } + } + + return results; + }, + }; +} + +export type JobsHealthService = ReturnType; + +export function getJobsHealthServiceProvider(getGuards: GetGuards) { + return { + jobsHealthServiceProvider( + savedObjectsClient: SavedObjectsClientContract, + request: KibanaRequest, + logger: Logger + ) { + return { + getTestsResults: async ( + ...args: Parameters + ): ReturnType => { + return await getGuards(request, savedObjectsClient) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(({ mlClient, scopedClient }) => + jobsHealthServiceProvider( + mlClient, + datafeedsProvider(scopedClient, mlClient), + logger + ).getTestsResults(...args) + ); + }, + }; + }, + }; +} + +export type JobsHealthServiceProvider = ReturnType; diff --git a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts index 07bca8f3aae74..e30ea01b27cb5 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts @@ -7,11 +7,7 @@ import { i18n } from '@kbn/i18n'; import { KibanaRequest } from 'kibana/server'; -import { - ML_ALERT_TYPES, - ML_ALERT_TYPES_CONFIG, - AnomalyScoreMatchGroupId, -} from '../../../common/constants/alerts'; +import { ML_ALERT_TYPES } from '../../../common/constants/alerts'; import { PLUGIN_ID } from '../../../common/constants/app'; import { MINIMUM_FULL_LICENSE } from '../../../common/license'; import { @@ -21,13 +17,12 @@ import { import { RegisterAlertParams } from './register_ml_alerts'; import { InfluencerAnomalyAlertDoc, RecordAnomalyAlertDoc } from '../../../common/types/alerts'; import { + ActionGroup, AlertInstanceContext, AlertInstanceState, AlertTypeState, } from '../../../../alerting/common'; -const alertTypeConfig = ML_ALERT_TYPES_CONFIG[ML_ALERT_TYPES.ANOMALY_DETECTION]; - export type AnomalyDetectionAlertContext = { name: string; jobIds: string[]; @@ -40,6 +35,17 @@ export type AnomalyDetectionAlertContext = { anomalyExplorerUrl: string; } & AlertInstanceContext; +export const ANOMALY_SCORE_MATCH_GROUP_ID = 'anomaly_score_match'; + +export type AnomalyScoreMatchGroupId = typeof ANOMALY_SCORE_MATCH_GROUP_ID; + +export const THRESHOLD_MET_GROUP: ActionGroup = { + id: ANOMALY_SCORE_MATCH_GROUP_ID, + name: i18n.translate('xpack.ml.anomalyDetectionAlert.actionGroupName', { + defaultMessage: 'Anomaly score matched the condition', + }), +}; + export function registerAnomalyDetectionAlertType({ alerting, mlSharedServices, @@ -53,9 +59,11 @@ export function registerAnomalyDetectionAlertType({ AnomalyScoreMatchGroupId >({ id: ML_ALERT_TYPES.ANOMALY_DETECTION, - name: alertTypeConfig.name, - actionGroups: alertTypeConfig.actionGroups, - defaultActionGroupId: alertTypeConfig.defaultActionGroupId, + name: i18n.translate('xpack.ml.anomalyDetectionAlert.name', { + defaultMessage: 'Anomaly detection alert', + }), + actionGroups: [THRESHOLD_MET_GROUP], + defaultActionGroupId: ANOMALY_SCORE_MATCH_GROUP_ID, validate: { params: mlAnomalyDetectionAlertParams, }, @@ -76,7 +84,7 @@ export function registerAnomalyDetectionAlertType({ { name: 'jobIds', description: i18n.translate('xpack.ml.alertContext.jobIdsDescription', { - defaultMessage: 'List of job IDs that triggered the alert instance', + defaultMessage: 'List of job IDs that triggered the alert', }), }, { @@ -132,7 +140,7 @@ export function registerAnomalyDetectionAlertType({ if (executionResult) { const alertInstanceName = executionResult.name; const alertInstance = services.alertInstanceFactory(alertInstanceName); - alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, executionResult); + alertInstance.scheduleActions(ANOMALY_SCORE_MATCH_GROUP_ID, executionResult); } }, }); diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts new file mode 100644 index 0000000000000..3547b44cc73e4 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { KibanaRequest } from 'kibana/server'; +import { ML_ALERT_TYPES } from '../../../common/constants/alerts'; +import { PLUGIN_ID } from '../../../common/constants/app'; +import { MINIMUM_FULL_LICENSE } from '../../../common/license'; +import { + anomalyDetectionJobsHealthRuleParams, + AnomalyDetectionJobsHealthRuleParams, +} from '../../routes/schemas/alerting_schema'; +import { RegisterAlertParams } from './register_ml_alerts'; +import { + ActionGroup, + AlertInstanceContext, + AlertInstanceState, + AlertTypeState, +} from '../../../../alerting/common'; + +export type AnomalyDetectionJobsHealthAlertContext = { + jobIds: string[]; + message: string; +} & AlertInstanceContext; + +export const ANOMALY_DETECTION_JOB_REALTIME_ISSUE = 'anomaly_detection_realtime_issue'; + +export type AnomalyDetectionJobRealtimeIssue = typeof ANOMALY_DETECTION_JOB_REALTIME_ISSUE; + +export const REALTIME_ISSUE_DETECTED: ActionGroup = { + id: ANOMALY_DETECTION_JOB_REALTIME_ISSUE, + name: i18n.translate('xpack.ml.jobsHealthAlertingRule.actionGroupName', { + defaultMessage: 'Real-time issue detected', + }), +}; + +export function registerJobsMonitoringRuleType({ + alerting, + mlServicesProviders, + logger, +}: RegisterAlertParams) { + alerting.registerType< + AnomalyDetectionJobsHealthRuleParams, + never, // Only use if defining useSavedObjectReferences hook + AlertTypeState, + AlertInstanceState, + AnomalyDetectionJobsHealthAlertContext, + AnomalyDetectionJobRealtimeIssue + >({ + id: ML_ALERT_TYPES.AD_JOBS_HEALTH, + name: i18n.translate('xpack.ml.jobsHealthAlertingRule.name', { + defaultMessage: 'Anomaly detection jobs health', + }), + actionGroups: [REALTIME_ISSUE_DETECTED], + defaultActionGroupId: ANOMALY_DETECTION_JOB_REALTIME_ISSUE, + validate: { + params: anomalyDetectionJobsHealthRuleParams, + }, + actionVariables: { + context: [ + { + name: 'jobIds', + description: i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.alertContext.jobIdsDescription', + { + defaultMessage: 'List of job IDs that triggered the alert', + } + ), + }, + { + name: 'message', + description: i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.alertContext.messageDescription', + { + defaultMessage: 'Alert info message', + } + ), + }, + ], + }, + producer: PLUGIN_ID, + minimumLicenseRequired: MINIMUM_FULL_LICENSE, + isExportable: true, + async executor({ services, params, alertId, state, previousStartedAt, startedAt, name }) { + const fakeRequest = {} as KibanaRequest; + const { getTestsResults } = mlServicesProviders.jobsHealthServiceProvider( + services.savedObjectsClient, + fakeRequest, + logger + ); + const executionResult = await getTestsResults(name, params); + + if (executionResult.length > 0) { + logger.info( + `Scheduling actions for tests: ${executionResult.map((v) => v.name).join(', ')}` + ); + + executionResult.forEach(({ name: alertInstanceName, context }) => { + const alertInstance = services.alertInstanceFactory(alertInstanceName); + alertInstance.scheduleActions(ANOMALY_DETECTION_JOB_REALTIME_ISSUE, context); + }); + } + }, + }); +} diff --git a/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts b/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts index 8368c606598f0..6f1e000c9a430 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts @@ -9,13 +9,17 @@ import { Logger } from 'kibana/server'; import { AlertingPlugin } from '../../../../alerting/server'; import { registerAnomalyDetectionAlertType } from './register_anomaly_detection_alert_type'; import { SharedServices } from '../../shared_services'; +import { registerJobsMonitoringRuleType } from './register_jobs_monitoring_rule_type'; +import { MlServicesProviders } from '../../shared_services/shared_services'; export interface RegisterAlertParams { alerting: AlertingPlugin['setup']; logger: Logger; mlSharedServices: SharedServices; + mlServicesProviders: MlServicesProviders; } export function registerMlAlerts(params: RegisterAlertParams) { registerAnomalyDetectionAlertType(params); + registerJobsMonitoringRuleType(params); } diff --git a/x-pack/plugins/ml/server/lib/request_authorization.ts b/x-pack/plugins/ml/server/lib/request_authorization.ts index 4aaeb7f611573..873d8a068ace0 100644 --- a/x-pack/plugins/ml/server/lib/request_authorization.ts +++ b/x-pack/plugins/ml/server/lib/request_authorization.ts @@ -7,7 +7,11 @@ import { KibanaRequest } from 'kibana/server'; -export function getAuthorizationHeader(request: KibanaRequest) { +export interface AuthorizationHeader { + headers?: { 'es-secondary-authorization': string | string[] }; +} + +export function getAuthorizationHeader(request: KibanaRequest): AuthorizationHeader { return request.headers.authorization === undefined ? {} : { diff --git a/x-pack/plugins/ml/server/models/job_service/index.ts b/x-pack/plugins/ml/server/models/job_service/index.ts index 94dc669bfd946..94c25581e8e84 100644 --- a/x-pack/plugins/ml/server/models/job_service/index.ts +++ b/x-pack/plugins/ml/server/models/job_service/index.ts @@ -13,16 +13,16 @@ import { newJobCapsProvider } from './new_job_caps'; import { newJobChartsProvider, topCategoriesProvider } from './new_job'; import { modelSnapshotProvider } from './model_snapshots'; import type { MlClient } from '../../lib/ml_client'; -import type { AlertsClient } from '../../../../alerting/server'; +import type { RulesClient } from '../../../../alerting/server'; export function jobServiceProvider( client: IScopedClusterClient, mlClient: MlClient, - alertsClient?: AlertsClient + rulesClient?: RulesClient ) { return { ...datafeedsProvider(client, mlClient), - ...jobsProvider(client, mlClient, alertsClient), + ...jobsProvider(client, mlClient, rulesClient), ...groupsProvider(mlClient), ...newJobCapsProvider(client), ...newJobChartsProvider(client), diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 22bac1cb08e19..ee336c96a9c0d 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -26,6 +26,7 @@ import { MlJobsResponse, MlJobsStatsResponse, JobsExistResponse, + BulkCreateResults, } from '../../../common/types/job_service'; import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; import { datafeedsProvider, MlDatafeedsResponse, MlDatafeedsStatsResponse } from './datafeeds'; @@ -40,9 +41,10 @@ import { import { groupsProvider } from './groups'; import type { MlClient } from '../../lib/ml_client'; import { isPopulatedObject } from '../../../common/util/object_utils'; -import type { AlertsClient } from '../../../../alerting/server'; +import type { RulesClient } from '../../../../alerting/server'; import { ML_ALERT_TYPES } from '../../../common/constants/alerts'; import { MlAnomalyDetectionAlertParams } from '../../routes/schemas/alerting_schema'; +import type { AuthorizationHeader } from '../../lib/request_authorization'; interface Results { [id: string]: { @@ -54,7 +56,7 @@ interface Results { export function jobsProvider( client: IScopedClusterClient, mlClient: MlClient, - alertsClient?: AlertsClient + rulesClient?: RulesClient ) { const { asInternalUser } = client; @@ -421,8 +423,8 @@ export function jobsProvider( jobs.push(tempJob); }); - if (alertsClient) { - const mlAlertingRules = await alertsClient.find({ + if (rulesClient) { + const mlAlertingRules = await rulesClient.find({ options: { filter: `alert.attributes.alertTypeId:${ML_ALERT_TYPES.ANOMALY_DETECTION}`, perPage: 1000, @@ -576,6 +578,37 @@ export function jobsProvider( return job.node === undefined && job.state === JOB_STATE.OPENING; } + async function bulkCreate( + jobs: Array<{ job: Job; datafeed: Datafeed }>, + authHeader: AuthorizationHeader + ) { + const results: BulkCreateResults = {}; + await Promise.all( + jobs.map(async ({ job, datafeed }) => { + results[job.job_id] = { job: { success: false }, datafeed: { success: false } }; + + try { + await mlClient.putJob({ job_id: job.job_id, body: job }); + results[job.job_id].job = { success: true }; + } catch (error) { + results[job.job_id].job = { success: false, error: error.body ?? error }; + } + + try { + await mlClient.putDatafeed( + { datafeed_id: datafeed.datafeed_id, body: datafeed }, + authHeader + ); + results[job.job_id].datafeed = { success: true }; + } catch (error) { + results[job.job_id].datafeed = { success: false, error: error.body ?? error }; + } + }) + ); + + return results; + } + return { forceDeleteJob, deleteJobs, @@ -589,5 +622,6 @@ export function jobsProvider( jobsExist, getAllJobAndGroupIds, getLookBackProgress, + bulkCreate, }; } diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 213be9421c41d..35f66e86b955a 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -196,7 +196,7 @@ export class MlServerPlugin initMlServerLog({ log: this.log }); - const sharedServices = createSharedServices( + const { internalServicesProviders, sharedServicesProviders } = createSharedServices( this.mlLicense, getSpaces, plugins.cloud, @@ -211,7 +211,8 @@ export class MlServerPlugin registerMlAlerts({ alerting: plugins.alerting, logger: this.log, - mlSharedServices: sharedServices, + mlSharedServices: sharedServicesProviders, + mlServicesProviders: internalServicesProviders, }); } @@ -219,7 +220,7 @@ export class MlServerPlugin registerCollector(plugins.usageCollection, this.kibanaIndexConfig.kibana.index); } - return { ...sharedServices }; + return sharedServicesProviders; } public start(coreStart: CoreStart): MlPluginStart { diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 27944b542b93f..346bf510c6c0c 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -91,6 +91,7 @@ "DeletingJobTasks", "DeleteJobs", "RevertModelSnapshot", + "BulkCreateJobs", "Calendars", "PutCalendars", diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 2ed4fd6fcd31a..bedc70566a62f 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -623,7 +623,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { try { const { analyticsIds, allSpaces } = request.body; - const results: { [id: string]: boolean } = {}; + const results: { [id: string]: { exists: boolean } } = {}; for (const id of analyticsIds) { try { const { body } = allSpaces @@ -633,17 +633,17 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout : await mlClient.getDataFrameAnalytics({ id, }); - results[id] = body.data_frame_analytics.length > 0; + results[id] = { exists: body.data_frame_analytics.length > 0 }; } catch (error) { if (error.statusCode !== 404) { throw error; } - results[id] = false; + results[id] = { exists: false }; } } return response.ok({ - body: { results }, + body: results, }); } catch (e) { return response.customError(wrapError(e)); diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 992822f6d6eb8..63310827ad989 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -16,7 +16,7 @@ import { datafeedIdsSchema, forceStartDatafeedSchema, jobIdsSchema, - optionaljobIdsSchema, + optionalJobIdsSchema, jobsWithTimerangeSchema, lookBackProgressSchema, topCategoriesSchema, @@ -24,6 +24,7 @@ import { revertModelSnapshotSchema, jobsExistSchema, datafeedPreviewSchema, + bulkCreateSchema, } from './schemas/job_service_schema'; import { jobIdSchema } from './schemas/anomaly_detectors_schema'; @@ -31,6 +32,7 @@ import { jobIdSchema } from './schemas/anomaly_detectors_schema'; import { jobServiceProvider } from '../models/job_service'; import { categorizationExamplesProvider } from '../models/job_service/new_job'; import { getAuthorizationHeader } from '../lib/request_authorization'; +import { Datafeed, Job } from '../../common/types/anomaly_detection_jobs'; /** * Routes for job service @@ -215,7 +217,7 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { * For any supplied job IDs, full job information will be returned, which include the analysis configuration, * job stats, datafeed stats, and calendars. * - * @apiSchema (body) optionaljobIdsSchema + * @apiSchema (body) optionalJobIdsSchema * * @apiSuccess {Array} jobsList list of jobs. For any supplied job IDs, the job object will contain a fullJob property * which includes the full configuration and stats for the job. @@ -224,7 +226,7 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { { path: '/api/ml/jobs/jobs_summary', validate: { - body: optionaljobIdsSchema, + body: optionalJobIdsSchema, }, options: { tags: ['access:ml:canGetJobs'], @@ -235,7 +237,7 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { const { jobsSummary } = jobServiceProvider( client, mlClient, - context.alerting?.getAlertsClient() + context.alerting?.getRulesClient() ); const { jobIds } = request.body; const resp = await jobsSummary(jobIds); @@ -323,13 +325,13 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { * @apiName CreateFullJobsList * @apiDescription Creates a list of jobs * - * @apiSchema (body) optionaljobIdsSchema + * @apiSchema (body) optionalJobIdsSchema */ router.post( { path: '/api/ml/jobs/jobs', validate: { - body: optionaljobIdsSchema, + body: optionalJobIdsSchema, }, options: { tags: ['access:ml:canGetJobs'], @@ -340,7 +342,7 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { const { createFullJobsList } = jobServiceProvider( client, mlClient, - context.alerting?.getAlertsClient() + context.alerting?.getRulesClient() ); const { jobIds } = request.body; const resp = await createFullJobsList(jobIds); @@ -878,4 +880,42 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { } }) ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/bulk_create Bulk create jobs and datafeeds + * @apiName BulkCreateJobs + * @apiDescription Bulk create jobs and datafeeds. + * + * @apiSchema (body) bulkCreateSchema + */ + router.post( + { + path: '/api/ml/jobs/bulk_create', + validate: { + body: bulkCreateSchema, + }, + options: { + tags: ['access:ml:canPreviewDatafeed'], + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { + try { + const bulkJobs = request.body; + + const { bulkCreate } = jobServiceProvider(client, mlClient); + const jobs = (Array.isArray(bulkJobs) ? bulkJobs : [bulkJobs]) as Array<{ + job: Job; + datafeed: Datafeed; + }>; + const body = await bulkCreate(jobs, getAuthorizationHeader(request)); + return response.ok({ + body, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/alerting_schema.ts b/x-pack/plugins/ml/server/routes/schemas/alerting_schema.ts index df22ccfe20821..4e0f9a9aa7c92 100644 --- a/x-pack/plugins/ml/server/routes/schemas/alerting_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/alerting_schema.ts @@ -10,22 +10,24 @@ import { i18n } from '@kbn/i18n'; import { ALERT_PREVIEW_SAMPLE_SIZE } from '../../../common/constants/alerts'; import { ANOMALY_RESULT_TYPE } from '../../../common/constants/anomalies'; -export const mlAnomalyDetectionAlertParams = schema.object({ - jobSelection: schema.object( - { - jobIds: schema.arrayOf(schema.string(), { defaultValue: [] }), - groupIds: schema.arrayOf(schema.string(), { defaultValue: [] }), +const jobsSelectionSchema = schema.object( + { + jobIds: schema.arrayOf(schema.string(), { defaultValue: [] }), + groupIds: schema.arrayOf(schema.string(), { defaultValue: [] }), + }, + { + validate: (v) => { + if (!v.jobIds?.length && !v.groupIds?.length) { + return i18n.translate('xpack.ml.alertTypes.anomalyDetection.jobSelection.errorMessage', { + defaultMessage: 'Job selection is required', + }); + } }, - { - validate: (v) => { - if (!v.jobIds?.length && !v.groupIds?.length) { - return i18n.translate('xpack.ml.alertTypes.anomalyDetection.jobSelection.errorMessage', { - defaultMessage: 'Job selection is required', - }); - } - }, - } - ), + } +); + +export const mlAnomalyDetectionAlertParams = schema.object({ + jobSelection: jobsSelectionSchema, /** Anomaly score threshold */ severity: schema.number({ min: 0, max: 100 }), /** Result type to alert upon */ @@ -58,3 +60,47 @@ export type MlAnomalyDetectionAlertParams = TypeOf; + +export const anomalyDetectionJobsHealthRuleParams = schema.object({ + includeJobs: jobsSelectionSchema, + excludeJobs: schema.nullable(jobsSelectionSchema), + testsConfig: schema.nullable( + schema.object({ + datafeed: schema.nullable( + schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }) + ), + mml: schema.nullable( + schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }) + ), + delayedData: schema.nullable( + schema.object({ + enabled: schema.boolean({ defaultValue: true }), + docsCount: schema.nullable(schema.number()), + timeInterval: schema.nullable(schema.string()), + }) + ), + behindRealtime: schema.nullable( + schema.object({ + enabled: schema.boolean({ defaultValue: true }), + timeInterval: schema.nullable(schema.string()), + }) + ), + errorMessages: schema.nullable( + schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }) + ), + }) + ), +}); + +export type AnomalyDetectionJobsHealthRuleParams = TypeOf< + typeof anomalyDetectionJobsHealthRuleParams +>; + +export type TestsConfig = AnomalyDetectionJobsHealthRuleParams['testsConfig']; +export type JobSelection = AnomalyDetectionJobsHealthRuleParams['includeJobs']; diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index df91dea101c7c..655350d367652 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -66,7 +66,7 @@ export const jobIdsSchema = schema.object({ jobIds: schema.arrayOf(schema.string()), }); -export const optionaljobIdsSchema = schema.object({ +export const optionalJobIdsSchema = schema.object({ /** Optional list of job IDs. */ jobIds: schema.maybe(schema.arrayOf(schema.string())), }); @@ -140,3 +140,16 @@ export const jobsExistSchema = schema.object({ jobIds: schema.arrayOf(schema.string()), allSpaces: schema.maybe(schema.boolean()), }); + +export const bulkCreateSchema = schema.oneOf([ + schema.arrayOf( + schema.object({ + job: schema.object(anomalyDetectionJobSchema), + datafeed: datafeedConfigSchema, + }) + ), + schema.object({ + job: schema.object(anomalyDetectionJobSchema), + datafeed: datafeedConfigSchema, + }), +]); diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index 79e579e30ed95..726d4d080ec19 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -215,7 +215,7 @@ export function systemRoutes( { path: '/api/ml/index_exists', validate: { - body: schema.object({ index: schema.string() }), + body: schema.object({ indices: schema.arrayOf(schema.string()) }), }, options: { tags: ['access:ml:canAccessML'], @@ -223,21 +223,21 @@ export function systemRoutes( }, routeGuard.basicLicenseAPIGuard(async ({ client, request, response }) => { try { - const { index } = request.body; + const { indices } = request.body; const options = { - index: [index], + index: indices, fields: ['*'], ignore_unavailable: true, allow_no_indices: true, }; const { body } = await client.asCurrentUser.fieldCaps(options); - const result = { exists: false }; - if (Array.isArray(body.indices) && body.indices.length !== 0) { - result.exists = true; - } + const result = indices.reduce((acc, cur) => { + acc[cur] = { exists: body.indices.includes(cur) }; + return acc; + }, {} as Record); return response.ok({ body: result, diff --git a/x-pack/plugins/ml/server/shared_services/shared_services.ts b/x-pack/plugins/ml/server/shared_services/shared_services.ts index caed3fd933298..3766a48b0537d 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -30,6 +30,10 @@ import { getAlertingServiceProvider, MlAlertingServiceProvider, } from './providers/alerting_service'; +import { + getJobsHealthServiceProvider, + JobsHealthServiceProvider, +} from '../lib/alerts/jobs_health_service'; export type SharedServices = JobServiceProvider & AnomalyDetectorsProvider & @@ -38,6 +42,8 @@ export type SharedServices = JobServiceProvider & ResultsServiceProvider & MlAlertingServiceProvider; +export type MlServicesProviders = JobsHealthServiceProvider; + interface Guards { isMinimumLicense(): Guards; isFullLicense(): Guards; @@ -71,7 +77,10 @@ export function createSharedServices( getClusterClient: () => IClusterClient | null, getInternalSavedObjectsClient: () => SavedObjectsClientContract | null, isMlReady: () => Promise -): SharedServices { +): { + sharedServicesProviders: SharedServices; + internalServicesProviders: MlServicesProviders; +} { const { isFullLicense, isMinimumLicense } = licenseChecks(mlLicense); function getGuards( request: KibanaRequest, @@ -118,12 +127,23 @@ export function createSharedServices( } return { - ...getJobServiceProvider(getGuards), - ...getAnomalyDetectorsProvider(getGuards), - ...getModulesProvider(getGuards), - ...getResultsServiceProvider(getGuards), - ...getMlSystemProvider(getGuards, mlLicense, getSpaces, cloud, resolveMlCapabilities), - ...getAlertingServiceProvider(getGuards), + /** + * Exposed providers for shared services used by other plugins + */ + sharedServicesProviders: { + ...getJobServiceProvider(getGuards), + ...getAnomalyDetectorsProvider(getGuards), + ...getModulesProvider(getGuards), + ...getResultsServiceProvider(getGuards), + ...getMlSystemProvider(getGuards, mlLicense, getSpaces, cloud, resolveMlCapabilities), + ...getAlertingServiceProvider(getGuards), + }, + /** + * Services providers for ML internal usage + */ + internalServicesProviders: { + ...getJobsHealthServiceProvider(getGuards), + }, }; } diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts index bd18d285e29aa..8cda6a15b5393 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts @@ -17,16 +17,16 @@ jest.mock('../static_globals', () => ({ })); describe('AlertsFactory', () => { - const alertsClient = { + const rulesClient = { find: jest.fn(), }; afterEach(() => { - alertsClient.find.mockReset(); + rulesClient.find.mockReset(); }); it('should get by type', async () => { - alertsClient.find = jest.fn().mockImplementation(() => { + rulesClient.find = jest.fn().mockImplementation(() => { return { total: 1, data: [ @@ -36,20 +36,20 @@ describe('AlertsFactory', () => { ], }; }); - const alert = await AlertsFactory.getByType(ALERT_CPU_USAGE, alertsClient as any); + const alert = await AlertsFactory.getByType(ALERT_CPU_USAGE, rulesClient as any); expect(alert).not.toBeNull(); expect(alert?.getId()).toBe(ALERT_CPU_USAGE); }); it('should pass in the correct filters', async () => { let filter = null; - alertsClient.find = jest.fn().mockImplementation(({ options }) => { + rulesClient.find = jest.fn().mockImplementation(({ options }) => { filter = options.filter; return { total: 0, }; }); - await AlertsFactory.getByType(ALERT_CPU_USAGE, alertsClient as any); + await AlertsFactory.getByType(ALERT_CPU_USAGE, rulesClient as any); expect(filter).toBe(`alert.attributes.alertTypeId:${ALERT_CPU_USAGE}`); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts index 0bb8fe39fd9a0..fad3da83e36a5 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts @@ -38,7 +38,7 @@ import { ALERT_CCR_READ_EXCEPTIONS, ALERT_LARGE_SHARD_SIZE, } from '../../common/constants'; -import { AlertsClient } from '../../../alerting/server'; +import { RulesClient } from '../../../alerting/server'; import { Alert } from '../../../alerting/common'; const BY_TYPE = { @@ -61,13 +61,13 @@ const BY_TYPE = { export class AlertsFactory { public static async getByType( type: string, - alertsClient: AlertsClient | undefined + rulesClient: RulesClient | undefined ): Promise { const alertCls = BY_TYPE[type]; - if (!alertCls || !alertsClient) { + if (!alertCls || !rulesClient) { return; } - const alertClientAlerts = await alertsClient.find({ + const alertClientAlerts = await rulesClient.find({ options: { filter: `alert.attributes.alertTypeId:${type}`, }, diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts index c89890397be7c..3fe4eac712487 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts @@ -19,7 +19,7 @@ describe('BaseAlert', () => { describe('create', () => { it('should create an alert if it does not exist', async () => { const alert = new BaseAlert(); - const alertsClient = { + const rulesClient = { create: jest.fn(), find: jest.fn().mockImplementation(() => { return { @@ -41,8 +41,8 @@ describe('BaseAlert', () => { }, ]; - await alert.createIfDoesNotExist(alertsClient as any, actionsClient as any, actions); - expect(alertsClient.create).toHaveBeenCalledWith({ + await alert.createIfDoesNotExist(rulesClient as any, actionsClient as any, actions); + expect(rulesClient.create).toHaveBeenCalledWith({ data: { actions: [ { @@ -73,7 +73,7 @@ describe('BaseAlert', () => { it('should not create an alert if it exists', async () => { const alert = new BaseAlert(); - const alertsClient = { + const rulesClient = { create: jest.fn(), find: jest.fn().mockImplementation(() => { return { @@ -96,14 +96,14 @@ describe('BaseAlert', () => { }, ]; - await alert.createIfDoesNotExist(alertsClient as any, actionsClient as any, actions); - expect(alertsClient.create).not.toHaveBeenCalled(); + await alert.createIfDoesNotExist(rulesClient as any, actionsClient as any, actions); + expect(rulesClient.create).not.toHaveBeenCalled(); }); }); describe('getStates', () => { it('should get alert states', async () => { - const alertsClient = { + const rulesClient = { getAlertState: jest.fn().mockImplementation(() => { return { alertInstances: { @@ -117,7 +117,7 @@ describe('BaseAlert', () => { const id = '456def'; const filters: any[] = []; const alert = new BaseAlert(); - const states = await alert.getStates(alertsClient as any, id, filters); + const states = await alert.getStates(rulesClient as any, id, filters); expect(states).toStrictEqual({ abc123: { id: 'foobar', @@ -126,7 +126,7 @@ describe('BaseAlert', () => { }); it('should return nothing if no states are available', async () => { - const alertsClient = { + const rulesClient = { getAlertState: jest.fn().mockImplementation(() => { return null; }), @@ -134,7 +134,7 @@ describe('BaseAlert', () => { const id = '456def'; const filters: any[] = []; const alert = new BaseAlert(); - const states = await alert.getStates(alertsClient as any, id, filters); + const states = await alert.getStates(rulesClient as any, id, filters); expect(states).toStrictEqual({}); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 0020ef779838f..7bc5d4242d0bd 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -11,7 +11,7 @@ import { AlertType, AlertExecutorOptions, AlertInstance, - AlertsClient, + RulesClient, AlertServices, } from '../../../alerting/server'; import { Alert, AlertTypeParams, RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; @@ -114,11 +114,11 @@ export class BaseAlert { } public async createIfDoesNotExist( - alertsClient: AlertsClient, + rulesClient: RulesClient, actionsClient: ActionsClient, actions: AlertEnableAction[] ): Promise> { - const existingAlertData = await alertsClient.find({ + const existingAlertData = await rulesClient.find({ options: { search: this.alertOptions.id, }, @@ -152,7 +152,7 @@ export class BaseAlert { throttle = '1d', interval = '1m', } = this.alertOptions; - return await alertsClient.create({ + return await rulesClient.create({ data: { enabled: true, tags: [], @@ -169,11 +169,11 @@ export class BaseAlert { } public async getStates( - alertsClient: AlertsClient, + rulesClient: RulesClient, id: string, filters: CommonAlertFilter[] ): Promise<{ [instanceId: string]: RawAlertInstance }> { - const states = await alertsClient.getAlertState({ id }); + const states = await rulesClient.getAlertState({ id }); if (!states || !states.alertInstances) { return {}; } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts index 0d2d9fdbed635..ffcab27568597 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts @@ -47,7 +47,7 @@ describe('fetchStatus', () => { }; let alertStates: AlertState[] = []; const licenseService = null; - const alertsClient = { + const rulesClient = { find: jest.fn(() => ({ total: 1, data: [ @@ -68,13 +68,13 @@ describe('fetchStatus', () => { }; afterEach(() => { - (alertsClient.find as jest.Mock).mockClear(); - (alertsClient.getAlertState as jest.Mock).mockClear(); + (rulesClient.find as jest.Mock).mockClear(); + (rulesClient.getAlertState as jest.Mock).mockClear(); alertStates.length = 0; }); it('should fetch from the alerts client', async () => { - const status = await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + const status = await fetchStatus(rulesClient as any, licenseService as any, alertTypes, [ defaultClusterState.clusterUuid, ]); expect(status).toEqual({ @@ -96,7 +96,7 @@ describe('fetchStatus', () => { }, ]; - const status = await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + const status = await fetchStatus(rulesClient as any, licenseService as any, alertTypes, [ defaultClusterState.clusterUuid, ]); expect(Object.values(status).length).toBe(1); @@ -105,32 +105,32 @@ describe('fetchStatus', () => { }); it('should pass in the right filter to the alerts client', async () => { - await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + await fetchStatus(rulesClient as any, licenseService as any, alertTypes, [ defaultClusterState.clusterUuid, ]); - expect((alertsClient.find as jest.Mock).mock.calls[0][0].options.filter).toBe( + expect((rulesClient.find as jest.Mock).mock.calls[0][0].options.filter).toBe( `alert.attributes.alertTypeId:${alertType}` ); }); it('should return nothing if no alert state is found', async () => { - alertsClient.getAlertState = jest.fn(() => ({ + rulesClient.getAlertState = jest.fn(() => ({ alertTypeState: null, })) as any; - const status = await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + const status = await fetchStatus(rulesClient as any, licenseService as any, alertTypes, [ defaultClusterState.clusterUuid, ]); expect(status[alertType].states.length).toEqual(0); }); it('should return nothing if no alerts are found', async () => { - alertsClient.find = jest.fn(() => ({ + rulesClient.find = jest.fn(() => ({ total: 0, data: [], })) as any; - const status = await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + const status = await fetchStatus(rulesClient as any, licenseService as any, alertTypes, [ defaultClusterState.clusterUuid, ]); expect(status).toEqual({}); @@ -145,7 +145,7 @@ describe('fetchStatus', () => { })), }; await fetchStatus( - alertsClient as any, + rulesClient as any, customLicenseService as any, [ALERT_CLUSTER_HEALTH], [defaultClusterState.clusterUuid] @@ -154,7 +154,7 @@ describe('fetchStatus', () => { }); it('should sort the alerts', async () => { - const customAlertsClient = { + const customRulesClient = { find: jest.fn(() => ({ total: 1, data: [ @@ -182,7 +182,7 @@ describe('fetchStatus', () => { })), }; const status = await fetchStatus( - customAlertsClient as any, + customRulesClient as any, licenseService as any, [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA], [defaultClusterState.clusterUuid] diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index a7b2457130744..f4fd792ddf922 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -6,7 +6,7 @@ */ import { AlertInstanceState } from '../../../common/types/alerts'; -import { AlertsClient } from '../../../../alerting/server'; +import { RulesClient } from '../../../../alerting/server'; import { AlertsFactory } from '../../alerts'; import { CommonAlertStatus, @@ -17,7 +17,7 @@ import { ALERTS } from '../../../common/constants'; import { MonitoringLicenseService } from '../../types'; export async function fetchStatus( - alertsClient: AlertsClient, + rulesClient: RulesClient, licenseService: MonitoringLicenseService, alertTypes: string[] | undefined, clusterUuids: string[], @@ -27,7 +27,7 @@ export async function fetchStatus( const byType: { [type: string]: CommonAlertStatus } = {}; await Promise.all( (alertTypes || ALERTS).map(async (type) => { - const alert = await AlertsFactory.getByType(type, alertsClient); + const alert = await AlertsFactory.getByType(type, rulesClient); if (!alert || !alert.rawAlert) { return; } @@ -45,7 +45,7 @@ export async function fetchStatus( } // Now that we have the id, we can get the state - const states = await alert.getStates(alertsClient, id, filters); + const states = await alert.getStates(rulesClient, id, filters); if (!states) { return result; } diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index 77b71d3e92f4c..9ef309cee7312 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -117,16 +117,16 @@ export async function getClustersFromRequest( // add alerts data if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { - const alertsClient = req.getAlertsClient(); + const rulesClient = req.getRulesClient(); const alertStatus = await fetchStatus( - alertsClient, + rulesClient, req.server.plugins.monitoring.info, undefined, clusters.map((cluster) => get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid)) ); for (const cluster of clusters) { - if (!alertsClient) { + if (!rulesClient) { cluster.alerts = { list: {}, alertsMeta: { diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index ca8d87bd300a0..aed48b7391529 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -347,9 +347,9 @@ export class MonitoringPlugin getKibanaStatsCollector: () => this.legacyShimDependencies.kibanaStatsCollector, getUiSettingsService: () => context.core.uiSettings.client, getActionTypeRegistry: () => context.actions?.listTypes(), - getAlertsClient: () => { + getRulesClient: () => { try { - return plugins.alerting.getAlertsClientWithRequest(req); + return plugins.alerting.getRulesClientWithRequest(req); } catch (err) { // If security is disabled, this call will throw an error unless a certain config is set for dist builds return null; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index e21304e8458e3..ef6e03e7c999c 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -56,10 +56,10 @@ export function enableAlertsRoute(server: LegacyServer, npRoute: RouteDependenci } } - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const actionsClient = context.actions?.getActionsClient(); const types = context.actions?.listTypes(); - if (!alertsClient || !actionsClient || !types) { + if (!rulesClient || !actionsClient || !types) { return response.ok({ body: undefined }); } @@ -99,7 +99,7 @@ export function enableAlertsRoute(server: LegacyServer, npRoute: RouteDependenci if (disabledWatcherClusterAlerts) { createdAlerts = await Promise.all( - alerts.map((alert) => alert.createIfDoesNotExist(alertsClient, actionsClient, actions)) + alerts.map((alert) => alert.createIfDoesNotExist(rulesClient, actionsClient, actions)) ); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts index 95e2cb63bec86..f77630e5d61a5 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts @@ -34,13 +34,13 @@ export function alertStatusRoute(server: any, npRoute: RouteDependencies) { try { const { clusterUuid } = request.params; const { alertTypeIds, filters } = request.body; - const alertsClient = context.alerting?.getAlertsClient(); - if (!alertsClient) { + const rulesClient = context.alerting?.getRulesClient(); + if (!rulesClient) { return response.ok({ body: undefined }); } const status = await fetchStatus( - alertsClient, + rulesClient, npRoute.licenseService, alertTypeIds, [clusterUuid], diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index b920f2bfacf80..07e56f0c00232 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -115,7 +115,7 @@ export interface LegacyRequest { getKibanaStatsCollector: () => any; getUiSettingsService: () => any; getActionTypeRegistry: () => any; - getAlertsClient: () => any; + getRulesClient: () => any; getActionsClient: () => any; server: LegacyServer; } diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 6bd96e012548d..f4b3754e4253e 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -8,6 +8,7 @@ ], "optionalPlugins": [ "home", + "discover", "lens", "licensing", "usageCollection" diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx new file mode 100644 index 0000000000000..0e17c6277618b --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiHeaderLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useKibana } from '../../../utils/kibana_react'; + +export function MobileAddData() { + const kibana = useKibana(); + + return ( + + {ADD_DATA_LABEL} + + ); +} + +const ADD_DATA_LABEL = i18n.translate('xpack.observability.mobile.addDataButtonLabel', { + defaultMessage: 'Add Mobile data', +}); diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx new file mode 100644 index 0000000000000..af91624769e6b --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiHeaderLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useKibana } from '../../../utils/kibana_react'; + +export function SyntheticsAddData() { + const kibana = useKibana(); + + return ( + + {ADD_DATA_LABEL} + + ); +} + +const ADD_DATA_LABEL = i18n.translate('xpack.observability..synthetics.addDataButtonLabel', { + defaultMessage: 'Add synthetics data', +}); diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx new file mode 100644 index 0000000000000..c6aa0742466f1 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiHeaderLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useKibana } from '../../../utils/kibana_react'; + +export function UXAddData() { + const kibana = useKibana(); + + return ( + + {ADD_DATA_LABEL} + + ); +} + +const ADD_DATA_LABEL = i18n.translate('xpack.observability.ux.addDataButtonLabel', { + defaultMessage: 'Add UX data', +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx new file mode 100644 index 0000000000000..329192abc99d2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '../../rtl_helpers'; +import { fireEvent, screen } from '@testing-library/dom'; +import React from 'react'; +import { sampleAttribute } from '../../configurations/test_data/sample_attribute'; +import * as pluginHook from '../../../../../hooks/use_plugin_context'; +import { TypedLensByValueInput } from '../../../../../../../lens/public'; +import { ExpViewActionMenuContent } from './action_menu'; + +jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ + appMountParameters: { + setHeaderActionMenu: jest.fn(), + }, +} as any); + +describe('Action Menu', function () { + it('should be able to click open in lens', async function () { + const { findByText, core } = render( + + ); + + expect(await screen.findByText('Open in Lens')).toBeInTheDocument(); + + fireEvent.click(await findByText('Open in Lens')); + + expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); + expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith( + { + id: '', + attributes: sampleAttribute, + timeRange: { to: 'now', from: 'now-10m' }, + }, + true + ); + }); + + it('should be able to click save', async function () { + const { findByText } = render( + + ); + + expect(await screen.findByText('Save')).toBeInTheDocument(); + + fireEvent.click(await findByText('Save')); + + expect(await screen.findByText('Lens Save Modal Component')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx new file mode 100644 index 0000000000000..38011eb5f8ffb --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { LensEmbeddableInput, TypedLensByValueInput } from '../../../../../../../lens/public'; +import { ObservabilityAppServices } from '../../../../../application/types'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; + +export function ExpViewActionMenuContent({ + timeRange, + lensAttributes, +}: { + timeRange?: { from: string; to: string }; + lensAttributes: TypedLensByValueInput['attributes'] | null; +}) { + const kServices = useKibana().services; + + const { lens } = kServices; + + const [isSaveOpen, setIsSaveOpen] = useState(false); + + const LensSaveModalComponent = lens.SaveModalComponent; + + return ( + <> + + + { + if (lensAttributes) { + lens.navigateToPrefilledEditor( + { + id: '', + timeRange, + attributes: lensAttributes, + }, + true + ); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.openInLens', { + defaultMessage: 'Open in Lens', + })} + + + + { + if (lensAttributes) { + setIsSaveOpen(true); + } + }} + size="s" + > + {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', { + defaultMessage: 'Save', + })} + + + + + {isSaveOpen && lensAttributes && ( + setIsSaveOpen(false)} + // if we want to do anything after the viz is saved + // right now there is no action, so an empty function + onSave={() => {}} + /> + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx new file mode 100644 index 0000000000000..23500b63e900a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.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 React from 'react'; +import { ExpViewActionMenuContent } from './action_menu'; +import HeaderMenuPortal from '../../../header_menu_portal'; +import { usePluginContext } from '../../../../../hooks/use_plugin_context'; +import { TypedLensByValueInput } from '../../../../../../../lens/public'; + +interface Props { + timeRange?: { from: string; to: string }; + lensAttributes: TypedLensByValueInput['attributes'] | null; +} +export function ExpViewActionMenu(props: Props) { + const { appMountParameters } = usePluginContext(); + + return ( + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx similarity index 58% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx index c30863585b3b0..0b8e1c1785c7f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx @@ -6,48 +6,48 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; -import DateMath from '@elastic/datemath'; import { Moment } from 'moment'; +import DateMath from '@elastic/datemath'; +import { i18n } from '@kbn/i18n'; import { useSeriesStorage } from '../hooks/use_series_storage'; import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; +import { SeriesUrl } from '../types'; +import { ReportTypes } from '../configurations/constants'; export const parseAbsoluteDate = (date: string, options = {}) => { return DateMath.parse(date, options)!; }; -export function DateRangePicker({ seriesId }: { seriesId: string }) { - const { firstSeriesId, getSeries, setSeries } = useSeriesStorage(); +export function DateRangePicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) { + const { firstSeries, setSeries, reportType } = useSeriesStorage(); const dateFormat = useUiSetting('dateFormat'); - const { - time: { from, to }, - reportType, - } = getSeries(firstSeriesId); + const seriesFrom = series.time?.from; + const seriesTo = series.time?.to; - const series = getSeries(seriesId); + const { from: mainFrom, to: mainTo } = firstSeries!.time; - const { - time: { from: seriesFrom, to: seriesTo }, - } = series; + const startDate = parseAbsoluteDate(seriesFrom ?? mainFrom)!; + const endDate = parseAbsoluteDate(seriesTo ?? mainTo, { roundUp: true })!; - const startDate = parseAbsoluteDate(seriesFrom ?? from)!; - const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!; + const getTotalDuration = () => { + const mainStartDate = parseAbsoluteDate(mainTo)!; + const mainEndDate = parseAbsoluteDate(mainTo, { roundUp: true })!; + return mainEndDate.diff(mainStartDate, 'millisecond'); + }; - const onStartChange = (newDate: Moment) => { - if (reportType === 'kpi-over-time') { - const mainStartDate = parseAbsoluteDate(from)!; - const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; - const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); - const newFrom = newDate.toISOString(); - const newTo = newDate.add(totalDuration, 'millisecond').toISOString(); + const onStartChange = (newStartDate: Moment) => { + if (reportType === ReportTypes.KPI) { + const totalDuration = getTotalDuration(); + const newFrom = newStartDate.toISOString(); + const newTo = newStartDate.add(totalDuration, 'millisecond').toISOString(); setSeries(seriesId, { ...series, time: { from: newFrom, to: newTo }, }); } else { - const newFrom = newDate.toISOString(); + const newFrom = newStartDate.toISOString(); setSeries(seriesId, { ...series, @@ -55,20 +55,19 @@ export function DateRangePicker({ seriesId }: { seriesId: string }) { }); } }; - const onEndChange = (newDate: Moment) => { - if (reportType === 'kpi-over-time') { - const mainStartDate = parseAbsoluteDate(from)!; - const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; - const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); - const newTo = newDate.toISOString(); - const newFrom = newDate.subtract(totalDuration, 'millisecond').toISOString(); + + const onEndChange = (newEndDate: Moment) => { + if (reportType === ReportTypes.KPI) { + const totalDuration = getTotalDuration(); + const newTo = newEndDate.toISOString(); + const newFrom = newEndDate.subtract(totalDuration, 'millisecond').toISOString(); setSeries(seriesId, { ...series, time: { from: newFrom, to: newTo }, }); } else { - const newTo = newDate.toISOString(); + const newTo = newEndDate.toISOString(); setSeries(seriesId, { ...series, @@ -90,7 +89,7 @@ export function DateRangePicker({ seriesId }: { seriesId: string }) { aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', { defaultMessage: 'Start date', })} - dateFormat={dateFormat} + dateFormat={dateFormat.replace('ss.SSS', 'ss')} showTimeSelect /> } @@ -104,7 +103,7 @@ export function DateRangePicker({ seriesId }: { seriesId: string }) { aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', { defaultMessage: 'End date', })} - dateFormat={dateFormat} + dateFormat={dateFormat.replace('ss.SSS', 'ss')} showTimeSelect /> } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx index 3566835b1701c..d17e451ef702c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx @@ -10,19 +10,19 @@ import { isEmpty } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { LOADING_VIEW } from '../series_builder/series_builder'; -import { SeriesUrl } from '../types'; +import { LOADING_VIEW } from '../series_editor/series_editor'; +import { ReportViewType, SeriesUrl } from '../types'; export function EmptyView({ loading, - height, series, + reportType, }: { loading: boolean; - height: string; - series: SeriesUrl; + series?: SeriesUrl; + reportType: ReportViewType; }) { - const { dataType, reportType, reportDefinitions } = series ?? {}; + const { dataType, reportDefinitions } = series ?? {}; let emptyMessage = EMPTY_LABEL; @@ -45,7 +45,7 @@ export function EmptyView({ } return ( - + {loading && ( ` +const Wrapper = styled.div` text-align: center; - height: ${(props) => props.height}; position: relative; `; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx index fe2953edd36d6..03fd23631f755 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; +import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; import { FilterLabel } from './filter_label'; import * as useSeriesHook from '../hooks/use_series_filters'; import { buildFilterLabel } from '../../filter_value_label/filter_value_label'; @@ -27,9 +27,10 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={'kpi-over-time'} + seriesId={0} removeFilter={jest.fn()} indexPattern={mockIndexPattern} + series={mockUxSeries} /> ); @@ -51,9 +52,10 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={'kpi-over-time'} + seriesId={0} removeFilter={removeFilter} indexPattern={mockIndexPattern} + series={mockUxSeries} /> ); @@ -74,9 +76,10 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={'kpi-over-time'} + seriesId={0} removeFilter={removeFilter} indexPattern={mockIndexPattern} + series={mockUxSeries} /> ); @@ -100,9 +103,10 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={true} - seriesId={'kpi-over-time'} + seriesId={0} removeFilter={jest.fn()} indexPattern={mockIndexPattern} + series={mockUxSeries} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx index a08e777c5ea71..c6254a85de9ac 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx @@ -9,21 +9,24 @@ import React from 'react'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useSeriesFilters } from '../hooks/use_series_filters'; import { FilterValueLabel } from '../../filter_value_label/filter_value_label'; +import { SeriesUrl } from '../types'; interface Props { field: string; label: string; - value: string; - seriesId: string; + value: string | string[]; + seriesId: number; + series: SeriesUrl; negate: boolean; definitionFilter?: boolean; indexPattern: IndexPattern; - removeFilter: (field: string, value: string, notVal: boolean) => void; + removeFilter: (field: string, value: string | string[], notVal: boolean) => void; } export function FilterLabel({ label, seriesId, + series, field, value, negate, @@ -31,7 +34,7 @@ export function FilterLabel({ removeFilter, definitionFilter, }: Props) { - const { invertFilter } = useSeriesFilters({ seriesId }); + const { invertFilter } = useSeriesFilters({ seriesId, series }); return indexPattern ? ( { + setSeries(seriesId, { ...series, color: colorN }); + }; + + const color = + series.color ?? ((theme.eui as unknown) as Record)[`euiColorVis${seriesId}`]; + + const button = ( + + setIsOpen((prevState) => !prevState)} hasArrow={false}> + + + + ); + + return ( + setIsOpen(false)}> + + + + + ); +} + +const PICK_A_COLOR_LABEL = i18n.translate( + 'xpack.observability.overview.exploratoryView.pickColor', + { + defaultMessage: 'Pick a color', + } +); + +const EDIT_SERIES_COLOR_LABEL = i18n.translate( + 'xpack.observability.overview.exploratoryView.editSeriesColor', + { + defaultMessage: 'Edit color for series', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx new file mode 100644 index 0000000000000..23d6589fecbcb --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { EuiSuperDatePicker, EuiText } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { useHasData } from '../../../../../hooks/use_has_data'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { useQuickTimeRanges } from '../../../../../hooks/use_quick_time_ranges'; +import { parseTimeParts } from '../../series_viewer/columns/utils'; +import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { SeriesUrl } from '../../types'; +import { ReportTypes } from '../../configurations/constants'; + +export interface TimePickerTime { + from: string; + to: string; +} + +export interface TimePickerQuickRange extends TimePickerTime { + display: string; +} + +interface Props { + seriesId: number; + series: SeriesUrl; + readonly?: boolean; +} +const readableUnit: Record = { + m: i18n.translate('xpack.observability.overview.exploratoryView.minutes', { + defaultMessage: 'Minutes', + }), + h: i18n.translate('xpack.observability.overview.exploratoryView.hour', { + defaultMessage: 'Hour', + }), + d: i18n.translate('xpack.observability.overview.exploratoryView.day', { + defaultMessage: 'Day', + }), +}; + +export function SeriesDatePicker({ series, seriesId, readonly = true }: Props) { + const { onRefreshTimeRange } = useHasData(); + + const commonlyUsedRanges = useQuickTimeRanges(); + + const { setSeries, reportType, allSeries, firstSeries } = useSeriesStorage(); + + function onTimeChange({ start, end }: { start: string; end: string }) { + onRefreshTimeRange(); + if (reportType === ReportTypes.KPI) { + allSeries.forEach((currSeries, seriesIndex) => { + setSeries(seriesIndex, { ...currSeries, time: { from: start, to: end } }); + }); + } else { + setSeries(seriesId, { ...series, time: { from: start, to: end } }); + } + } + + const seriesTime = series.time ?? firstSeries!.time; + + const dateFormat = useUiSetting('dateFormat').replace('ss.SSS', 'ss'); + + if (readonly) { + const timeParts = parseTimeParts(seriesTime?.from, seriesTime?.to); + + if (timeParts) { + const { + timeTense: timeTenseDefault, + timeUnits: timeUnitsDefault, + timeValue: timeValueDefault, + } = timeParts; + + return ( + {`${timeTenseDefault} ${timeValueDefault} ${ + readableUnit?.[timeUnitsDefault] ?? timeUnitsDefault + }`} + ); + } else { + return ( + + {i18n.translate('xpack.observability.overview.exploratoryView.dateRangeReadonly', { + defaultMessage: '{start} to {end}', + values: { + start: moment(seriesTime.from).format(dateFormat), + end: moment(seriesTime.to).format(dateFormat), + }, + })} + + ); + } + } + + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx similarity index 50% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx index 931dfbe07cd23..3517508300e4b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx @@ -6,67 +6,48 @@ */ import React from 'react'; -import { mockUseHasData, render } from '../rtl_helpers'; +import { mockUseHasData, render } from '../../rtl_helpers'; import { fireEvent, waitFor } from '@testing-library/react'; import { SeriesDatePicker } from './index'; -import { DEFAULT_TIME } from '../configurations/constants'; describe('SeriesDatePicker', function () { it('should render properly', function () { const initSeries = { - data: { - 'uptime-pings-histogram': { + data: [ + { + name: 'uptime-pings-histogram', dataType: 'synthetics' as const, - reportType: 'data-distribution' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, - }, + ], }; - const { getByText } = render(, { initSeries }); - - getByText('Last 30 minutes'); - }); - - it('should set defaults', async function () { - const initSeries = { - data: { - 'uptime-pings-histogram': { - reportType: 'kpi-over-time' as const, - dataType: 'synthetics' as const, - breakdown: 'monitor.status', - }, - }, - }; - const { setSeries: setSeries1 } = render( - , - { initSeries: initSeries as any } - ); - expect(setSeries1).toHaveBeenCalledTimes(1); - expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', { - breakdown: 'monitor.status', - dataType: 'synthetics' as const, - reportType: 'kpi-over-time' as const, - time: DEFAULT_TIME, + const { getByText } = render(, { + initSeries, }); + + getByText('Last 30 Minutes'); }); it('should set series data', async function () { const initSeries = { - data: { - 'uptime-pings-histogram': { + data: [ + { + name: 'uptime-pings-histogram', dataType: 'synthetics' as const, - reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, - }, + ], }; const { onRefreshTimeRange } = mockUseHasData(); - const { getByTestId, setSeries } = render(, { - initSeries, - }); + const { getByTestId, setSeries } = render( + , + { + initSeries, + } + ); await waitFor(function () { fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton')); @@ -76,10 +57,10 @@ describe('SeriesDatePicker', function () { expect(onRefreshTimeRange).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith('series-id', { + expect(setSeries).toHaveBeenCalledWith(0, { + name: 'uptime-pings-histogram', breakdown: 'monitor.status', dataType: 'synthetics', - reportType: 'kpi-over-time', time: { from: 'now/d', to: 'now/d' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index ba1f2214223e3..bf5feb7d5863c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -94,6 +94,19 @@ export const DataViewLabels: Record = { 'device-data-distribution': DEVICE_DISTRIBUTION_LABEL, }; +export enum ReportTypes { + KPI = 'kpi-over-time', + DISTRIBUTION = 'data-distribution', + CORE_WEB_VITAL = 'core-web-vitals', + DEVICE_DISTRIBUTION = 'device-data-distribution', +} + +export enum DataTypes { + SYNTHETICS = 'synthetics', + UX = 'ux', + MOBILE = 'mobile', +} + export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; export const FILTER_RECORDS = 'FILTER_RECORDS'; export const TERMS_COLUMN = 'TERMS_COLUMN'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts index 6f990015fbc62..55ac75b47c056 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts @@ -8,10 +8,12 @@ export enum URL_KEYS { DATA_TYPE = 'dt', OPERATION_TYPE = 'op', - REPORT_TYPE = 'rt', SERIES_TYPE = 'st', BREAK_DOWN = 'bd', FILTERS = 'ft', REPORT_DEFINITIONS = 'rdf', SELECTED_METRIC = 'mt', + HIDDEN = 'h', + NAME = 'n', + COLOR = 'c', } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 574a9f6a2bc10..3f6551986527c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -15,6 +15,7 @@ import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config'; import { getMobileKPIConfig } from './mobile/kpi_over_time_config'; import { getMobileKPIDistributionConfig } from './mobile/distribution_config'; import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config'; +import { DataTypes, ReportTypes } from './constants'; interface Props { reportType: ReportViewType; @@ -24,24 +25,24 @@ interface Props { export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => { switch (dataType) { - case 'ux': - if (reportType === 'data-distribution') { + case DataTypes.UX: + if (reportType === ReportTypes.DISTRIBUTION) { return getRumDistributionConfig({ indexPattern }); } - if (reportType === 'core-web-vitals') { + if (reportType === ReportTypes.CORE_WEB_VITAL) { return getCoreWebVitalsConfig({ indexPattern }); } return getKPITrendsLensConfig({ indexPattern }); - case 'synthetics': - if (reportType === 'data-distribution') { + case DataTypes.SYNTHETICS: + if (reportType === ReportTypes.DISTRIBUTION) { return getSyntheticsDistributionConfig({ indexPattern }); } return getSyntheticsKPIConfig({ indexPattern }); - case 'mobile': - if (reportType === 'data-distribution') { + case DataTypes.MOBILE: + if (reportType === ReportTypes.DISTRIBUTION) { return getMobileKPIDistributionConfig({ indexPattern }); } - if (reportType === 'device-data-distribution') { + if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { return getMobileDeviceDistributionConfig({ indexPattern }); } return getMobileKPIConfig({ indexPattern }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index ae70bbdcfa3b8..08d2da4714e47 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -16,7 +16,7 @@ import { } from './constants/elasticsearch_fieldnames'; import { buildExistsFilter, buildPhrasesFilter } from './utils'; import { sampleAttributeKpi } from './test_data/sample_attribute_kpi'; -import { REPORT_METRIC_FIELD } from './constants'; +import { RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from './constants'; describe('Lens Attribute', () => { mockAppIndexPattern(); @@ -38,6 +38,9 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: {}, time: { from: 'now-15m', to: 'now' }, + color: 'green', + name: 'test-series', + selectedMetricField: TRANSACTION_DURATION, }; beforeEach(() => { @@ -50,7 +53,7 @@ describe('Lens Attribute', () => { it('should return expected json for kpi report type', function () { const seriesConfigKpi = getDefaultConfigs({ - reportType: 'kpi-over-time', + reportType: ReportTypes.KPI, dataType: 'ux', indexPattern: mockIndexPattern, }); @@ -63,6 +66,9 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'service.name': ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, + color: 'green', + name: 'test-series', + selectedMetricField: RECORDS_FIELD, }, ]); @@ -135,6 +141,9 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'performance.metric': [LCP_FIELD] }, time: { from: 'now-15m', to: 'now' }, + color: 'green', + name: 'test-series', + selectedMetricField: TRANSACTION_DURATION, }; lnsAttr = new LensAttributes([layerConfig1]); @@ -277,7 +286,7 @@ describe('Lens Attribute', () => { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'Pages loaded', + label: 'test-series', operationType: 'formula', params: { format: { @@ -383,7 +392,7 @@ describe('Lens Attribute', () => { palette: undefined, seriesType: 'line', xAccessor: 'x-axis-column-layer0', - yConfig: [{ forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], }, ], legend: { isVisible: true, position: 'right' }, @@ -403,6 +412,9 @@ describe('Lens Attribute', () => { reportDefinitions: { 'performance.metric': [LCP_FIELD] }, breakdown: USER_AGENT_NAME, time: { from: 'now-15m', to: 'now' }, + color: 'green', + name: 'test-series', + selectedMetricField: TRANSACTION_DURATION, }; lnsAttr = new LensAttributes([layerConfig1]); @@ -422,7 +434,7 @@ describe('Lens Attribute', () => { seriesType: 'line', splitAccessor: 'breakdown-column-layer0', xAccessor: 'x-axis-column-layer0', - yConfig: [{ forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], }, ]); @@ -483,7 +495,7 @@ describe('Lens Attribute', () => { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'Pages loaded', + label: 'test-series', operationType: 'formula', params: { format: { @@ -589,6 +601,9 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'performance.metric': [LCP_FIELD] }, time: { from: 'now-15m', to: 'now' }, + color: 'green', + name: 'test-series', + selectedMetricField: TRANSACTION_DURATION, }; const filters = lnsAttr.getLayerFilters(layerConfig1, 2); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index dfb17ee470d35..5426d3bcd4233 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { capitalize } from 'lodash'; + import { CountIndexPatternColumn, DateHistogramIndexPatternColumn, @@ -36,10 +37,11 @@ import { REPORT_METRIC_FIELD, RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD, + ReportTypes, } from './constants'; import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types'; import { PersistableFilter } from '../../../../../../lens/common'; -import { parseAbsoluteDate } from '../series_date_picker/date_range_picker'; +import { parseAbsoluteDate } from '../components/date_range_picker'; import { getDistributionInPercentageColumn } from './lens_columns/overall_column'; function getLayerReferenceName(layerId: string) { @@ -73,14 +75,6 @@ export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricF timeScale = currField?.timeScale; columnLabel = currField?.label; } - } else if (metricOptions?.[0].field || metricOptions?.[0].id) { - const firstMetricOption = metricOptions?.[0]; - - selectedMetricField = firstMetricOption.field || firstMetricOption.id; - columnType = firstMetricOption.columnType; - columnFilters = firstMetricOption.columnFilters; - timeScale = firstMetricOption.timeScale; - columnLabel = firstMetricOption.label; } return { fieldName: selectedMetricField!, columnType, columnFilters, timeScale, columnLabel }; @@ -95,7 +89,9 @@ export interface LayerConfig { reportDefinitions: URLReportDefinition; time: { to: string; from: string }; indexPattern: IndexPattern; - selectedMetricField?: string; + selectedMetricField: string; + color: string; + name: string; } export class LensAttributes { @@ -471,14 +467,15 @@ export class LensAttributes { getLayerFilters(layerConfig: LayerConfig, totalLayers: number) { const { filters, - time: { from, to }, + time, seriesConfig: { baseFilters: layerFilters, reportType }, } = layerConfig; let baseFilters = ''; - if (reportType !== 'kpi-over-time' && totalLayers > 1) { + + if (reportType !== ReportTypes.KPI && totalLayers > 1 && time) { // for kpi over time, we don't need to add time range filters // since those are essentially plotted along the x-axis - baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`; + baseFilters += `@timestamp >= ${time.from} and @timestamp <= ${time.to}`; } layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => { @@ -534,7 +531,11 @@ export class LensAttributes { } getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) { - if (index === 0 || mainLayerConfig.seriesConfig.reportType !== 'kpi-over-time') { + if ( + index === 0 || + mainLayerConfig.seriesConfig.reportType !== ReportTypes.KPI || + !layerConfig.time + ) { return null; } @@ -546,11 +547,14 @@ export class LensAttributes { time: { from }, } = layerConfig; - const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'); + const inDays = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days')); if (inDays > 1) { return inDays + 'd'; } - const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'); + const inHours = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours')); + if (inHours === 0) { + return null; + } return inHours + 'h'; } @@ -568,6 +572,12 @@ export class LensAttributes { const { sourceField } = seriesConfig.xAxisColumn; + let label = timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label; + + if (layerConfig.seriesConfig.reportType !== ReportTypes.CORE_WEB_VITAL && layerConfig.name) { + label = layerConfig.name; + } + layers[layerId] = { columnOrder: [ `x-axis-column-${layerId}`, @@ -581,7 +591,7 @@ export class LensAttributes { [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId), [`y-axis-column-${layerId}`]: { ...mainYAxis, - label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label, + label, filter: { query: columnFilter, language: 'kuery' }, ...(timeShift ? { timeShift } : {}), }, @@ -624,7 +634,7 @@ export class LensAttributes { seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType, palette: layerConfig.seriesConfig.palette, yConfig: layerConfig.seriesConfig.yConfig || [ - { forAccessor: `y-axis-column-layer${index}` }, + { forAccessor: `y-axis-column-layer${index}`, color: layerConfig.color }, ], xAccessor: `x-axis-column-layer${index}`, ...(layerConfig.breakdown && @@ -638,7 +648,7 @@ export class LensAttributes { }; } - getJSON(): TypedLensByValueInput['attributes'] { + getJSON(refresh?: number): TypedLensByValueInput['attributes'] { const uniqueIndexPatternsIds = Array.from( new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)]) ); @@ -647,7 +657,7 @@ export class LensAttributes { return { title: 'Prefilled from exploratory view app', - description: '', + description: String(refresh), visualizationType: 'lnsXY', references: [ ...uniqueIndexPatternsIds.map((patternId) => ({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index d1612a08f5551..4e178bba7e02a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, REPORT_METRIC_FIELD, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, ReportTypes, USE_BREAK_DOWN_COLUMN } from '../constants'; import { buildPhraseFilter } from '../utils'; import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames'; import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels'; @@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: 'device-data-distribution', + reportType: ReportTypes.DEVICE_DISTRIBUTION, defaultSeriesType: 'bar', seriesTypes: ['bar', 'bar_horizontal'], xAxisColumn: { @@ -38,13 +38,13 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) ...MobileFields, [SERVICE_NAME]: MOBILE_APP, }, + definitionFields: [SERVICE_NAME], metricOptions: [ { - id: 'labels.device_id', field: 'labels.device_id', + id: 'labels.device_id', label: NUMBER_OF_DEVICES, }, ], - definitionFields: [SERVICE_NAME], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts index 9b1c4c8da3e9b..1da27be4fcc95 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; +import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -21,7 +21,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: 'data-distribution', + reportType: ReportTypes.DISTRIBUTION, defaultSeriesType: 'bar', seriesTypes: ['line', 'bar'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts index 945a631078a33..3ee5b3125fcda 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts @@ -6,7 +6,13 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; +import { + FieldLabels, + OPERATION_COLUMN, + RECORDS_FIELD, + REPORT_METRIC_FIELD, + ReportTypes, +} from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -26,7 +32,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: 'kpi-over-time', + reportType: ReportTypes.KPI, defaultSeriesType: 'line', seriesTypes: ['line', 'bar', 'area'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts index 07bb13f957e45..35e094996f6f2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts @@ -9,7 +9,7 @@ import { mockAppIndexPattern, mockIndexPattern } from '../../rtl_helpers'; import { getDefaultConfigs } from '../default_configs'; import { LayerConfig, LensAttributes } from '../lens_attributes'; import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv'; -import { SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; +import { LCP_FIELD, SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; describe('Core web vital config test', function () { mockAppIndexPattern(); @@ -24,10 +24,13 @@ describe('Core web vital config test', function () { const layerConfig: LayerConfig = { seriesConfig, + color: 'green', + name: 'test-series', + breakdown: USER_AGENT_OS, indexPattern: mockIndexPattern, - reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, - breakdown: USER_AGENT_OS, + reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, + selectedMetricField: LCP_FIELD, }; beforeEach(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts index 62455df248085..e8d620388a89e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts @@ -11,6 +11,7 @@ import { FieldLabels, FILTER_RECORDS, REPORT_METRIC_FIELD, + ReportTypes, USE_BREAK_DOWN_COLUMN, } from '../constants'; import { buildPhraseFilter } from '../utils'; @@ -38,7 +39,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon return { defaultSeriesType: 'bar_horizontal_percentage_stacked', - reportType: 'core-web-vitals', + reportType: ReportTypes.CORE_WEB_VITAL, seriesTypes: ['bar_horizontal_percentage_stacked'], xAxisColumn: { sourceField: USE_BREAK_DOWN_COLUMN, @@ -153,5 +154,6 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon { color: statusPallete[1], forAccessor: 'y-axis-column-1' }, { color: statusPallete[2], forAccessor: 'y-axis-column-2' }, ], + query: { query: 'transaction.type: "page-load"', language: 'kuery' }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts index f34c8db6c197d..de6f2c67b2aeb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts @@ -6,7 +6,12 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants'; +import { + FieldLabels, + REPORT_METRIC_FIELD, + RECORDS_PERCENTAGE_FIELD, + ReportTypes, +} from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -41,7 +46,7 @@ import { export function getRumDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: 'data-distribution', + reportType: ReportTypes.DISTRIBUTION, defaultSeriesType: 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts index 5899b16d12b4f..9112778eadaa7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts @@ -6,7 +6,13 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; +import { + FieldLabels, + OPERATION_COLUMN, + RECORDS_FIELD, + REPORT_METRIC_FIELD, + ReportTypes, +} from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -43,7 +49,7 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesCon return { defaultSeriesType: 'bar_stacked', seriesTypes: [], - reportType: 'kpi-over-time', + reportType: ReportTypes.KPI, xAxisColumn: { sourceField: '@timestamp', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index 730e742f9d8c5..da90f45d15201 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -6,7 +6,12 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants'; +import { + FieldLabels, + REPORT_METRIC_FIELD, + RECORDS_PERCENTAGE_FIELD, + ReportTypes, +} from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -30,7 +35,7 @@ export function getSyntheticsDistributionConfig({ indexPattern, }: ConfigProps): SeriesConfig { return { - reportType: 'data-distribution', + reportType: ReportTypes.DISTRIBUTION, defaultSeriesType: series?.seriesType || 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 4ee22181d4334..65b43a83a8fb5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD } from '../constants'; +import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -30,7 +30,7 @@ const SUMMARY_DOWN = 'summary.down'; export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: 'kpi-over-time', + reportType: ReportTypes.KPI, defaultSeriesType: 'bar_stacked', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index 569d68ad4ebff..a5898f33e0ec0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -5,12 +5,18 @@ * 2.0. */ export const sampleAttribute = { - title: 'Prefilled from exploratory view app', - description: '', - visualizationType: 'lnsXY', + description: 'undefined', references: [ - { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, - { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, + { + id: 'apm-*', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: 'apm-*', + name: 'indexpattern-datasource-layer-layer0', + type: 'index-pattern', + }, ], state: { datasourceStates: { @@ -28,17 +34,23 @@ export const sampleAttribute = { ], columns: { 'x-axis-column-layer0': { - sourceField: 'transaction.duration.us', - label: 'Page load time', dataType: 'number', - operationType: 'range', isBucketed: true, - scale: 'interval', + label: 'Page load time', + operationType: 'range', params: { - type: 'histogram', - ranges: [{ from: 0, to: 1000, label: '' }], maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', }, + scale: 'interval', + sourceField: 'transaction.duration.us', }, 'y-axis-column-layer0': { dataType: 'number', @@ -48,7 +60,7 @@ export const sampleAttribute = { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'Pages loaded', + label: 'test-series', operationType: 'formula', params: { format: { @@ -81,16 +93,16 @@ export const sampleAttribute = { 'y-axis-column-layer0X1': { customLabel: true, dataType: 'number', - isBucketed: false, - label: 'Part of count() / overall_sum(count())', - operationType: 'count', - scale: 'ratio', - sourceField: 'Records', filter: { language: 'kuery', query: 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, + isBucketed: false, + label: 'Part of count() / overall_sum(count())', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', }, 'y-axis-column-layer0X2': { customLabel: true, @@ -141,26 +153,51 @@ export const sampleAttribute = { }, }, }, + filters: [], + query: { + language: 'kuery', + query: 'transaction.duration.us < 60000000', + }, visualization: { - legend: { isVisible: true, position: 'right' }, - valueLabels: 'hide', - fittingFunction: 'Linear', + axisTitlesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, curveType: 'CURVE_MONOTONE_X', - axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, - tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, - gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, - preferredSeriesType: 'line', + fittingFunction: 'Linear', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, layers: [ { accessors: ['y-axis-column-layer0'], layerId: 'layer0', seriesType: 'line', - yConfig: [{ forAccessor: 'y-axis-column-layer0' }], xAccessor: 'x-axis-column-layer0', + yConfig: [ + { + color: 'green', + forAccessor: 'y-axis-column-layer0', + }, + ], }, ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'line', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'hide', }, - query: { query: 'transaction.duration.us < 60000000', language: 'kuery' }, - filters: [], }, + title: 'Prefilled from exploratory view app', + visualizationType: 'lnsXY', }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts index 2087b85b81886..425bf069cc87f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -5,7 +5,7 @@ * 2.0. */ export const sampleAttributeCoreWebVital = { - description: '', + description: 'undefined', references: [ { id: 'apm-*', @@ -94,7 +94,7 @@ export const sampleAttributeCoreWebVital = { filters: [], query: { language: 'kuery', - query: '', + query: 'transaction.type: "page-load"', }, visualization: { axisTitlesVisibilitySettings: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts index 7f066caf66bf1..85bafdecabde0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts @@ -5,12 +5,18 @@ * 2.0. */ export const sampleAttributeKpi = { - title: 'Prefilled from exploratory view app', - description: '', - visualizationType: 'lnsXY', + description: 'undefined', references: [ - { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, - { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, + { + id: 'apm-*', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: 'apm-*', + name: 'indexpattern-datasource-layer-layer0', + type: 'index-pattern', + }, ], state: { datasourceStates: { @@ -20,25 +26,27 @@ export const sampleAttributeKpi = { columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], columns: { 'x-axis-column-layer0': { - sourceField: '@timestamp', dataType: 'date', isBucketed: true, label: '@timestamp', operationType: 'date_histogram', - params: { interval: 'auto' }, + params: { + interval: 'auto', + }, scale: 'interval', + sourceField: '@timestamp', }, 'y-axis-column-layer0': { dataType: 'number', + filter: { + language: 'kuery', + query: 'transaction.type: page-load and processor.event: transaction', + }, isBucketed: false, - label: 'Page views', + label: 'test-series', operationType: 'count', scale: 'ratio', sourceField: 'Records', - filter: { - query: 'transaction.type: page-load and processor.event: transaction', - language: 'kuery', - }, }, }, incompleteColumns: {}, @@ -46,26 +54,51 @@ export const sampleAttributeKpi = { }, }, }, + filters: [], + query: { + language: 'kuery', + query: '', + }, visualization: { - legend: { isVisible: true, position: 'right' }, - valueLabels: 'hide', - fittingFunction: 'Linear', + axisTitlesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, curveType: 'CURVE_MONOTONE_X', - axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, - tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, - gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, - preferredSeriesType: 'line', + fittingFunction: 'Linear', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, layers: [ { accessors: ['y-axis-column-layer0'], layerId: 'layer0', seriesType: 'line', - yConfig: [{ forAccessor: 'y-axis-column-layer0' }], xAccessor: 'x-axis-column-layer0', + yConfig: [ + { + color: 'green', + forAccessor: 'y-axis-column-layer0', + }, + ], }, ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'line', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'hide', }, - query: { query: '', language: 'kuery' }, - filters: [], }, + title: 'Prefilled from exploratory view app', + visualizationType: 'lnsXY', }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index f7df2939d9909..694250e5749cb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; -import type { SeriesUrl, UrlFilter } from '../types'; +import type { ReportViewType, SeriesUrl, UrlFilter } from '../types'; import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public'; @@ -16,40 +16,43 @@ export function convertToShortUrl(series: SeriesUrl) { const { operationType, seriesType, - reportType, breakdown, filters, reportDefinitions, dataType, selectedMetricField, + hidden, + name, + color, ...restSeries } = series; return { [URL_KEYS.OPERATION_TYPE]: operationType, - [URL_KEYS.REPORT_TYPE]: reportType, [URL_KEYS.SERIES_TYPE]: seriesType, [URL_KEYS.BREAK_DOWN]: breakdown, [URL_KEYS.FILTERS]: filters, [URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions, [URL_KEYS.DATA_TYPE]: dataType, [URL_KEYS.SELECTED_METRIC]: selectedMetricField, + [URL_KEYS.HIDDEN]: hidden, + [URL_KEYS.NAME]: name, + [URL_KEYS.COLOR]: color, ...restSeries, }; } -export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { - const allSeriesIds = Object.keys(allSeries); - - const allShortSeries: AllShortSeries = {}; - - allSeriesIds.forEach((seriesKey) => { - allShortSeries[seriesKey] = convertToShortUrl(allSeries[seriesKey]); - }); +export function createExploratoryViewUrl( + { reportType, allSeries }: { reportType: ReportViewType; allSeries: AllSeries }, + baseHref = '' +) { + const allShortSeries: AllShortSeries = allSeries.map((series) => convertToShortUrl(series)); return ( baseHref + - `/app/observability/exploratory-view#?sr=${rison.encode(allShortSeries as RisonValue)}` + `/app/observability/exploratory-view/configure#?reportType=${reportType}&sr=${rison.encode( + (allShortSeries as unknown) as RisonValue + )}` ); } 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 989ebf17c2062..21c749258bebe 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 @@ -11,6 +11,13 @@ import { render, mockCore, mockAppIndexPattern } from './rtl_helpers'; import { ExploratoryView } from './exploratory_view'; import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils'; import * as obsvInd from './utils/observability_index_patterns'; +import * as pluginHook from '../../../hooks/use_plugin_context'; + +jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ + appMountParameters: { + setHeaderActionMenu: jest.fn(), + }, +} as any); describe('ExploratoryView', () => { mockAppIndexPattern(); @@ -41,29 +48,18 @@ describe('ExploratoryView', () => { it('renders exploratory view', async () => { render(); - expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); + expect(await screen.findByText(/Preview/i)).toBeInTheDocument(); + expect(await screen.findByText(/Configure series/i)).toBeInTheDocument(); + expect(await screen.findByText(/Hide chart/i)).toBeInTheDocument(); + expect(await screen.findByText(/Refresh/i)).toBeInTheDocument(); expect( await screen.findByRole('heading', { name: /Performance Distribution/i }) ).toBeInTheDocument(); }); it('renders lens component when there is series', async () => { - const initSeries = { - data: { - 'ux-series': { - isNew: true, - dataType: 'ux' as const, - reportType: 'data-distribution' as const, - breakdown: 'user_agent .name', - reportDefinitions: { 'service.name': ['elastic-co'] }, - time: { from: 'now-15m', to: 'now' }, - }, - }, - }; - - render(, { initSeries }); + render(); - expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); expect((await screen.findAllByText('Performance distribution'))[0]).toBeInTheDocument(); expect(await screen.findByText(/Lens Embeddable Component/i)).toBeInTheDocument(); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index af04108c56790..cb901b8b588f3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -4,11 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef, useState } from 'react'; -import { EuiPanel, EuiTitle } from '@elastic/eui'; +import { EuiButtonEmpty, EuiPanel, EuiResizableContainer, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; -import { isEmpty } from 'lodash'; +import { useRouteMatch } from 'react-router-dom'; +import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; @@ -16,40 +18,15 @@ import { useSeriesStorage } from './hooks/use_series_storage'; import { useLensAttributes } from './hooks/use_lens_attributes'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; -import { SeriesBuilder } from './series_builder/series_builder'; -import { SeriesUrl } from './types'; +import { SeriesViews } from './views/series_views'; import { LensEmbeddable } from './lens_embeddable'; import { EmptyView } from './components/empty_view'; -export const combineTimeRanges = ( - allSeries: Record, - firstSeries?: SeriesUrl -) => { - let to: string = ''; - let from: string = ''; - if (firstSeries?.reportType === 'kpi-over-time') { - return firstSeries.time; - } - Object.values(allSeries ?? {}).forEach((series) => { - if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) { - const seriesTo = new Date(series.time.to); - const seriesFrom = new Date(series.time.from); - if (!to || seriesTo > new Date(to)) { - to = series.time.to; - } - if (!from || seriesFrom < new Date(from)) { - from = series.time.from; - } - } - }); - return { to, from }; -}; +export type PanelId = 'seriesPanel' | 'chartPanel'; export function ExploratoryView({ saveAttributes, - multiSeries, }: { - multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { const { @@ -69,20 +46,19 @@ export function ExploratoryView({ const { loadIndexPattern, loading } = useAppIndexPatternContext(); - const { firstSeries, firstSeriesId, allSeries } = useSeriesStorage(); + const { firstSeries, allSeries, lastRefresh, reportType } = useSeriesStorage(); const lensAttributesT = useLensAttributes(); const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { const headerOffset = wrapperRef.current.getBoundingClientRect().top; - const seriesOffset = seriesBuilderRef.current.getBoundingClientRect().height; - setHeight(`calc(100vh - ${seriesOffset + headerOffset + 40}px)`); + setHeight(`calc(100vh - ${headerOffset + 40}px)`); } }; useEffect(() => { - Object.values(allSeries).forEach((seriesT) => { + allSeries.forEach((seriesT) => { loadIndexPattern({ dataType: seriesT.dataType, }); @@ -96,38 +72,104 @@ export function ExploratoryView({ } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(lensAttributesT ?? {})]); + }, [JSON.stringify(lensAttributesT ?? {}), lastRefresh]); useEffect(() => { setHeightOffset(); }); + const collapseFn = useRef<(id: PanelId, direction: PanelDirection) => void>(); + + const [hiddenPanel, setHiddenPanel] = useState(''); + + const isPreview = !!useRouteMatch('/exploratory-view/preview'); + + const onCollapse = (panelId: string) => { + setHiddenPanel((prevState) => (panelId === prevState ? '' : panelId)); + }; + + const onChange = (panelId: PanelId) => { + onCollapse(panelId); + if (collapseFn.current) { + collapseFn.current(panelId, panelId === 'seriesPanel' ? 'right' : 'left'); + } + }; + return ( {lens ? ( <> - + - {lensAttributes ? ( - - ) : ( - + + {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { + collapseFn.current = (id, direction) => togglePanel?.(id, { direction }); + + return ( + <> + + {lensAttributes ? ( + + ) : ( + + )} + + + + {!isPreview && + (hiddenPanel === 'chartPanel' ? ( + onChange('chartPanel')} iconType="arrowDown"> + {SHOW_CHART_LABEL} + + ) : ( + onChange('chartPanel')} + iconType="arrowUp" + color="text" + > + {HIDE_CHART_LABEL} + + ))} + + + + ); + }} + + {hiddenPanel === 'seriesPanel' && ( + onChange('seriesPanel')} iconType="arrowUp"> + {PREVIEW_LABEL} + )} - ) : ( -

- {i18n.translate('xpack.observability.overview.exploratoryView.lensDisabled', { - defaultMessage: - 'Lens app is not available, please enable Lens to use exploratory view.', - })} -

+

{LENS_NOT_AVAILABLE}

)}
@@ -147,4 +189,39 @@ const Wrapper = styled(EuiPanel)` margin: 0 auto; width: 100%; overflow-x: auto; + position: relative; +`; + +const ShowPreview = styled(EuiButtonEmpty)` + position: absolute; + bottom: 34px; +`; +const HideChart = styled(EuiButtonEmpty)` + position: absolute; + top: -35px; + right: 50px; `; +const ShowChart = styled(EuiButtonEmpty)` + position: absolute; + top: -10px; + right: 50px; +`; + +const HIDE_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.hideChart', { + defaultMessage: 'Hide chart', +}); + +const SHOW_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.showChart', { + defaultMessage: 'Show chart', +}); + +const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.preview', { + defaultMessage: 'Preview', +}); + +const LENS_NOT_AVAILABLE = i18n.translate( + 'xpack.observability.overview.exploratoryView.lensDisabled', + { + defaultMessage: 'Lens app is not available, please enable Lens to use exploratory view.', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx index 8cd8977fcf741..1f910b946deb3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -8,51 +8,22 @@ import React from 'react'; import { render } from '../rtl_helpers'; import { ExploratoryViewHeader } from './header'; -import { fireEvent } from '@testing-library/dom'; +import * as pluginHook from '../../../../hooks/use_plugin_context'; + +jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ + appMountParameters: { + setHeaderActionMenu: jest.fn(), + }, +} as any); describe('ExploratoryViewHeader', function () { it('should render properly', function () { const { getByText } = render( ); - getByText('Open in Lens'); - }); - - it('should be able to click open in lens', function () { - const initSeries = { - data: { - 'uptime-pings-histogram': { - dataType: 'synthetics' as const, - reportType: 'kpi-over-time' as const, - breakdown: 'monitor.status', - time: { from: 'now-15m', to: 'now' }, - }, - }, - }; - - const { getByText, core } = render( - , - { initSeries } - ); - fireEvent.click(getByText('Open in Lens')); - - expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); - expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith( - { - attributes: { title: 'Performance distribution' }, - id: '', - timeRange: { - from: 'now-15m', - to: 'now', - }, - }, - true - ); + getByText('Refresh'); }); }); 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 ded56ec9e817f..bec8673f88b4e 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 @@ -5,43 +5,37 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { TypedLensByValueInput, LensEmbeddableInput } from '../../../../../../lens/public'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { TypedLensByValueInput } from '../../../../../../lens/public'; import { DataViewLabels } from '../configurations/constants'; -import { ObservabilityAppServices } from '../../../../application/types'; import { useSeriesStorage } from '../hooks/use_series_storage'; -import { combineTimeRanges } from '../exploratory_view'; +import { LastUpdated } from './last_updated'; +import { combineTimeRanges } from '../lens_embeddable'; +import { ExpViewActionMenu } from '../components/action_menu'; interface Props { - seriesId: string; + seriesId?: number; + lastUpdated?: number; lensAttributes: TypedLensByValueInput['attributes'] | null; } -export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { - const kServices = useKibana().services; +export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: Props) { + const { getSeries, allSeries, setLastRefresh, reportType } = useSeriesStorage(); - const { lens } = kServices; + const series = seriesId ? getSeries(seriesId) : undefined; - const { getSeries, allSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - - const [isSaveOpen, setIsSaveOpen] = useState(false); - - const LensSaveModalComponent = lens.SaveModalComponent; - - const timeRange = combineTimeRanges(allSeries, series); + const timeRange = combineTimeRanges(reportType, allSeries, series); return ( <> +

- {DataViewLabels[series.reportType] ?? + {DataViewLabels[reportType] ?? i18n.translate('xpack.observability.expView.heading.label', { defaultMessage: 'Analyze data', })}{' '} @@ -57,53 +51,18 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { - { - if (lensAttributes) { - lens.navigateToPrefilledEditor( - { - id: '', - timeRange, - attributes: lensAttributes, - }, - true - ); - } - }} - > - {i18n.translate('xpack.observability.expView.heading.openInLens', { - defaultMessage: 'Open in Lens', - })} - + - { - if (lensAttributes) { - setIsSaveOpen(true); - } - }} - > - {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', { - defaultMessage: 'Save', - })} + setLastRefresh(Date.now())}> + {REFRESH_LABEL} - - {isSaveOpen && lensAttributes && ( - setIsSaveOpen(false)} - onSave={() => {}} - /> - )} ); } + +const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', { + defaultMessage: 'Refresh', +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx similarity index 55% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx index 874171de123d2..c352ec0423dd8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx @@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react'; import { EuiIcon, EuiText } from '@elastic/eui'; import moment from 'moment'; +import { FormattedMessage } from '@kbn/i18n/react'; interface Props { lastUpdated?: number; @@ -18,20 +19,34 @@ export function LastUpdated({ lastUpdated }: Props) { useEffect(() => { const interVal = setInterval(() => { setRefresh(Date.now()); - }, 1000); + }, 5000); return () => { clearInterval(interVal); }; }, []); + useEffect(() => { + setRefresh(Date.now()); + }, [lastUpdated]); + if (!lastUpdated) { return null; } + const isWarning = moment().diff(moment(lastUpdated), 'minute') > 5; + const isDanger = moment().diff(moment(lastUpdated), 'minute') > 10; + return ( - - Last Updated: {moment(lastUpdated).from(refresh)} + + + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index 7a5f12a72b1f0..d65917093d129 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -27,7 +27,7 @@ interface ProviderProps { } type HasAppDataState = Record; -type IndexPatternState = Record; +export type IndexPatternState = Record; type LoadingState = Record; export function IndexPatternContextProvider({ children }: ProviderProps) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx new file mode 100644 index 0000000000000..e86144c124949 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useState } from 'react'; +import { useKibana } from '../../../../utils/kibana_react'; +import { SeriesConfig, SeriesUrl } from '../types'; +import { useAppIndexPatternContext } from './use_app_index_pattern'; +import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter } from '../configurations/utils'; +import { getFiltersFromDefs } from './use_lens_attributes'; +import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants'; + +interface UseDiscoverLink { + seriesConfig: SeriesConfig; + series: SeriesUrl; +} + +export const useDiscoverLink = ({ series, seriesConfig }: UseDiscoverLink) => { + const kServices = useKibana().services; + const { + application: { navigateToUrl }, + } = kServices; + + const { indexPatterns } = useAppIndexPatternContext(); + + const urlGenerator = kServices.discover?.urlGenerator; + const [discoverUrl, setDiscoverUrl] = useState(''); + + useEffect(() => { + const indexPattern = indexPatterns?.[series.dataType]; + + const definitions = series.reportDefinitions ?? {}; + const filters = [...(seriesConfig?.baseFilters ?? [])]; + + const definitionFilters = getFiltersFromDefs(definitions); + + definitionFilters.forEach(({ field, values = [] }) => { + if (values.length > 1) { + filters.push(buildPhrasesFilter(field, values, indexPattern)[0]); + } else { + filters.push(buildPhraseFilter(field, values[0], indexPattern)[0]); + } + }); + + const selectedMetricField = series.selectedMetricField; + + if ( + selectedMetricField && + selectedMetricField !== RECORDS_FIELD && + selectedMetricField !== RECORDS_PERCENTAGE_FIELD + ) { + filters.push(buildExistsFilter(selectedMetricField, indexPattern)[0]); + } + + const getDiscoverUrl = async () => { + if (!urlGenerator?.createUrl) return; + + const newUrl = await urlGenerator.createUrl({ + filters, + indexPatternId: indexPattern?.id, + }); + setDiscoverUrl(newUrl); + }; + getDiscoverUrl(); + }, [ + indexPatterns, + series.dataType, + series.reportDefinitions, + series.selectedMetricField, + seriesConfig?.baseFilters, + urlGenerator, + ]); + + const onClick = useCallback( + (event: React.MouseEvent) => { + if (discoverUrl) { + event.preventDefault(); + + return navigateToUrl(discoverUrl); + } + }, + [discoverUrl, navigateToUrl] + ); + + return { + href: discoverUrl, + onClick, + }; +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 8bb265b4f6d89..71945734eeabc 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -9,12 +9,18 @@ import { useMemo } from 'react'; import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../../lens/public'; import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; -import { useSeriesStorage } from './use_series_storage'; +import { + AllSeries, + allSeriesKey, + convertAllShortSeries, + useSeriesStorage, +} from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { SeriesUrl, UrlFilter } from '../types'; import { useAppIndexPatternContext } from './use_app_index_pattern'; import { ALL_VALUES_SELECTED } from '../../field_value_suggestions/field_value_combobox'; +import { useTheme } from '../../../../hooks/use_theme'; export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => { return Object.entries(reportDefinitions ?? {}) @@ -28,41 +34,56 @@ export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitio }; export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => { - const { allSeriesIds, allSeries } = useSeriesStorage(); + const { storage, autoApply, allSeries, lastRefresh, reportType } = useSeriesStorage(); const { indexPatterns } = useAppIndexPatternContext(); + const theme = useTheme(); + return useMemo(() => { - if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) { + if (isEmpty(indexPatterns) || isEmpty(allSeries) || !reportType) { return null; } + const allSeriesT: AllSeries = autoApply + ? allSeries + : convertAllShortSeries(storage.get(allSeriesKey) ?? []); + const layerConfigs: LayerConfig[] = []; - allSeriesIds.forEach((seriesIdT) => { - const seriesT = allSeries[seriesIdT]; - const indexPattern = indexPatterns?.[seriesT?.dataType]; - if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) { + allSeriesT.forEach((series, seriesIndex) => { + const indexPattern = indexPatterns?.[series?.dataType]; + + if ( + indexPattern && + !isEmpty(series.reportDefinitions) && + !series.hidden && + series.selectedMetricField + ) { const seriesConfig = getDefaultConfigs({ - reportType: seriesT.reportType, - dataType: seriesT.dataType, + reportType, indexPattern, + dataType: series.dataType, }); - const filters: UrlFilter[] = (seriesT.filters ?? []).concat( - getFiltersFromDefs(seriesT.reportDefinitions) + const filters: UrlFilter[] = (series.filters ?? []).concat( + getFiltersFromDefs(series.reportDefinitions) ); + const color = `euiColorVis${seriesIndex}`; + layerConfigs.push({ filters, indexPattern, seriesConfig, - time: seriesT.time, - breakdown: seriesT.breakdown, - seriesType: seriesT.seriesType, - operationType: seriesT.operationType, - reportDefinitions: seriesT.reportDefinitions ?? {}, - selectedMetricField: seriesT.selectedMetricField, + time: series.time, + name: series.name, + breakdown: series.breakdown, + seriesType: series.seriesType, + operationType: series.operationType, + reportDefinitions: series.reportDefinitions ?? {}, + selectedMetricField: series.selectedMetricField, + color: series.color ?? ((theme.eui as unknown) as Record)[color], }); } }); @@ -73,6 +94,6 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null const lensAttributes = new LensAttributes(layerConfigs); - return lensAttributes.getJSON(); - }, [indexPatterns, allSeriesIds, allSeries]); + return lensAttributes.getJSON(lastRefresh); + }, [indexPatterns, allSeries, reportType, autoApply, storage, theme, lastRefresh]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts index 2d2618bc46152..f2a6130cdc59d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts @@ -6,18 +6,16 @@ */ import { useSeriesStorage } from './use_series_storage'; -import { UrlFilter } from '../types'; +import { SeriesUrl, UrlFilter } from '../types'; export interface UpdateFilter { field: string; - value: string; + value: string | string[]; negate?: boolean; } -export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { - const { getSeries, setSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); +export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; series: SeriesUrl }) => { + const { setSeries } = useSeriesStorage(); const filters = series.filters ?? []; @@ -26,10 +24,14 @@ export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { .map((filter) => { if (filter.field === field) { if (negate) { - const notValuesN = filter.notValues?.filter((val) => val !== value); + const notValuesN = filter.notValues?.filter((val) => + value instanceof Array ? !value.includes(val) : val !== value + ); return { ...filter, notValues: notValuesN }; } else { - const valuesN = filter.values?.filter((val) => val !== value); + const valuesN = filter.values?.filter((val) => + value instanceof Array ? !value.includes(val) : val !== value + ); return { ...filter, values: valuesN }; } } @@ -43,9 +45,9 @@ export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { const addFilter = ({ field, value, negate }: UpdateFilter) => { const currFilter: UrlFilter = { field }; if (negate) { - currFilter.notValues = [value]; + currFilter.notValues = value instanceof Array ? value : [value]; } else { - currFilter.values = [value]; + currFilter.values = value instanceof Array ? value : [value]; } if (filters.length === 0) { setSeries(seriesId, { ...series, filters: [currFilter] }); @@ -65,13 +67,26 @@ export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { const currNotValues = currFilter.notValues ?? []; const currValues = currFilter.values ?? []; - const notValues = currNotValues.filter((val) => val !== value); - const values = currValues.filter((val) => val !== value); + const notValues = currNotValues.filter((val) => + value instanceof Array ? !value.includes(val) : val !== value + ); + + const values = currValues.filter((val) => + value instanceof Array ? !value.includes(val) : val !== value + ); if (negate) { - notValues.push(value); + if (value instanceof Array) { + notValues.push(...value); + } else { + notValues.push(value); + } } else { - values.push(value); + if (value instanceof Array) { + values.push(...value); + } else { + values.push(value); + } } currFilter.notValues = notValues.length > 0 ? notValues : undefined; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx index c32acc47abd1b..ce6d7bd94d8e4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx @@ -6,37 +6,39 @@ */ import React, { useEffect } from 'react'; - -import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage'; +import { Route, Router } from 'react-router-dom'; import { render } from '@testing-library/react'; +import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage'; +import { getHistoryFromUrl } from '../rtl_helpers'; -const mockSingleSeries = { - 'performance-distribution': { - reportType: 'data-distribution', +const mockSingleSeries = [ + { + name: 'performance-distribution', dataType: 'ux', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, -}; +]; -const mockMultipleSeries = { - 'performance-distribution': { - reportType: 'data-distribution', +const mockMultipleSeries = [ + { + name: 'performance-distribution', dataType: 'ux', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - 'kpi-over-time': { - reportType: 'kpi-over-time', + { + name: 'kpi-over-time', dataType: 'synthetics', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, -}; +]; -describe('userSeries', function () { +describe('userSeriesStorage', function () { function setupTestComponent(seriesData: any) { const setData = jest.fn(); + function TestComponent() { const data = useSeriesStorage(); @@ -48,11 +50,20 @@ describe('userSeries', function () { } render( - - - + + + (key === 'sr' ? seriesData : null)), + set: jest.fn(), + }} + > + + + + ); return setData; @@ -63,22 +74,20 @@ describe('userSeries', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: { - 'performance-distribution': { - breakdown: 'user_agent.name', + allSeries: [ + { + name: 'performance-distribution', dataType: 'ux', - reportType: 'data-distribution', + breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - }, - allSeriesIds: ['performance-distribution'], + ], firstSeries: { - breakdown: 'user_agent.name', + name: 'performance-distribution', dataType: 'ux', - reportType: 'data-distribution', + breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - firstSeriesId: 'performance-distribution', }) ); }); @@ -89,42 +98,38 @@ describe('userSeries', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: { - 'performance-distribution': { - breakdown: 'user_agent.name', + allSeries: [ + { + name: 'performance-distribution', dataType: 'ux', - reportType: 'data-distribution', + breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - 'kpi-over-time': { - reportType: 'kpi-over-time', + { + name: 'kpi-over-time', dataType: 'synthetics', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - }, - allSeriesIds: ['performance-distribution', 'kpi-over-time'], + ], firstSeries: { - breakdown: 'user_agent.name', + name: 'performance-distribution', dataType: 'ux', - reportType: 'data-distribution', + breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - firstSeriesId: 'performance-distribution', }) ); }); it('should return expected result when there are no series', function () { - const setData = setupTestComponent({}); + const setData = setupTestComponent([]); - expect(setData).toHaveBeenCalledTimes(2); + expect(setData).toHaveBeenCalledTimes(1); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: {}, - allSeriesIds: [], + allSeries: [], firstSeries: undefined, - firstSeriesId: undefined, }) ); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index a47a124d14b4d..04f8751e2a0b6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -6,6 +6,7 @@ */ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import { useRouteMatch } from 'react-router-dom'; import { IKbnUrlStateStorage, ISessionStorageStateStorage, @@ -22,13 +23,19 @@ import { OperationType, SeriesType } from '../../../../../../lens/public'; import { URL_KEYS } from '../configurations/constants/url_constants'; export interface SeriesContextValue { - firstSeries: SeriesUrl; - firstSeriesId: string; - allSeriesIds: string[]; + firstSeries?: SeriesUrl; + autoApply: boolean; + lastRefresh: number; + setLastRefresh: (val: number) => void; + setAutoApply: (val: boolean) => void; + applyChanges: () => void; allSeries: AllSeries; - setSeries: (seriesIdN: string, newValue: SeriesUrl) => void; - getSeries: (seriesId: string) => SeriesUrl; - removeSeries: (seriesId: string) => void; + setSeries: (seriesIndex: number, newValue: SeriesUrl) => void; + getSeries: (seriesIndex: number) => SeriesUrl | undefined; + removeSeries: (seriesIndex: number) => void; + setReportType: (reportType: string) => void; + storage: IKbnUrlStateStorage | ISessionStorageStateStorage; + reportType: ReportViewType; } export const UrlStorageContext = createContext({} as SeriesContextValue); @@ -36,72 +43,112 @@ interface ProviderProps { storage: IKbnUrlStateStorage | ISessionStorageStateStorage; } -function convertAllShortSeries(allShortSeries: AllShortSeries) { - const allSeriesIds = Object.keys(allShortSeries); - const allSeriesN: AllSeries = {}; - allSeriesIds.forEach((seriesKey) => { - allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); - }); - - return allSeriesN; +export function convertAllShortSeries(allShortSeries: AllShortSeries) { + return (allShortSeries ?? []).map((shortSeries) => convertFromShortUrl(shortSeries)); } +export const allSeriesKey = 'sr'; +const autoApplyKey = 'autoApply'; +const reportTypeKey = 'reportType'; + export function UrlStorageContextProvider({ children, storage, }: ProviderProps & { children: JSX.Element }) { - const allSeriesKey = 'sr'; - - const [allShortSeries, setAllShortSeries] = useState( - () => storage.get(allSeriesKey) ?? {} - ); const [allSeries, setAllSeries] = useState(() => - convertAllShortSeries(storage.get(allSeriesKey) ?? {}) + convertAllShortSeries(storage.get(allSeriesKey) ?? []) + ); + + const [autoApply, setAutoApply] = useState(() => storage.get(autoApplyKey) ?? true); + const [lastRefresh, setLastRefresh] = useState(() => Date.now()); + + const [reportType, setReportType] = useState( + () => (storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '' ); - const [firstSeriesId, setFirstSeriesId] = useState(''); + const [firstSeries, setFirstSeries] = useState(); + const isPreview = !!useRouteMatch('/exploratory-view/preview'); useEffect(() => { - const allSeriesIds = Object.keys(allShortSeries); - const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {}); + const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); - setAllSeries(allSeriesN); - setFirstSeriesId(allSeriesIds?.[0]); - setFirstSeries(allSeriesN?.[allSeriesIds?.[0]]); - (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - }, [allShortSeries, storage]); + const firstSeriesT = allSeries?.[0]; - const setSeries = (seriesIdN: string, newValue: SeriesUrl) => { - setAllShortSeries((prevState) => { - prevState[seriesIdN] = convertToShortUrl(newValue); - return { ...prevState }; - }); - }; + setFirstSeries(firstSeriesT); - const removeSeries = (seriesIdN: string) => { - setAllShortSeries((prevState) => { - delete prevState[seriesIdN]; - return { ...prevState }; + if (autoApply) { + (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); + } + }, [allSeries, autoApply, storage]); + + useEffect(() => { + // needed for tab change + const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); + + (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); + (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); + // this is only needed for tab change, so we will not add allSeries into dependencies + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isPreview, storage]); + + const setSeries = useCallback((seriesIndex: number, newValue: SeriesUrl) => { + setAllSeries((prevAllSeries) => { + const newStateRest = prevAllSeries.map((series, index) => { + if (index === seriesIndex) { + return newValue; + } + return series; + }); + + if (prevAllSeries.length === seriesIndex) { + return [...newStateRest, newValue]; + } + + return [...newStateRest]; }); - }; + }, []); + + useEffect(() => { + (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); + }, [reportType, storage]); - const allSeriesIds = Object.keys(allShortSeries); + const removeSeries = useCallback((seriesIndex: number) => { + setAllSeries((prevAllSeries) => + prevAllSeries.filter((seriesT, index) => index !== seriesIndex) + ); + }, []); const getSeries = useCallback( - (seriesId?: string) => { - return seriesId ? allSeries?.[seriesId] ?? {} : ({} as SeriesUrl); + (seriesIndex: number) => { + return allSeries[seriesIndex]; }, [allSeries] ); + const applyChanges = useCallback(() => { + const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); + + (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); + setLastRefresh(Date.now()); + }, [allSeries, storage]); + + useEffect(() => { + (storage as IKbnUrlStateStorage).set(autoApplyKey, autoApply); + }, [autoApply, storage]); + const value = { + autoApply, + setAutoApply, + applyChanges, storage, getSeries, setSeries, removeSeries, - firstSeriesId, allSeries, - allSeriesIds, + lastRefresh, + setLastRefresh, + setReportType, + reportType: storage.get(reportTypeKey) as ReportViewType, firstSeries: firstSeries!, }; return {children}; @@ -112,10 +159,9 @@ export function useSeriesStorage() { } function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { - const { dt, op, st, rt, bd, ft, time, rdf, mt, ...restSeries } = newValue; + const { dt, op, st, bd, ft, time, rdf, mt, h, n, c, ...restSeries } = newValue; return { operationType: op, - reportType: rt!, seriesType: st, breakdown: bd, filters: ft!, @@ -123,26 +169,31 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { reportDefinitions: rdf, dataType: dt!, selectedMetricField: mt, + hidden: h, + name: n, + color: c, ...restSeries, }; } interface ShortUrlSeries { [URL_KEYS.OPERATION_TYPE]?: OperationType; - [URL_KEYS.REPORT_TYPE]?: ReportViewType; [URL_KEYS.DATA_TYPE]?: AppDataType; [URL_KEYS.SERIES_TYPE]?: SeriesType; [URL_KEYS.BREAK_DOWN]?: string; [URL_KEYS.FILTERS]?: UrlFilter[]; [URL_KEYS.REPORT_DEFINITIONS]?: URLReportDefinition; [URL_KEYS.SELECTED_METRIC]?: string; + [URL_KEYS.HIDDEN]?: boolean; + [URL_KEYS.NAME]: string; + [URL_KEYS.COLOR]?: string; time?: { to: string; from: string; }; } -export type AllShortSeries = Record; -export type AllSeries = Record; +export type AllShortSeries = ShortUrlSeries[]; +export type AllSeries = SeriesUrl[]; -export const NEW_SERIES_KEY = 'new-series-key'; +export const NEW_SERIES_KEY = 'new-series'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index e55752ceb62ba..3de29b02853e8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -25,11 +25,9 @@ import { TypedLensByValueInput } from '../../../../../lens/public'; export function ExploratoryViewPage({ saveAttributes, - multiSeries = false, useSessionStorage = false, }: { useSessionStorage?: boolean; - multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' }); @@ -61,7 +59,7 @@ export function ExploratoryViewPage({ - + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx index 4cb586fe94ceb..9e4d9486dc155 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx @@ -7,16 +7,51 @@ import { i18n } from '@kbn/i18n'; import React, { Dispatch, SetStateAction, useCallback } from 'react'; -import { combineTimeRanges } from './exploratory_view'; +import styled from 'styled-components'; +import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useSeriesStorage } from './hooks/use_series_storage'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ReportViewType, SeriesUrl } from './types'; +import { ReportTypes } from './configurations/constants'; interface Props { lensAttributes: TypedLensByValueInput['attributes']; setLastUpdated: Dispatch>; } +export const combineTimeRanges = ( + reportType: ReportViewType, + allSeries: SeriesUrl[], + firstSeries?: SeriesUrl +) => { + let to: string = ''; + let from: string = ''; + + if (reportType === ReportTypes.KPI) { + return firstSeries?.time; + } + + allSeries.forEach((series) => { + if ( + series.dataType && + series.selectedMetricField && + !isEmpty(series.reportDefinitions) && + series.time + ) { + const seriesTo = new Date(series.time.to); + const seriesFrom = new Date(series.time.from); + if (!to || seriesTo > new Date(to)) { + to = series.time.to; + } + if (!from || seriesFrom < new Date(from)) { + from = series.time.from; + } + } + }); + + return { to, from }; +}; export function LensEmbeddable(props: Props) { const { lensAttributes, setLastUpdated } = props; @@ -27,9 +62,11 @@ export function LensEmbeddable(props: Props) { const LensComponent = lens?.EmbeddableComponent; - const { firstSeriesId, firstSeries: series, setSeries, allSeries } = useSeriesStorage(); + const { firstSeries, setSeries, allSeries, reportType } = useSeriesStorage(); - const timeRange = combineTimeRanges(allSeries, series); + const firstSeriesId = 0; + + const timeRange = firstSeries ? combineTimeRanges(reportType, allSeries, firstSeries) : null; const onLensLoad = useCallback(() => { setLastUpdated(Date.now()); @@ -37,9 +74,9 @@ export function LensEmbeddable(props: Props) { const onBrushEnd = useCallback( ({ range }: { range: number[] }) => { - if (series?.reportType !== 'data-distribution') { + if (reportType !== 'data-distribution' && firstSeries) { setSeries(firstSeriesId, { - ...series, + ...firstSeries, time: { from: new Date(range[0]).toISOString(), to: new Date(range[1]).toISOString(), @@ -53,16 +90,30 @@ export function LensEmbeddable(props: Props) { ); } }, - [notifications?.toasts, series, firstSeriesId, setSeries] + [reportType, setSeries, firstSeries, notifications?.toasts] ); + if (timeRange === null || !firstSeries) { + return null; + } + return ( - + + + ); } + +const LensWrapper = styled.div` + height: 100%; + + &&& > div { + height: 100%; + } +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 972e3beb4b722..0e609cbe6c9e5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -10,7 +10,7 @@ import React, { ReactElement } from 'react'; import { stringify } from 'query-string'; // eslint-disable-next-line import/no-extraneous-dependencies import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; -import { Router } from 'react-router-dom'; +import { Route, Router } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; import { CoreStart } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; @@ -24,7 +24,7 @@ import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/ import { lensPluginMock } from '../../../../../lens/public/mocks'; import * as useAppIndexPatternHook from './hooks/use_app_index_pattern'; import { IndexPatternContextProvider } from './hooks/use_app_index_pattern'; -import { AllSeries, UrlStorageContext } from './hooks/use_series_storage'; +import { AllSeries, SeriesContextValue, UrlStorageContext } from './hooks/use_series_storage'; import * as fetcherHook from '../../../hooks/use_fetcher'; import * as useSeriesFilterHook from './hooks/use_series_filters'; @@ -39,9 +39,10 @@ import { IndexPattern, IndexPatternsContract, } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { AppDataType, UrlFilter } from './types'; +import { AppDataType, SeriesUrl, UrlFilter } from './types'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ListItem } from '../../../hooks/use_values_list'; +import { TRANSACTION_DURATION } from './configurations/constants/elasticsearch_fieldnames'; interface KibanaProps { services?: KibanaServices; @@ -158,9 +159,11 @@ export function MockRouter({ }: MockRouterProps) { return ( - - {children} - + + + {children} + + ); } @@ -173,7 +176,7 @@ export function render( core: customCore, kibanaProps, renderOptions, - url, + url = '/app/observability/exploratory-view/configure#?autoApply=!t', initSeries = {}, }: RenderRouterOptions = {} ) { @@ -203,7 +206,7 @@ export function render( }; } -const getHistoryFromUrl = (url: Url) => { +export const getHistoryFromUrl = (url: Url) => { if (typeof url === 'string') { return createMemoryHistory({ initialEntries: [url], @@ -252,6 +255,15 @@ export const mockUseValuesList = (values?: ListItem[]) => { return { spy, onRefreshTimeRange }; }; +export const mockUxSeries = { + name: 'performance-distribution', + dataType: 'ux', + breakdown: 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + reportDefinitions: { 'service.name': ['elastic-co'] }, + selectedMetricField: TRANSACTION_DURATION, +} as SeriesUrl; + function mockSeriesStorageContext({ data, filters, @@ -261,34 +273,34 @@ function mockSeriesStorageContext({ filters?: UrlFilter[]; breakdown?: string; }) { - const mockDataSeries = data || { - 'performance-distribution': { - reportType: 'data-distribution', - dataType: 'ux', - breakdown: breakdown || 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - ...(filters ? { filters } : {}), - }, + const testSeries = { + ...mockUxSeries, + breakdown: breakdown || 'user_agent.name', + ...(filters ? { filters } : {}), }; - const allSeriesIds = Object.keys(mockDataSeries); - const firstSeriesId = allSeriesIds?.[0]; - const series = mockDataSeries[firstSeriesId]; + const mockDataSeries = data || [testSeries]; const removeSeries = jest.fn(); const setSeries = jest.fn(); - const getSeries = jest.fn().mockReturnValue(series); + const getSeries = jest.fn().mockReturnValue(testSeries); return { - firstSeriesId, - allSeriesIds, removeSeries, setSeries, getSeries, - firstSeries: mockDataSeries[firstSeriesId], + autoApply: true, + reportType: 'data-distribution', + lastRefresh: Date.now(), + setLastRefresh: jest.fn(), + setAutoApply: jest.fn(), + applyChanges: jest.fn(), + firstSeries: mockDataSeries[0], allSeries: mockDataSeries, - }; + setReportType: jest.fn(), + storage: { get: jest.fn() } as any, + } as SeriesContextValue; } export function mockUseSeriesFilter() { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx deleted file mode 100644 index b10702ebded57..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; -import { mockAppIndexPattern, render } from '../../rtl_helpers'; -import { dataTypes, DataTypesCol } from './data_types_col'; - -describe('DataTypesCol', function () { - const seriesId = 'test-series-id'; - - mockAppIndexPattern(); - - it('should render properly', function () { - const { getByText } = render(); - - dataTypes.forEach(({ label }) => { - getByText(label); - }); - }); - - it('should set series on change', function () { - const { setSeries } = render(); - - fireEvent.click(screen.getByText(/user experience \(rum\)/i)); - - expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { - dataType: 'ux', - isNew: true, - time: { - from: 'now-15m', - to: 'now', - }, - }); - }); - - it('should set series on change on already selected', function () { - const initSeries = { - data: { - [seriesId]: { - dataType: 'synthetics' as const, - reportType: 'kpi-over-time' as const, - breakdown: 'monitor.status', - time: { from: 'now-15m', to: 'now' }, - }, - }, - }; - - render(, { initSeries }); - - const button = screen.getByRole('button', { - name: /Synthetic Monitoring/i, - }); - - expect(button.classList).toContain('euiButton--fill'); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx deleted file mode 100644 index f386f62d9ed73..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import styled from 'styled-components'; -import { AppDataType } from '../../types'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; - -export const dataTypes: Array<{ id: AppDataType; label: string }> = [ - { id: 'synthetics', label: 'Synthetic Monitoring' }, - { id: 'ux', label: 'User Experience (RUM)' }, - { id: 'mobile', label: 'Mobile Experience' }, - // { id: 'infra_logs', label: 'Logs' }, - // { id: 'infra_metrics', label: 'Metrics' }, - // { id: 'apm', label: 'APM' }, -]; - -export function DataTypesCol({ seriesId }: { seriesId: string }) { - const { getSeries, setSeries, removeSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - const { loading } = useAppIndexPatternContext(); - - const onDataTypeChange = (dataType?: AppDataType) => { - if (!dataType) { - removeSeries(seriesId); - } else { - setSeries(seriesId || `${dataType}-series`, { - dataType, - isNew: true, - time: series.time, - } as any); - } - }; - - const selectedDataType = series.dataType; - - return ( - - {dataTypes.map(({ id: dataTypeId, label }) => ( - - - - ))} - - ); -} - -const FlexGroup = styled(EuiFlexGroup)` - width: 100%; -`; - -const Button = styled(EuiButton)` - will-change: transform; -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx deleted file mode 100644 index 6be78084ae195..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import styled from 'styled-components'; -import { SeriesDatePicker } from '../../series_date_picker'; -import { DateRangePicker } from '../../series_date_picker/date_range_picker'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; - -interface Props { - seriesId: string; -} -export function DatePickerCol({ seriesId }: Props) { - const { firstSeriesId, getSeries } = useSeriesStorage(); - const { reportType } = getSeries(firstSeriesId); - - return ( - - {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( - - ) : ( - - )} - - ); -} - -const Wrapper = styled.div` - .euiSuperDatePicker__flexWrapper { - width: 100%; - > .euiFlexItem { - margin-right: 0px; - } - } -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx deleted file mode 100644 index a5e5ad3900ded..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; -import { getDefaultConfigs } from '../../configurations/default_configs'; -import { mockIndexPattern, render } from '../../rtl_helpers'; -import { ReportBreakdowns } from './report_breakdowns'; -import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; - -describe('Series Builder ReportBreakdowns', function () { - const seriesId = 'test-series-id'; - const dataViewSeries = getDefaultConfigs({ - reportType: 'data-distribution', - dataType: 'ux', - indexPattern: mockIndexPattern, - }); - - it('should render properly', function () { - render(); - - screen.getByText('Select an option: , is selected'); - screen.getAllByText('Browser family'); - }); - - it('should set new series breakdown on change', function () { - const { setSeries } = render( - - ); - - const btn = screen.getByRole('button', { - name: /select an option: Browser family , is selected/i, - hidden: true, - }); - - fireEvent.click(btn); - - fireEvent.click(screen.getByText(/operating system/i)); - - expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { - breakdown: USER_AGENT_OS, - dataType: 'ux', - reportType: 'data-distribution', - time: { from: 'now-15m', to: 'now' }, - }); - }); - it('should set undefined on new series on no select breakdown', function () { - const { setSeries } = render( - - ); - - const btn = screen.getByRole('button', { - name: /select an option: Browser family , is selected/i, - hidden: true, - }); - - fireEvent.click(btn); - - fireEvent.click(screen.getByText(/no breakdown/i)); - - expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { - breakdown: undefined, - dataType: 'ux', - reportType: 'data-distribution', - time: { from: 'now-15m', to: 'now' }, - }); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx deleted file mode 100644 index fa2d01691ce1d..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx +++ /dev/null @@ -1,26 +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 { Breakdowns } from '../../series_editor/columns/breakdowns'; -import { SeriesConfig } from '../../types'; - -export function ReportBreakdowns({ - seriesId, - seriesConfig, -}: { - seriesConfig: SeriesConfig; - seriesId: string; -}) { - return ( - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx deleted file mode 100644 index 0c620abf56e8a..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; -import styled from 'styled-components'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { ReportMetricOptions } from '../report_metric_options'; -import { SeriesConfig } from '../../types'; -import { SeriesChartTypesSelect } from './chart_types'; -import { OperationTypeSelect } from './operation_type_select'; -import { DatePickerCol } from './date_picker_col'; -import { parseCustomFieldName } from '../../configurations/lens_attributes'; -import { ReportDefinitionField } from './report_definition_field'; - -function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { - const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); - - return columnType; -} - -export function ReportDefinitionCol({ - seriesConfig, - seriesId, -}: { - seriesConfig: SeriesConfig; - seriesId: string; -}) { - const { getSeries, setSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - - const { reportDefinitions: selectedReportDefinitions = {}, selectedMetricField } = series ?? {}; - - const { - definitionFields, - defaultSeriesType, - hasOperationType, - yAxisColumns, - metricOptions, - } = seriesConfig; - - const onChange = (field: string, value?: string[]) => { - if (!value?.[0]) { - delete selectedReportDefinitions[field]; - setSeries(seriesId, { - ...series, - reportDefinitions: { ...selectedReportDefinitions }, - }); - } else { - setSeries(seriesId, { - ...series, - reportDefinitions: { ...selectedReportDefinitions, [field]: value }, - }); - } - }; - - const columnType = getColumnType(seriesConfig, selectedMetricField); - - return ( - - - - - - {definitionFields.map((field) => ( - - - - ))} - {metricOptions && ( - - - - )} - {(hasOperationType || columnType === 'operation') && ( - - - - )} - - - - - ); -} - -const FlexGroup = styled(EuiFlexGroup)` - width: 100%; -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx deleted file mode 100644 index 0b183b5f20c03..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { screen } from '@testing-library/react'; -import { ReportFilters } from './report_filters'; -import { getDefaultConfigs } from '../../configurations/default_configs'; -import { mockIndexPattern, render } from '../../rtl_helpers'; - -describe('Series Builder ReportFilters', function () { - const seriesId = 'test-series-id'; - - const dataViewSeries = getDefaultConfigs({ - reportType: 'data-distribution', - indexPattern: mockIndexPattern, - dataType: 'ux', - }); - - it('should render properly', function () { - render(); - - screen.getByText('Add filter'); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx deleted file mode 100644 index d5938c5387e8f..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { SeriesFilter } from '../../series_editor/columns/series_filter'; -import { SeriesConfig } from '../../types'; - -export function ReportFilters({ - seriesConfig, - seriesId, -}: { - seriesConfig: SeriesConfig; - seriesId: string; -}) { - return ( - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx deleted file mode 100644 index 12ae8560453c9..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx +++ /dev/null @@ -1,79 +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 { fireEvent, screen } from '@testing-library/react'; -import { mockAppIndexPattern, render } from '../../rtl_helpers'; -import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col'; -import { ReportTypes } from '../series_builder'; -import { DEFAULT_TIME } from '../../configurations/constants'; - -describe('ReportTypesCol', function () { - const seriesId = 'performance-distribution'; - - mockAppIndexPattern(); - - it('should render properly', function () { - render(); - screen.getByText('Performance distribution'); - screen.getByText('KPI over time'); - }); - - it('should display empty message', function () { - render(); - screen.getByText(SELECTED_DATA_TYPE_FOR_REPORT); - }); - - it('should set series on change', function () { - const { setSeries } = render( - - ); - - fireEvent.click(screen.getByText(/KPI over time/i)); - - expect(setSeries).toHaveBeenCalledWith(seriesId, { - dataType: 'ux', - selectedMetricField: undefined, - reportType: 'kpi-over-time', - time: { from: 'now-15m', to: 'now' }, - }); - expect(setSeries).toHaveBeenCalledTimes(1); - }); - - it('should set selected as filled', function () { - const initSeries = { - data: { - [seriesId]: { - dataType: 'synthetics' as const, - reportType: 'kpi-over-time' as const, - breakdown: 'monitor.status', - time: { from: 'now-15m', to: 'now' }, - isNew: true, - }, - }, - }; - - const { setSeries } = render( - , - { initSeries } - ); - - const button = screen.getByRole('button', { - name: /KPI over time/i, - }); - - expect(button.classList).toContain('euiButton--fill'); - fireEvent.click(button); - - // undefined on click selected - expect(setSeries).toHaveBeenCalledWith(seriesId, { - dataType: 'synthetics', - time: DEFAULT_TIME, - isNew: true, - }); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx deleted file mode 100644 index c4eebbfaca3eb..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ /dev/null @@ -1,108 +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 { i18n } from '@kbn/i18n'; -import { map } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import styled from 'styled-components'; -import { ReportViewType, SeriesUrl } from '../../types'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { DEFAULT_TIME } from '../../configurations/constants'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { ReportTypeItem } from '../series_builder'; - -interface Props { - seriesId: string; - reportTypes: ReportTypeItem[]; -} - -export function ReportTypesCol({ seriesId, reportTypes }: Props) { - const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage(); - - const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId); - - const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType); - - if (!restSeries.dataType) { - return ( - - ); - } - - if (!loading && !hasData) { - return ( - - ); - } - - const disabledReportTypes: ReportViewType[] = map( - reportTypes.filter( - ({ reportType }) => firstSeriesId !== seriesId && reportType !== firstSeries.reportType - ), - 'reportType' - ); - - return reportTypes?.length > 0 ? ( - - {reportTypes.map(({ reportType, label }) => ( - - - - ))} - - ) : ( - {SELECTED_DATA_TYPE_FOR_REPORT} - ); -} - -export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate( - 'xpack.observability.expView.reportType.noDataType', - { defaultMessage: 'No data type selected.' } -); - -const FlexGroup = styled(EuiFlexGroup)` - width: 100%; -`; - -const Button = styled(EuiButton)` - will-change: transform; -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx deleted file mode 100644 index a2a3e34c21834..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiSuperSelect } from '@elastic/eui'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { SeriesConfig } from '../types'; - -interface Props { - seriesId: string; - defaultValue?: string; - options: SeriesConfig['metricOptions']; -} - -export function ReportMetricOptions({ seriesId, options: opts }: Props) { - const { getSeries, setSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - - const onChange = (value: string) => { - setSeries(seriesId, { - ...series, - selectedMetricField: value, - }); - }; - - const options = opts ?? []; - - return ( - ({ - value: fd || id, - inputDisplay: label, - }))} - valueOfSelected={series.selectedMetricField || options?.[0].field || options?.[0].id} - onChange={(value) => onChange(value)} - /> - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx deleted file mode 100644 index 684cf3a210a51..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx +++ /dev/null @@ -1,303 +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, { RefObject, useEffect, useState } from 'react'; -import { isEmpty } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { - EuiBasicTable, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiSwitch, -} from '@elastic/eui'; -import { rgba } from 'polished'; -import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; -import { DataTypesCol } from './columns/data_types_col'; -import { ReportTypesCol } from './columns/report_types_col'; -import { ReportDefinitionCol } from './columns/report_definition_col'; -import { ReportFilters } from './columns/report_filters'; -import { ReportBreakdowns } from './columns/report_breakdowns'; -import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { getDefaultConfigs } from '../configurations/default_configs'; -import { SeriesEditor } from '../series_editor/series_editor'; -import { SeriesActions } from '../series_editor/columns/series_actions'; -import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; -import { LastUpdated } from './last_updated'; -import { - CORE_WEB_VITALS_LABEL, - DEVICE_DISTRIBUTION_LABEL, - KPI_OVER_TIME_LABEL, - PERF_DIST_LABEL, -} from '../configurations/constants/labels'; - -export interface ReportTypeItem { - id: string; - reportType: ReportViewType; - label: string; -} - -export const ReportTypes: Record = { - synthetics: [ - { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, - { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, - ], - ux: [ - { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, - { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, - { id: 'cwv', reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, - ], - mobile: [ - { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, - { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, - { id: 'mdd', reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, - ], - apm: [], - infra_logs: [], - infra_metrics: [], -}; - -interface BuilderItem { - id: string; - series: SeriesUrl; - seriesConfig?: SeriesConfig; -} - -export function SeriesBuilder({ - seriesBuilderRef, - lastUpdated, - multiSeries, -}: { - seriesBuilderRef: RefObject; - lastUpdated?: number; - multiSeries?: boolean; -}) { - const [editorItems, setEditorItems] = useState([]); - const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage(); - - const { loading, indexPatterns } = useAppIndexPatternContext(); - - useEffect(() => { - const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => { - if (indexPatterns?.[dataType]) { - return getDefaultConfigs({ - dataType, - indexPattern: indexPatterns[dataType], - reportType: reportType!, - }); - } - }; - - const seriesToEdit: BuilderItem[] = - allSeriesIds - .filter((sId) => { - return allSeries?.[sId]?.isNew; - }) - .map((sId) => { - const series = getSeries(sId); - const seriesConfig = getDataViewSeries(series.dataType, series.reportType); - - return { id: sId, series, seriesConfig }; - }) ?? []; - const initSeries: BuilderItem[] = [{ id: 'series-id', series: {} as SeriesUrl }]; - setEditorItems(multiSeries || seriesToEdit.length > 0 ? seriesToEdit : initSeries); - }, [allSeries, allSeriesIds, getSeries, indexPatterns, loading, multiSeries]); - - const columns = [ - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', { - defaultMessage: 'Data Type', - }), - field: 'id', - width: '15%', - render: (seriesId: string) => , - }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.report', { - defaultMessage: 'Report', - }), - width: '15%', - field: 'id', - render: (seriesId: string, { series: { dataType } }: BuilderItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.definition', { - defaultMessage: 'Definition', - }), - width: '30%', - field: 'id', - render: ( - seriesId: string, - { series: { dataType, reportType }, seriesConfig }: BuilderItem - ) => { - if (dataType && seriesConfig) { - return loading ? ( - LOADING_VIEW - ) : reportType ? ( - - ) : ( - SELECT_REPORT_TYPE - ); - } - - return null; - }, - }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.filters', { - defaultMessage: 'Filters', - }), - width: '20%', - field: 'id', - render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => - reportType && seriesConfig ? ( - - ) : null, - }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdown', { - defaultMessage: 'Breakdowns', - }), - width: '20%', - field: 'id', - render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => - reportType && seriesConfig ? ( - - ) : null, - }, - ...(multiSeries - ? [ - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', { - defaultMessage: 'Actions', - }), - align: 'center' as const, - width: '10%', - field: 'id', - render: (seriesId: string, item: BuilderItem) => ( - - ), - }, - ] - : []), - ]; - - const applySeries = () => { - editorItems.forEach(({ series, id: seriesId }) => { - const { reportType, reportDefinitions, isNew, ...restSeries } = series; - - if (reportType && !isEmpty(reportDefinitions)) { - const reportDefId = Object.values(reportDefinitions ?? {})[0]; - const newSeriesId = `${reportDefId}-${reportType}`; - - const newSeriesN: SeriesUrl = { - ...restSeries, - reportType, - reportDefinitions, - }; - - setSeries(newSeriesId, newSeriesN); - removeSeries(seriesId); - } - }); - }; - - const addSeries = () => { - const prevSeries = allSeries?.[allSeriesIds?.[0]]; - setSeries( - `${NEW_SERIES_KEY}-${editorItems.length + 1}`, - prevSeries - ? ({ isNew: true, time: prevSeries.time } as SeriesUrl) - : ({ isNew: true } as SeriesUrl) - ); - }; - - return ( - - {multiSeries && ( - - - - - - {}} - compressed - /> - - - applySeries()} isDisabled={true} size="s"> - {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { - defaultMessage: 'Apply changes', - })} - - - - addSeries()} size="s"> - {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { - defaultMessage: 'Add Series', - })} - - - - )} -
- {multiSeries && } - {editorItems.length > 0 && ( - - )} - -
-
- ); -} - -const Wrapper = euiStyled.div` - max-height: 50vh; - overflow-y: scroll; - overflow-x: clip; - &::-webkit-scrollbar { - height: ${({ theme }) => theme.eui.euiScrollBar}; - width: ${({ theme }) => theme.eui.euiScrollBar}; - } - &::-webkit-scrollbar-thumb { - background-clip: content-box; - background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; - border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; - } - &::-webkit-scrollbar-corner, - &::-webkit-scrollbar-track { - background-color: transparent; - } -`; - -export const LOADING_VIEW = i18n.translate( - 'xpack.observability.expView.seriesBuilder.loadingView', - { - defaultMessage: 'Loading view ...', - } -); - -export const SELECT_REPORT_TYPE = i18n.translate( - 'xpack.observability.expView.seriesBuilder.selectReportType', - { - defaultMessage: 'No report type selected', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx deleted file mode 100644 index e21da424b58c8..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiSuperDatePicker } from '@elastic/eui'; -import React, { useEffect } from 'react'; -import { useHasData } from '../../../../hooks/use_has_data'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges'; -import { DEFAULT_TIME } from '../configurations/constants'; - -export interface TimePickerTime { - from: string; - to: string; -} - -export interface TimePickerQuickRange extends TimePickerTime { - display: string; -} - -interface Props { - seriesId: string; -} - -export function SeriesDatePicker({ seriesId }: Props) { - const { onRefreshTimeRange } = useHasData(); - - const commonlyUsedRanges = useQuickTimeRanges(); - - const { getSeries, setSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - - function onTimeChange({ start, end }: { start: string; end: string }) { - onRefreshTimeRange(); - setSeries(seriesId, { ...series, time: { from: start, to: end } }); - } - - useEffect(() => { - if (!series || !series.time) { - setSeries(seriesId, { ...series, time: DEFAULT_TIME }); - } - }, [series, seriesId, setSeries]); - - return ( - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx deleted file mode 100644 index 207a53e13f1ad..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx +++ /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 React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { Breakdowns } from './columns/breakdowns'; -import { SeriesConfig } from '../types'; -import { ChartOptions } from './columns/chart_options'; - -interface Props { - seriesConfig: SeriesConfig; - seriesId: string; - breakdownFields: string[]; -} -export function ChartEditOptions({ seriesConfig, seriesId, breakdownFields }: Props) { - return ( - - - - - - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx deleted file mode 100644 index f2a6377fd9b71..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { SeriesConfig } from '../../types'; -import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select'; -import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types'; - -interface Props { - seriesConfig: SeriesConfig; - seriesId: string; -} - -export function ChartOptions({ seriesConfig, seriesId }: Props) { - return ( - - - - - {seriesConfig.hasOperationType && ( - - - - )} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx similarity index 85% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx index c054853d9c877..8f196b8a05dda 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { render } from '../../rtl_helpers'; +import { mockUxSeries, render } from '../../rtl_helpers'; import { SeriesChartTypesSelect, XYChartTypesSelect } from './chart_types'; describe.skip('SeriesChartTypesSelect', function () { it('should render properly', async function () { - render(); + render(); await waitFor(() => { screen.getByText(/chart type/i); @@ -21,7 +21,7 @@ describe.skip('SeriesChartTypesSelect', function () { it('should call set series on change', async function () { const { setSeries } = render( - + ); await waitFor(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx similarity index 77% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx index 50c2f91e6067d..27d846502dbe6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx @@ -6,11 +6,11 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIcon, EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; -import { useFetcher } from '../../../../..'; +import { SeriesUrl, useFetcher } from '../../../../..'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { SeriesType } from '../../../../../../../lens/public'; @@ -20,16 +20,14 @@ const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes. export function SeriesChartTypesSelect({ seriesId, - seriesTypes, + series, defaultChartType, }: { - seriesId: string; - seriesTypes?: SeriesType[]; + seriesId: number; + series: SeriesUrl; defaultChartType: SeriesType; }) { - const { getSeries, setSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); + const { setSeries } = useSeriesStorage(); const seriesType = series?.seriesType ?? defaultChartType; @@ -42,17 +40,15 @@ export function SeriesChartTypesSelect({ onChange={onChange} value={seriesType} excludeChartTypes={['bar_percentage_stacked']} - includeChartTypes={ - seriesTypes || [ - 'bar', - 'bar_horizontal', - 'line', - 'area', - 'bar_stacked', - 'area_stacked', - 'bar_horizontal_percentage_stacked', - ] - } + includeChartTypes={[ + 'bar', + 'bar_horizontal', + 'line', + 'area', + 'bar_stacked', + 'area_stacked', + 'bar_horizontal_percentage_stacked', + ]} label={CHART_TYPE_LABEL} /> ); @@ -105,14 +101,14 @@ export function XYChartTypesSelect({ }); return ( - + + + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx new file mode 100644 index 0000000000000..838631e1f05df --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { mockAppIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; +import { DataTypesLabels, DataTypesSelect } from './data_type_select'; +import { DataTypes } from '../../configurations/constants'; + +describe('DataTypeSelect', function () { + const seriesId = 0; + + mockAppIndexPattern(); + + it('should render properly', function () { + render(); + }); + + it('should set series on change', async function () { + const { setSeries } = render(); + + fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.UX])); + fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.SYNTHETICS])); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'synthetics', + name: 'synthetics-series-1', + time: { + from: 'now-15m', + to: 'now', + }, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx new file mode 100644 index 0000000000000..b0a6e3b5e26b0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSuperSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { AppDataType, SeriesUrl } from '../../types'; +import { DataTypes, ReportTypes } from '../../configurations/constants'; + +interface Props { + seriesId: number; + series: SeriesUrl; +} + +export const DataTypesLabels = { + [DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', { + defaultMessage: 'User experience (RUM)', + }), + + [DataTypes.SYNTHETICS]: i18n.translate( + 'xpack.observability.overview.exploratoryView.syntheticsLabel', + { + defaultMessage: 'Synthetics monitoring', + } + ), + + [DataTypes.MOBILE]: i18n.translate( + 'xpack.observability.overview.exploratoryView.mobileExperienceLabel', + { + defaultMessage: 'Mobile experience', + } + ), +}; + +export const dataTypes: Array<{ id: AppDataType; label: string }> = [ + { + id: DataTypes.SYNTHETICS, + label: DataTypesLabels[DataTypes.SYNTHETICS], + }, + { + id: DataTypes.UX, + label: DataTypesLabels[DataTypes.UX], + }, + { + id: DataTypes.MOBILE, + label: DataTypesLabels[DataTypes.MOBILE], + }, +]; + +const SELECT_DATA_TYPE = 'SELECT_DATA_TYPE'; + +export function DataTypesSelect({ seriesId, series }: Props) { + const { setSeries, reportType } = useSeriesStorage(); + + const onDataTypeChange = (dataType: AppDataType) => { + if (String(dataType) !== SELECT_DATA_TYPE) { + setSeries(seriesId, { + dataType, + time: series.time, + name: `${dataType}-series-${seriesId + 1}`, + }); + } + }; + + const options = dataTypes + .filter(({ id }) => { + if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { + return id === DataTypes.MOBILE; + } + if (reportType === ReportTypes.CORE_WEB_VITAL) { + return id === DataTypes.UX; + } + return true; + }) + .map(({ id, label }) => ({ + value: id, + inputDisplay: label, + })); + + return ( + onDataTypeChange(value as AppDataType)} + style={{ minWidth: 220 }} + /> + ); +} + +const SELECT_DATA_TYPE_LABEL = i18n.translate( + 'xpack.observability.overview.exploratoryView.selectDataType', + { + defaultMessage: 'Select data type', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx index 41e83f407af2b..032eb66dcfa4f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx @@ -6,24 +6,84 @@ */ import React from 'react'; -import { SeriesDatePicker } from '../../series_date_picker'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { DateRangePicker } from '../../series_date_picker/date_range_picker'; +import { DateRangePicker } from '../../components/date_range_picker'; +import { SeriesDatePicker } from '../../components/series_date_picker'; +import { AppDataType, SeriesUrl } from '../../types'; +import { ReportTypes } from '../../configurations/constants'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { SyntheticsAddData } from '../../../add_data_buttons/synthetics_add_data'; +import { MobileAddData } from '../../../add_data_buttons/mobile_add_data'; +import { UXAddData } from '../../../add_data_buttons/ux_add_data'; interface Props { - seriesId: string; + seriesId: number; + series: SeriesUrl; } -export function DatePickerCol({ seriesId }: Props) { - const { firstSeriesId, getSeries } = useSeriesStorage(); - const { reportType } = getSeries(firstSeriesId); + +const AddDataComponents: Record = { + mobile: MobileAddData, + ux: UXAddData, + synthetics: SyntheticsAddData, + apm: null, + infra_logs: null, + infra_metrics: null, +}; + +export function DatePickerCol({ seriesId, series }: Props) { + const { reportType } = useSeriesStorage(); + + const { hasAppData } = useAppIndexPatternContext(); + + if (!series.dataType) { + return null; + } + + const AddDataButton = AddDataComponents[series.dataType]; + if (hasAppData[series.dataType] === false && AddDataButton !== null) { + return ( + + + + {i18n.translate('xpack.observability.overview.exploratoryView.noDataAvailable', { + defaultMessage: 'No {dataType} data available.', + values: { + dataType: series.dataType, + }, + })} + + + + + + + ); + } + + if (!series.selectedMetricField) { + return null; + } return ( -
- {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( - + + {seriesId === 0 || reportType !== ReportTypes.KPI ? ( + ) : ( - + )} -
+ ); } + +const Wrapper = styled.div` + width: 100%; + .euiSuperDatePicker__flexWrapper { + width: 100%; + > .euiFlexItem { + margin-right: 0; + } + } +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx similarity index 69% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx index 516f04e3812ba..ced4d3af057ff 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx @@ -7,62 +7,66 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; -import { render } from '../../rtl_helpers'; +import { mockUxSeries, render } from '../../rtl_helpers'; import { OperationTypeSelect } from './operation_type_select'; describe('OperationTypeSelect', function () { it('should render properly', function () { - render(); + render(); screen.getByText('Select an option: , is selected'); }); it('should display selected value', function () { const initSeries = { - data: { - 'performance-distribution': { + data: [ + { + name: 'performance-distribution', dataType: 'ux' as const, - reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, - }, + ], }; - render(, { initSeries }); + render(, { + initSeries, + }); screen.getByText('Median'); }); it('should call set series on change', function () { const initSeries = { - data: { - 'series-id': { + data: [ + { + name: 'performance-distribution', dataType: 'ux' as const, - reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, - }, + ], }; - const { setSeries } = render(, { initSeries }); + const { setSeries } = render(, { + initSeries, + }); fireEvent.click(screen.getByTestId('operationTypeSelect')); - expect(setSeries).toHaveBeenCalledWith('series-id', { + expect(setSeries).toHaveBeenCalledWith(0, { operationType: 'median', dataType: 'ux', - reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, + name: 'performance-distribution', }); fireEvent.click(screen.getByText('95th Percentile')); - expect(setSeries).toHaveBeenCalledWith('series-id', { + expect(setSeries).toHaveBeenCalledWith(0, { operationType: '95th', dataType: 'ux', - reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, + name: 'performance-distribution', }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx similarity index 91% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx index fce1383f30f34..4c10c9311704d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx @@ -11,17 +11,18 @@ import { EuiSuperSelect } from '@elastic/eui'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { OperationType } from '../../../../../../../lens/public'; +import { SeriesUrl } from '../../types'; export function OperationTypeSelect({ seriesId, + series, defaultOperationType, }: { - seriesId: string; + seriesId: number; + series: SeriesUrl; defaultOperationType?: OperationType; }) { - const { getSeries, setSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); + const { setSeries } = useSeriesStorage(); const operationType = series?.operationType; @@ -83,11 +84,7 @@ export function OperationTypeSelect({ return ( { - removeSeries(seriesId); - }; - return ( - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx similarity index 69% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx index cac1eccada311..ee04ee8891302 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx @@ -12,14 +12,14 @@ import { mockAppIndexPattern, mockIndexPattern, mockUseValuesList, + mockUxSeries, render, } from '../../rtl_helpers'; import { ReportDefinitionCol } from './report_definition_col'; -import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Series Builder ReportDefinitionCol', function () { mockAppIndexPattern(); - const seriesId = 'test-series-id'; + const seriesId = 0; const seriesConfig = getDefaultConfigs({ reportType: 'data-distribution', @@ -27,34 +27,22 @@ describe('Series Builder ReportDefinitionCol', function () { dataType: 'ux', }); - const initSeries = { - data: { - [seriesId]: { - dataType: 'ux' as const, - reportType: 'data-distribution' as const, - time: { from: 'now-30d', to: 'now' }, - reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, - }, - }, - }; - mockUseValuesList([{ label: 'elastic-co', count: 10 }]); it('should render properly', async function () { - render(, { - initSeries, - }); + render( + + ); screen.getByText('Web Application'); screen.getByText('Environment'); - screen.getByText('Select an option: Page load time, is selected'); - screen.getByText('Page load time'); + screen.getByText('Search Environment'); }); it('should render selected report definitions', async function () { - render(, { - initSeries, - }); + render( + + ); expect(await screen.findByText('elastic-co')).toBeInTheDocument(); @@ -63,8 +51,7 @@ describe('Series Builder ReportDefinitionCol', function () { it('should be able to remove selected definition', async function () { const { setSeries } = render( - , - { initSeries } + ); expect( @@ -78,11 +65,14 @@ describe('Series Builder ReportDefinitionCol', function () { fireEvent.click(removeBtn); expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux', + name: 'performance-distribution', + breakdown: 'user_agent.name', reportDefinitions: {}, - reportType: 'data-distribution', - time: { from: 'now-30d', to: 'now' }, + selectedMetricField: 'transaction.duration.us', + time: { from: 'now-15m', to: 'now' }, }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx new file mode 100644 index 0000000000000..dad2a7da2367b --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesConfig, SeriesUrl } from '../../types'; +import { ReportDefinitionField } from './report_definition_field'; + +export function ReportDefinitionCol({ + seriesId, + series, + seriesConfig, +}: { + seriesId: number; + series: SeriesUrl; + seriesConfig: SeriesConfig; +}) { + const { setSeries } = useSeriesStorage(); + + const { reportDefinitions: selectedReportDefinitions = {} } = series; + + const { definitionFields } = seriesConfig; + + const onChange = (field: string, value?: string[]) => { + if (!value?.[0]) { + delete selectedReportDefinitions[field]; + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions }, + }); + } else { + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions, [field]: value }, + }); + } + }; + + return ( + + {definitionFields.map((field) => ( + + + + ))} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx similarity index 66% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx index d137b36a7e8c7..3651b4b7f075b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx @@ -6,30 +6,25 @@ */ import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty } from 'lodash'; +import { ExistsFilter } from '@kbn/es-query'; import FieldValueSuggestions from '../../../field_value_suggestions'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; import { PersistableFilter } from '../../../../../../../lens/common'; -import { ExistsFilter } from '../../../../../../../../../src/plugins/data/common/es_query/filters'; import { buildPhrasesFilter } from '../../configurations/utils'; -import { SeriesConfig } from '../../types'; +import { SeriesConfig, SeriesUrl } from '../../types'; import { ALL_VALUES_SELECTED } from '../../../field_value_suggestions/field_value_combobox'; interface Props { - seriesId: string; + seriesId: number; + series: SeriesUrl; field: string; seriesConfig: SeriesConfig; onChange: (field: string, value?: string[]) => void; } -export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange }: Props) { - const { getSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - +export function ReportDefinitionField({ series, field, seriesConfig, onChange }: Props) { const { indexPattern } = useAppIndexPatternContext(series.dataType); const { reportDefinitions: selectedReportDefinitions = {} } = series; @@ -64,23 +59,26 @@ export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(selectedReportDefinitions), JSON.stringify(baseFilters)]); + if (!indexPattern) { + return null; + } + return ( - - - {indexPattern && ( - onChange(field, val)} - filters={queryFilters} - time={series.time} - fullWidth={true} - allowAllValuesSelection={true} - /> - )} - - + onChange(field, val)} + filters={queryFilters} + time={series.time} + fullWidth={true} + asCombobox={true} + allowExclusions={false} + allowAllValuesSelection={true} + usePrependLabel={false} + compressed={false} + required={isEmpty(selectedReportDefinitions)} + /> ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx new file mode 100644 index 0000000000000..01c9fce7637bb --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx @@ -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 React from 'react'; +import { EuiSuperSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { ReportViewType } from '../../types'; +import { + CORE_WEB_VITALS_LABEL, + DEVICE_DISTRIBUTION_LABEL, + KPI_OVER_TIME_LABEL, + PERF_DIST_LABEL, +} from '../../configurations/constants/labels'; + +const SELECT_REPORT_TYPE = 'SELECT_REPORT_TYPE'; + +export const reportTypesList: Array<{ + reportType: ReportViewType | typeof SELECT_REPORT_TYPE; + label: string; +}> = [ + { + reportType: SELECT_REPORT_TYPE, + label: i18n.translate('xpack.observability.expView.reportType.selectLabel', { + defaultMessage: 'Select report type', + }), + }, + { reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, + { reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, +]; + +export function ReportTypesSelect() { + const { setReportType, reportType: selectedReportType, allSeries } = useSeriesStorage(); + + const onReportTypeChange = (reportType: ReportViewType) => { + setReportType(reportType); + }; + + const options = reportTypesList + .filter(({ reportType }) => (selectedReportType ? reportType !== SELECT_REPORT_TYPE : true)) + .map(({ reportType, label }) => ({ + value: reportType, + inputDisplay: reportType === SELECT_REPORT_TYPE ? label : {label}, + dropdownDisplay: label, + })); + + return ( + onReportTypeChange(value as ReportViewType)} + style={{ minWidth: 200 }} + isInvalid={!selectedReportType && allSeries.length > 0} + disabled={allSeries.length > 0} + /> + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx deleted file mode 100644 index 51ebe6c6bd9d5..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx +++ /dev/null @@ -1,103 +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 { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash'; -import { RemoveSeries } from './remove_series'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesUrl } from '../../types'; - -interface Props { - seriesId: string; - editorMode?: boolean; -} -export function SeriesActions({ seriesId, editorMode = false }: Props) { - const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage(); - const series = getSeries(seriesId); - - const onEdit = () => { - setSeries(seriesId, { ...series, isNew: true }); - }; - - const copySeries = () => { - let copySeriesId: string = `${seriesId}-copy`; - if (allSeriesIds.includes(copySeriesId)) { - copySeriesId = copySeriesId + allSeriesIds.length; - } - setSeries(copySeriesId, series); - }; - - const { reportType, reportDefinitions, isNew, ...restSeries } = series; - const isSaveAble = reportType && !isEmpty(reportDefinitions); - - const saveSeries = () => { - if (isSaveAble) { - const reportDefId = Object.values(reportDefinitions ?? {})[0]; - let newSeriesId = `${reportDefId}-${reportType}`; - - if (allSeriesIds.includes(newSeriesId)) { - newSeriesId = `${newSeriesId}-${allSeriesIds.length}`; - } - const newSeriesN: SeriesUrl = { - ...restSeries, - reportType, - reportDefinitions, - }; - - setSeries(newSeriesId, newSeriesN); - removeSeries(seriesId); - } - }; - - return ( - - {!editorMode && ( - - - - )} - {editorMode && ( - - - - )} - {editorMode && ( - - - - )} - - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx deleted file mode 100644 index 02144c6929b38..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import React, { useState, Fragment } from 'react'; -import { - EuiButton, - EuiPopover, - EuiSpacer, - EuiButtonEmpty, - EuiFlexItem, - EuiFlexGroup, -} from '@elastic/eui'; -import { FilterExpanded } from './filter_expanded'; -import { SeriesConfig } from '../../types'; -import { FieldLabels } from '../../configurations/constants/constants'; -import { SelectedFilters } from '../selected_filters'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; - -interface Props { - seriesId: string; - filterFields: SeriesConfig['filterFields']; - baseFilters: SeriesConfig['baseFilters']; - seriesConfig: SeriesConfig; - isNew?: boolean; - labels?: Record; -} - -export interface Field { - label: string; - field: string; - nested?: string; - isNegated?: boolean; -} - -export function SeriesFilter({ - seriesConfig, - isNew, - seriesId, - filterFields = [], - baseFilters, - labels, -}: Props) { - const [isPopoverVisible, setIsPopoverVisible] = useState(false); - - const [selectedField, setSelectedField] = useState(); - - const options: Field[] = filterFields.map((field) => { - if (typeof field === 'string') { - return { label: labels?.[field] ?? FieldLabels[field], field }; - } - - return { - field: field.field, - nested: field.nested, - isNegated: field.isNegated, - label: labels?.[field.field] ?? FieldLabels[field.field], - }; - }); - - const { setSeries, getSeries } = useSeriesStorage(); - const urlSeries = getSeries(seriesId); - - const button = ( - { - setIsPopoverVisible((prevState) => !prevState); - }} - size="s" - > - {i18n.translate('xpack.observability.expView.seriesEditor.addFilter', { - defaultMessage: 'Add filter', - })} - - ); - - const mainPanel = ( - <> - - {options.map((opt) => ( - - { - setSelectedField(opt); - }} - > - {opt.label} - - - - ))} - - ); - - const childPanel = selectedField ? ( - { - setSelectedField(undefined); - }} - filters={baseFilters} - /> - ) : null; - - const closePopover = () => { - setIsPopoverVisible(false); - setSelectedField(undefined); - }; - - return ( - - - - - {!selectedField ? mainPanel : childPanel} - - - {(urlSeries.filters ?? []).length > 0 && ( - - { - setSeries(seriesId, { ...urlSeries, filters: undefined }); - }} - size="s" - > - {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { - defaultMessage: 'Clear filters', - })} - - - )} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx new file mode 100644 index 0000000000000..801c885ec9a62 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { SeriesConfig, SeriesUrl } from '../types'; +import { ReportDefinitionCol } from './columns/report_definition_col'; +import { OperationTypeSelect } from './columns/operation_type_select'; +import { parseCustomFieldName } from '../configurations/lens_attributes'; +import { SeriesFilter } from '../series_viewer/columns/series_filter'; + +function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { + const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); + + return columnType; +} + +interface Props { + seriesId: number; + series: SeriesUrl; + seriesConfig: SeriesConfig; +} +export function ExpandedSeriesRow({ seriesId, series, seriesConfig }: Props) { + if (!seriesConfig) { + return null; + } + + const { selectedMetricField } = series ?? {}; + + const { hasOperationType, yAxisColumns } = seriesConfig; + + const columnType = getColumnType(seriesConfig, selectedMetricField); + + return ( +
+ + + + + + + + + + + + + {(hasOperationType || columnType === 'operation') && ( + + + + + + )} + + +
+ ); +} + +const FILTERS_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.selectFilters', { + defaultMessage: 'Filters', +}); + +const OPERATION_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.operation', { + defaultMessage: 'Operation', +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx new file mode 100644 index 0000000000000..85eb85e0fc30a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSuperSelect, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { SeriesConfig, SeriesUrl } from '../types'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants'; + +interface Props { + seriesId: number; + series: SeriesUrl; + defaultValue?: string; + metricOptions: SeriesConfig['metricOptions']; +} + +const SELECT_REPORT_METRIC = 'SELECT_REPORT_METRIC'; + +export function ReportMetricOptions({ seriesId, series, metricOptions }: Props) { + const { setSeries } = useSeriesStorage(); + + const { indexPatterns } = useAppIndexPatternContext(); + + const onChange = (value: string) => { + setSeries(seriesId, { + ...series, + selectedMetricField: value, + }); + }; + + if (!series.dataType) { + return null; + } + + const indexPattern = indexPatterns?.[series.dataType]; + + const options = (metricOptions ?? []).map(({ label, field, id }) => { + let disabled = false; + + if (field !== RECORDS_FIELD && field !== RECORDS_PERCENTAGE_FIELD && field) { + disabled = !Boolean(indexPattern?.getFieldByName(field)); + } + return { + disabled, + value: field || id, + dropdownDisplay: disabled ? ( + {field}, + }} + /> + } + > + {label} + + ) : ( + label + ), + inputDisplay: label, + }; + }); + + return ( + onChange(value)} + style={{ minWidth: 220 }} + /> + ); +} + +const SELECT_REPORT_METRIC_LABEL = i18n.translate( + 'xpack.observability.expView.seriesEditor.selectReportMetric', + { + defaultMessage: 'Select report metric', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx deleted file mode 100644 index 5d2ce6ba84951..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { FilterLabel } from '../components/filter_label'; -import { SeriesConfig, UrlFilter } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { useSeriesFilters } from '../hooks/use_series_filters'; -import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; - -interface Props { - seriesId: string; - seriesConfig: SeriesConfig; - isNew?: boolean; -} -export function SelectedFilters({ seriesId, isNew, seriesConfig }: Props) { - const { getSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - - const { reportDefinitions = {} } = series; - - const { labels } = seriesConfig; - - const filters: UrlFilter[] = series.filters ?? []; - - let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); - - // we don't want to display report definition filters in new series view - if (isNew) { - definitionFilters = []; - } - - const { removeFilter } = useSeriesFilters({ seriesId }); - - const { indexPattern } = useAppIndexPatternContext(series.dataType); - - return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? ( - - - {filters.map(({ field, values, notValues }) => ( - - {(values ?? []).map((val) => ( - - removeFilter({ field, value: val, negate: false })} - negate={false} - indexPattern={indexPattern} - /> - - ))} - {(notValues ?? []).map((val) => ( - - removeFilter({ field, value: val, negate: true })} - indexPattern={indexPattern} - /> - - ))} - - ))} - - {definitionFilters.map(({ field, values }) => ( - - {(values ?? []).map((val) => ( - - { - // FIXME handle this use case - }} - negate={false} - definitionFilter={true} - indexPattern={indexPattern} - /> - - ))} - - ))} - - - ) : null; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index c3cc8484d1751..80fe400830832 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -5,134 +5,399 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { SeriesFilter } from './columns/series_filter'; -import { SeriesConfig } from '../types'; -import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; +import { + EuiBasicTable, + EuiButtonIcon, + EuiSpacer, + EuiFormRow, + EuiFlexItem, + EuiFlexGroup, + EuiButtonEmpty, +} from '@elastic/eui'; +import { rgba } from 'polished'; +import classNames from 'classnames'; +import { isEmpty } from 'lodash'; +import { euiStyled } from './../../../../../../../../src/plugins/kibana_react/common'; +import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; +import { SeriesContextValue, useSeriesStorage } from '../hooks/use_series_storage'; +import { IndexPatternState, useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; import { getDefaultConfigs } from '../configurations/default_configs'; +import { SeriesActions } from '../series_viewer/columns/series_actions'; +import { SeriesInfo } from '../series_viewer/columns/series_info'; +import { DataTypesSelect } from './columns/data_type_select'; import { DatePickerCol } from './columns/date_picker_col'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { SeriesActions } from './columns/series_actions'; -import { ChartEditOptions } from './chart_edit_options'; +import { ExpandedSeriesRow } from './expanded_series_row'; +import { SeriesName } from '../series_viewer/columns/series_name'; +import { ReportTypesSelect } from './columns/report_type_select'; +import { ViewActions } from '../views/view_actions'; +import { ReportMetricOptions } from './report_metric_options'; +import { Breakdowns } from '../series_viewer/columns/breakdowns'; -interface EditItem { - seriesConfig: SeriesConfig; +export interface ReportTypeItem { id: string; + reportType: ReportViewType; + label: string; +} + +export interface BuilderItem { + id: number; + series: SeriesUrl; + seriesConfig: SeriesConfig; } -export function SeriesEditor() { - const { allSeries, allSeriesIds } = useSeriesStorage(); +type ExpandedRowMap = Record; + +export const getSeriesToEdit = ({ + indexPatterns, + allSeries, + reportType, +}: { + allSeries: SeriesContextValue['allSeries']; + indexPatterns: IndexPatternState; + reportType: ReportViewType; +}): BuilderItem[] => { + const getDataViewSeries = (dataType: AppDataType) => { + if (indexPatterns?.[dataType]) { + return getDefaultConfigs({ + dataType, + reportType, + indexPattern: indexPatterns[dataType], + }); + } + }; + + return allSeries.map((series, seriesIndex) => { + const seriesConfig = getDataViewSeries(series.dataType)!; + + return { id: seriesIndex, series, seriesConfig }; + }); +}; + +export const SeriesEditor = React.memo(function () { + const [editorItems, setEditorItems] = useState([]); + + const { getSeries, allSeries, reportType, removeSeries } = useSeriesStorage(); + + const { loading, indexPatterns } = useAppIndexPatternContext(); + + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( + {} + ); + + useEffect(() => { + const newExpandRows: ExpandedRowMap = {}; + + setEditorItems((prevState) => { + const newEditorItems = getSeriesToEdit({ + reportType, + allSeries, + indexPatterns, + }); + + newEditorItems.forEach(({ series, id, seriesConfig }) => { + const prevSeriesItem = prevState.find(({ id: prevId }) => prevId === id); + if ( + prevSeriesItem && + series.selectedMetricField && + prevSeriesItem.series.selectedMetricField !== series.selectedMetricField + ) { + newExpandRows[id] = ( + + ); + } + }); + return [...newEditorItems]; + }); + + setItemIdToExpandedRowMap((prevState) => { + return { ...prevState, ...newExpandRows }; + }); + }, [allSeries, getSeries, indexPatterns, loading, reportType]); + + useEffect(() => { + setItemIdToExpandedRowMap((prevState) => { + const itemIdToExpandedRowMapValues = { ...prevState }; + + const newEditorItems = getSeriesToEdit({ + reportType, + allSeries, + indexPatterns, + }); + + newEditorItems.forEach((item) => { + if (itemIdToExpandedRowMapValues[item.id]) { + itemIdToExpandedRowMapValues[item.id] = ( + + ); + } + }); + return itemIdToExpandedRowMapValues; + }); + }, [allSeries, editorItems, indexPatterns, reportType]); + + const toggleDetails = (item: BuilderItem) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + if (itemIdToExpandedRowMapValues[item.id]) { + delete itemIdToExpandedRowMapValues[item.id]; + } else { + itemIdToExpandedRowMapValues[item.id] = ( + + ); + } + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }; const columns = [ + { + align: 'left' as const, + width: '40px', + isExpander: true, + field: 'id', + name: '', + render: (id: number, item: BuilderItem) => + item.series.dataType && item.series.selectedMetricField ? ( + toggleDetails(item)} + isDisabled={!item.series.dataType || !item.series.selectedMetricField} + aria-label={itemIdToExpandedRowMap[item.id] ? COLLAPSE_LABEL : EXPAND_LABEL} + iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} + /> + ) : null, + }, + { + name: '', + field: 'id', + width: '40px', + render: (seriesId: number, { seriesConfig, series }: BuilderItem) => ( + + ), + }, { name: i18n.translate('xpack.observability.expView.seriesEditor.name', { defaultMessage: 'Name', }), field: 'id', - width: '15%', - render: (seriesId: string) => ( - - {' '} - {seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId} - + width: '20%', + render: (seriesId: number, { series }: BuilderItem) => ( + ), }, { - name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { - defaultMessage: 'Filters', + name: i18n.translate('xpack.observability.expView.seriesEditor.dataType', { + defaultMessage: 'Data type', }), - field: 'defaultFilters', + field: 'id', width: '15%', - render: (seriesId: string, { seriesConfig, id }: EditItem) => ( - + render: (seriesId: number, { series }: BuilderItem) => ( + ), }, { - name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', { - defaultMessage: 'Breakdowns', + name: i18n.translate('xpack.observability.expView.seriesEditor.reportMetric', { + defaultMessage: 'Report metric', }), field: 'id', - width: '25%', - render: (seriesId: string, { seriesConfig, id }: EditItem) => ( - ( + ), }, { - name: ( -
- -
+ name: i18n.translate('xpack.observability.expView.seriesEditor.time', { + defaultMessage: 'Time', + }), + field: 'id', + width: '27%', + render: (seriesId: number, { series }: BuilderItem) => ( + ), - width: '20%', + }, + + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdownBy', { + defaultMessage: 'Breakdown by', + }), + width: '10%', field: 'id', - align: 'right' as const, - render: (seriesId: string, item: EditItem) => , + render: (seriesId: number, { series, seriesConfig }: BuilderItem) => ( + + ), }, + { - name: i18n.translate('xpack.observability.expView.seriesEditor.actions', { + name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', { defaultMessage: 'Actions', }), align: 'center' as const, - width: '10%', + width: '8%', field: 'id', - render: (seriesId: string, item: EditItem) => , + render: (seriesId: number, { series, seriesConfig }: BuilderItem) => ( + + ), }, ]; - const { indexPatterns } = useAppIndexPatternContext(); - const items: EditItem[] = []; - - allSeriesIds.forEach((seriesKey) => { - const series = allSeries[seriesKey]; - if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) { - items.push({ - id: seriesKey, - seriesConfig: getDefaultConfigs({ - indexPattern: indexPatterns[series.dataType], - reportType: series.reportType, - dataType: series.dataType, - }), - }); - } - }); + const getRowProps = (item: BuilderItem) => { + const { dataType, reportDefinitions, selectedMetricField } = item.series; - if (items.length === 0 && allSeriesIds.length > 0) { - return null; - } + return { + className: classNames({ + isExpanded: itemIdToExpandedRowMap[item.id], + isIncomplete: !dataType || isEmpty(reportDefinitions) || !selectedMetricField, + }), + // commenting this for now, since adding on click on row, blocks adding space + // into text field for name column + // ...(dataType && selectedMetricField + // ? { + // onClick: (evt: MouseEvent) => { + // const targetElem = evt.target as HTMLElement; + // + // if ( + // targetElem.classList.contains('euiTableCellContent') && + // targetElem.tagName !== 'BUTTON' + // ) { + // toggleDetails(item); + // } + // evt.stopPropagation(); + // evt.preventDefault(); + // }, + // } + // : {}), + }; + }; + + const resetView = () => { + const totalSeries = allSeries.length; + for (let i = totalSeries; i >= 0; i--) { + removeSeries(i); + } + setEditorItems([]); + setItemIdToExpandedRowMap({}); + }; return ( - <> - - - - + +
+ + + + + + + + {reportType && ( + + resetView()} color="text"> + {RESET_LABEL} + + + )} + + + + + + + + {editorItems.length > 0 && ( + + )} + +
+
); -} +}); + +const Wrapper = euiStyled.div` + max-height: 50vh; + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &&& { + .euiTableRow-isExpandedRow .euiTableRowCell { + border-top: none; + background-color: #FFFFFF; + border-bottom: 2px solid #d3dae6; + border-right: 2px solid rgb(211, 218, 230); + border-left: 2px solid rgb(211, 218, 230); + } + + .isExpanded { + border-right: 2px solid rgb(211, 218, 230); + border-left: 2px solid rgb(211, 218, 230); + .euiTableRowCell { + border-bottom: none; + } + } + .isIncomplete .euiTableRowCell { + background-color: rgba(254, 197, 20, 0.1); + } + } +`; + +export const LOADING_VIEW = i18n.translate( + 'xpack.observability.expView.seriesBuilder.loadingView', + { + defaultMessage: 'Loading view ...', + } +); + +export const SELECT_REPORT_TYPE = i18n.translate( + 'xpack.observability.expView.seriesBuilder.selectReportType', + { + defaultMessage: 'No report type selected', + } +); + +export const RESET_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.reset', { + defaultMessage: 'Reset', +}); + +export const REPORT_TYPE_LABEL = i18n.translate( + 'xpack.observability.expView.seriesBuilder.reportType', + { + defaultMessage: 'Report type', + } +); + +const COLLAPSE_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.collapse', { + defaultMessage: 'Collapse', +}); + +const EXPAND_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.expand', { + defaultMessage: 'Exapnd', +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx similarity index 74% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx index 84568e1c5068a..21b766227a562 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { Breakdowns } from './breakdowns'; -import { mockIndexPattern, render } from '../../rtl_helpers'; +import { mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; @@ -20,13 +20,7 @@ describe('Breakdowns', function () { }); it('should render properly', async function () { - render( - - ); + render(); screen.getAllByText('Browser family'); }); @@ -36,9 +30,9 @@ describe('Breakdowns', function () { const { setSeries } = render( , { initSeries } ); @@ -49,10 +43,14 @@ describe('Breakdowns', function () { fireEvent.click(screen.getByText('Browser family')); - expect(setSeries).toHaveBeenCalledWith('series-id', { + expect(setSeries).toHaveBeenCalledWith(0, { breakdown: 'user_agent.name', dataType: 'ux', - reportType: 'data-distribution', + name: 'performance-distribution', + reportDefinitions: { + 'service.name': ['elastic-co'], + }, + selectedMetricField: 'transaction.duration.us', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx similarity index 71% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx index 2237935d466ad..315f63e33bed0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx @@ -8,20 +8,20 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useRouteMatch } from 'react-router-dom'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants'; -import { SeriesConfig } from '../../types'; +import { SeriesConfig, SeriesUrl } from '../../types'; interface Props { - seriesId: string; - breakdowns: string[]; + seriesId: number; + series: SeriesUrl; seriesConfig: SeriesConfig; } -export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { - const { setSeries, getSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); +export function Breakdowns({ seriesConfig, seriesId, series }: Props) { + const { setSeries } = useSeriesStorage(); + const isPreview = !!useRouteMatch('/exploratory-view/preview'); const selectedBreakdown = series.breakdown; const NO_BREAKDOWN = 'no_breakdown'; @@ -40,9 +40,13 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { } }; + if (!seriesConfig) { + return null; + } + const hasUseBreakdownColumn = seriesConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN; - const items = breakdowns.map((breakdown) => ({ + const items = seriesConfig.breakdownFields.map((breakdown) => ({ id: breakdown, label: seriesConfig.labels[breakdown], })); @@ -50,14 +54,12 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { if (!hasUseBreakdownColumn) { items.push({ id: NO_BREAKDOWN, - label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', { - defaultMessage: 'No breakdown', - }), + label: NO_BREAK_DOWN_LABEL, }); } const options = items.map(({ id, label }) => ({ - inputDisplay: id === NO_BREAKDOWN ? label : {label}, + inputDisplay: label, value: id, dropdownDisplay: label, })); @@ -69,7 +71,7 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) {
onOptionChange(value)} @@ -78,3 +80,10 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) {
); } + +export const NO_BREAK_DOWN_LABEL = i18n.translate( + 'xpack.observability.exp.breakDownFilter.noBreakdown', + { + defaultMessage: 'No breakdown', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx new file mode 100644 index 0000000000000..e6ba505c82091 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { EuiPopover, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + useKibana, + ToolbarButton, +} from '../../../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; +import { SeriesUrl, useFetcher } from '../../../../..'; +import { SeriesConfig } from '../../types'; +import { SeriesChartTypesSelect } from '../../series_editor/columns/chart_types'; + +interface Props { + seriesId: number; + series: SeriesUrl; + seriesConfig: SeriesConfig; +} + +export function SeriesChartTypes({ seriesId, series, seriesConfig }: Props) { + const seriesType = series?.seriesType ?? seriesConfig.defaultSeriesType; + + const { + services: { lens }, + } = useKibana(); + + const { data = [] } = useFetcher(() => lens.getXyVisTypes(), [lens]); + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + setIsPopoverOpen(false)} + button={ + + id === seriesType)?.icon!} + aria-label={CHART_TYPE_LABEL} + onClick={() => setIsPopoverOpen((prevState) => !prevState)} + /> + + } + > + + + ); +} + +const EDIT_CHART_TYPE_LABEL = i18n.translate( + 'xpack.observability.expView.seriesEditor.editChartSeriesLabel', + { + defaultMessage: 'Edit chart type for series', + } +); + +const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.label', { + defaultMessage: 'Chart type', +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx similarity index 73% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx index 2fadb0e56433e..2657765bde8e3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx @@ -8,20 +8,24 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { FilterExpanded } from './filter_expanded'; -import { mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers'; +import { mockUxSeries, mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers'; import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('FilterExpanded', function () { + const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }]; + + const mockSeries = { ...mockUxSeries, filters }; + it('should render properly', async function () { - const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; + const initSeries = { filters }; mockAppIndexPattern(); render( , { initSeries } @@ -30,42 +34,36 @@ describe('FilterExpanded', function () { screen.getByText('Browser Family'); }); it('should call go back on click', async function () { - const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; - const goBack = jest.fn(); + const initSeries = { filters }; render( , { initSeries } ); fireEvent.click(screen.getByText('Browser Family')); - - expect(goBack).toHaveBeenCalledTimes(1); - expect(goBack).toHaveBeenCalledWith(); }); it('should call useValuesList on load', async function () { - const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; + const initSeries = { filters }; const { spy } = mockUseValuesList([ { label: 'Chrome', count: 10 }, { label: 'Firefox', count: 5 }, ]); - const goBack = jest.fn(); - render( , { initSeries } @@ -80,7 +78,7 @@ describe('FilterExpanded', function () { ); }); it('should filter display values', async function () { - const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; + const initSeries = { filters }; mockUseValuesList([ { label: 'Chrome', count: 10 }, @@ -89,15 +87,17 @@ describe('FilterExpanded', function () { render( , { initSeries } ); + fireEvent.click(screen.getByText('Browser Family')); + expect(screen.queryByText('Firefox')).toBeTruthy(); fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'ch' } }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx similarity index 54% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx index 6f9d8efdc0681..1ef25722aff5c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx @@ -6,48 +6,56 @@ */ import React, { useState, Fragment } from 'react'; -import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup, EuiText } from '@elastic/eui'; +import { + EuiFieldSearch, + EuiSpacer, + EuiFilterGroup, + EuiText, + EuiPopover, + EuiFilterButton, +} from '@elastic/eui'; import styled from 'styled-components'; import { rgba } from 'polished'; import { i18n } from '@kbn/i18n'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { map } from 'lodash'; +import { ExistsFilter } from '@kbn/es-query'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesConfig, UrlFilter } from '../../types'; +import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; import { PersistableFilter } from '../../../../../../../lens/common'; -import { ExistsFilter } from '../../../../../../../../../src/plugins/data/common/es_query/filters'; interface Props { - seriesId: string; + seriesId: number; + series: SeriesUrl; label: string; field: string; isNegated?: boolean; - goBack: () => void; nestedField?: string; filters: SeriesConfig['baseFilters']; } +export interface NestedFilterOpen { + value: string; + negate: boolean; +} + export function FilterExpanded({ seriesId, + series, field, label, - goBack, nestedField, isNegated, filters: defaultFilters, }: Props) { const [value, setValue] = useState(''); - const [isOpen, setIsOpen] = useState({ value: '', negate: false }); - - const { getSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); + const [isOpen, setIsOpen] = useState(false); + const [isNestedOpen, setIsNestedOpen] = useState({ value: '', negate: false }); const queryFilters: ESFilter[] = []; @@ -81,62 +89,71 @@ export function FilterExpanded({ ); return ( - - goBack()}> - {label} - - { - setValue(evt.target.value); - }} - placeholder={i18n.translate('xpack.observability.filters.expanded.search', { - defaultMessage: 'Search for {label}', - values: { label }, - })} - /> - - - {displayValues.length === 0 && !loading && ( - - {i18n.translate('xpack.observability.filters.expanded.noFilter', { - defaultMessage: 'No filters found.', - })} - - )} - {displayValues.map((opt) => ( - - - {isNegated !== false && ( + setIsOpen((prevState) => !prevState)} iconType="arrowDown"> + {label} + + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + > + + { + setValue(evt.target.value); + }} + placeholder={i18n.translate('xpack.observability.filters.expanded.search', { + defaultMessage: 'Search for {label}', + values: { label }, + })} + /> + + + {displayValues.length === 0 && !loading && ( + + {i18n.translate('xpack.observability.filters.expanded.noFilter', { + defaultMessage: 'No filters found.', + })} + + )} + {displayValues.map((opt) => ( + + + {isNegated !== false && ( + + )} - )} - - - - - ))} - - + + + + ))} + + + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx similarity index 91% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx index c1790fea8c0c4..409b9d93444b7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { FilterValueButton } from './filter_value_btn'; -import { mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; +import { mockUxSeries, mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; import { USER_AGENT_NAME, USER_AGENT_VERSION, @@ -19,11 +19,12 @@ describe('FilterValueButton', function () { render( ); @@ -34,11 +35,12 @@ describe('FilterValueButton', function () { render( ); @@ -54,12 +56,13 @@ describe('FilterValueButton', function () { render( ); @@ -80,12 +83,13 @@ describe('FilterValueButton', function () { render( ); @@ -104,12 +108,13 @@ describe('FilterValueButton', function () { render( ); @@ -129,13 +134,14 @@ describe('FilterValueButton', function () { render( ); @@ -160,13 +166,14 @@ describe('FilterValueButton', function () { render( ); @@ -194,13 +201,14 @@ describe('FilterValueButton', function () { render( ); @@ -219,13 +227,14 @@ describe('FilterValueButton', function () { render( ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx similarity index 92% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx index bf4ca6eb83d94..111f915a95f46 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx @@ -8,10 +8,11 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { EuiFilterButton, hexToRgb } from '@elastic/eui'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useSeriesFilters } from '../../hooks/use_series_filters'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import FieldValueSuggestions from '../../../field_value_suggestions'; +import { SeriesUrl } from '../../types'; +import { NestedFilterOpen } from './filter_expanded'; interface Props { value: string; @@ -19,12 +20,13 @@ interface Props { allSelectedValues?: string[]; negate: boolean; nestedField?: string; - seriesId: string; + seriesId: number; + series: SeriesUrl; isNestedOpen: { value: string; negate: boolean; }; - setIsNestedOpen: (val: { value: string; negate: boolean }) => void; + setIsNestedOpen: (val: NestedFilterOpen) => void; } export function FilterValueButton({ @@ -34,16 +36,13 @@ export function FilterValueButton({ field, negate, seriesId, + series, nestedField, allSelectedValues, }: Props) { - const { getSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - const { indexPatterns } = useAppIndexPatternContext(series.dataType); - const { setFilter, removeFilter } = useSeriesFilters({ seriesId }); + const { setFilter, removeFilter } = useSeriesFilters({ seriesId, series }); const hasActiveFilters = (allSelectedValues ?? []).includes(value); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx new file mode 100644 index 0000000000000..2d38b81e12c9f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: number; +} + +export function RemoveSeries({ seriesId }: Props) { + const { removeSeries, allSeries } = useSeriesStorage(); + + const onClick = () => { + removeSeries(seriesId); + }; + + const isDisabled = seriesId === 0 && allSeries.length > 1; + + return ( + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx new file mode 100644 index 0000000000000..72ae111f002b1 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { RemoveSeries } from './remove_series'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesConfig, SeriesUrl } from '../../types'; +import { useDiscoverLink } from '../../hooks/use_discover_link'; + +interface Props { + seriesId: number; + series: SeriesUrl; + seriesConfig: SeriesConfig; +} +export function SeriesActions({ seriesId, series, seriesConfig }: Props) { + const { setSeries, allSeries } = useSeriesStorage(); + + const { href: discoverHref } = useDiscoverLink({ series, seriesConfig }); + + const copySeries = () => { + let copySeriesId: string = `${series.name}-copy`; + if (allSeries.find(({ name }) => name === copySeriesId)) { + copySeriesId = copySeriesId + allSeries.length; + } + setSeries(allSeries.length, { ...series, name: copySeriesId }); + }; + + const toggleSeries = () => { + if (series.hidden) { + setSeries(seriesId, { ...series, hidden: undefined }); + } else { + setSeries(seriesId, { ...series, hidden: true }); + } + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx new file mode 100644 index 0000000000000..87c17d03282c3 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFilterGroup, EuiSpacer } from '@elastic/eui'; +import { useRouteMatch } from 'react-router-dom'; +import { FilterExpanded } from './filter_expanded'; +import { SeriesConfig, SeriesUrl } from '../../types'; +import { FieldLabels } from '../../configurations/constants/constants'; +import { SelectedFilters } from '../selected_filters'; + +interface Props { + seriesId: number; + seriesConfig: SeriesConfig; + series: SeriesUrl; +} + +export interface Field { + label: string; + field: string; + nested?: string; + isNegated?: boolean; +} + +export function SeriesFilter({ series, seriesConfig, seriesId }: Props) { + const isPreview = !!useRouteMatch('/exploratory-view/preview'); + + const options: Field[] = seriesConfig.filterFields.map((field) => { + if (typeof field === 'string') { + return { label: seriesConfig.labels?.[field] ?? FieldLabels[field], field }; + } + + return { + field: field.field, + nested: field.nested, + isNegated: field.isNegated, + label: seriesConfig.labels?.[field.field] ?? FieldLabels[field.field], + }; + }); + + return ( + <> + {!isPreview && ( + <> + + {options.map((opt) => ( + + ))} + + + + )} + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx new file mode 100644 index 0000000000000..3506acbeb528d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { isEmpty } from 'lodash'; +import { EuiBadge, EuiBadgeGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useRouteMatch } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { SeriesChartTypes } from './chart_types'; +import { SeriesConfig, SeriesUrl } from '../../types'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { SeriesColorPicker } from '../../components/series_color_picker'; +import { dataTypes } from '../../series_editor/columns/data_type_select'; + +interface Props { + seriesId: number; + series: SeriesUrl; + seriesConfig?: SeriesConfig; +} + +export function SeriesInfo({ seriesId, series, seriesConfig }: Props) { + const isConfigure = !!useRouteMatch('/exploratory-view/configure'); + + const { dataType, reportDefinitions, selectedMetricField } = series; + + const { loading } = useAppIndexPatternContext(); + + const isIncomplete = + (!dataType || isEmpty(reportDefinitions) || !selectedMetricField) && !loading; + + if (!seriesConfig) { + return null; + } + + const { definitionFields, labels } = seriesConfig; + + const incompleteDefinition = isEmpty(reportDefinitions) + ? i18n.translate('xpack.observability.overview.exploratoryView.missingReportDefinition', { + defaultMessage: 'Missing {reportDefinition}', + values: { reportDefinition: labels?.[definitionFields[0]] }, + }) + : ''; + + let incompleteMessage = !selectedMetricField ? MISSING_REPORT_METRIC_LABEL : incompleteDefinition; + + if (!dataType) { + incompleteMessage = MISSING_DATA_TYPE_LABEL; + } + + if (!isIncomplete && seriesConfig && isConfigure) { + return ( + + + + + + + + + ); + } + + return ( + + + {isIncomplete && {incompleteMessage}} + + {!isConfigure && ( + + + {dataTypes.find(({ id }) => id === dataType)!.label} + + + )} + + ); +} + +const MISSING_REPORT_METRIC_LABEL = i18n.translate( + 'xpack.observability.overview.exploratoryView.missingReportMetric', + { + defaultMessage: 'Missing report metric', + } +); + +const MISSING_DATA_TYPE_LABEL = i18n.translate( + 'xpack.observability.overview.exploratoryView.missingDataType', + { + defaultMessage: 'Missing data type', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx new file mode 100644 index 0000000000000..e35966a9fb0d2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, ChangeEvent, useEffect } from 'react'; +import { EuiFieldText } from '@elastic/eui'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesUrl } from '../../types'; + +interface Props { + seriesId: number; + series: SeriesUrl; +} + +export function SeriesName({ series, seriesId }: Props) { + const { setSeries } = useSeriesStorage(); + + const [value, setValue] = useState(series.name); + + const onChange = (e: ChangeEvent) => { + setValue(e.target.value); + }; + + const onSave = () => { + if (value !== series.name) { + setSeries(seriesId, { ...series, name: value }); + } + }; + + useEffect(() => { + setValue(series.name); + }, [series.name]); + + return ; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts new file mode 100644 index 0000000000000..b9ee53a7e8e2d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import dateMath from '@elastic/datemath'; +import _isString from 'lodash/isString'; + +const LAST = 'Last'; +const NEXT = 'Next'; + +const isNow = (value: string) => value === 'now'; + +export const isString = (value: any): value is string => _isString(value); +export interface QuickSelect { + timeTense: string; + timeValue: number; + timeUnits: TimeUnitId; +} +export type TimeUnitFromNowId = 's+' | 'm+' | 'h+' | 'd+' | 'w+' | 'M+' | 'y+'; +export type TimeUnitId = 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y'; + +export interface RelativeOption { + text: string; + value: TimeUnitId | TimeUnitFromNowId; +} + +export const relativeOptions: RelativeOption[] = [ + { text: 'Seconds ago', value: 's' }, + { text: 'Minutes ago', value: 'm' }, + { text: 'Hours ago', value: 'h' }, + { text: 'Days ago', value: 'd' }, + { text: 'Weeks ago', value: 'w' }, + { text: 'Months ago', value: 'M' }, + { text: 'Years ago', value: 'y' }, + + { text: 'Seconds from now', value: 's+' }, + { text: 'Minutes from now', value: 'm+' }, + { text: 'Hours from now', value: 'h+' }, + { text: 'Days from now', value: 'd+' }, + { text: 'Weeks from now', value: 'w+' }, + { text: 'Months from now', value: 'M+' }, + { text: 'Years from now', value: 'y+' }, +]; + +const timeUnitIds = relativeOptions + .map(({ value }) => value) + .filter((value) => !value.includes('+')) as TimeUnitId[]; + +export const relativeUnitsFromLargestToSmallest = timeUnitIds.reverse(); + +/** + * This function returns time value, time unit and time tense for a given time string. + * + * For example: for `now-40m` it will parse output as time value to `40` time unit to `m` and time unit to `last`. + * + * If given a datetime string it will return a default value. + * + * If the given string is in the format such as `now/d` it will parse the string to moment object and find the time value, time unit and time tense using moment + * + * This function accepts two strings start and end time. I the start value is now then it uses the end value to parse. + */ +export function parseTimeParts(start: string, end: string): QuickSelect | null { + const value = isNow(start) ? end : start; + + const matches = isString(value) && value.match(/now(([-+])(\d+)([smhdwMy])(\/[smhdwMy])?)?/); + + if (!matches) { + return null; + } + + const operator = matches[2]; + const matchedTimeValue = matches[3]; + const timeUnits = matches[4] as TimeUnitId; + + if (matchedTimeValue && timeUnits && operator) { + return { + timeTense: operator === '+' ? NEXT : LAST, + timeUnits, + timeValue: parseInt(matchedTimeValue, 10), + }; + } + + const duration = moment.duration(moment().diff(dateMath.parse(value))); + let unitOp = ''; + for (let i = 0; i < relativeUnitsFromLargestToSmallest.length; i++) { + const as = duration.as(relativeUnitsFromLargestToSmallest[i]); + if (as < 0) { + unitOp = '+'; + } + if (Math.abs(as) > 1) { + return { + timeValue: Math.round(Math.abs(as)), + timeUnits: relativeUnitsFromLargestToSmallest[i], + timeTense: unitOp === '+' ? NEXT : LAST, + }; + } + } + + return null; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx similarity index 71% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx index eb76772a66c7e..8fc5ae95fd41b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; +import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; import { SelectedFilters } from './selected_filters'; import { getDefaultConfigs } from '../configurations/default_configs'; import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames'; @@ -22,11 +22,19 @@ describe('SelectedFilters', function () { }); it('should render properly', async function () { - const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; + const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }]; + const initSeries = { filters }; - render(, { - initSeries, - }); + render( + , + { + initSeries, + } + ); await waitFor(() => { screen.getByText('Chrome'); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx new file mode 100644 index 0000000000000..46adba1dbde55 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment } from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useRouteMatch } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FilterLabel } from '../components/filter_label'; +import { SeriesConfig, SeriesUrl, UrlFilter } from '../types'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { useSeriesFilters } from '../hooks/use_series_filters'; +import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; +import { useSeriesStorage } from '../hooks/use_series_storage'; + +interface Props { + seriesId: number; + series: SeriesUrl; + seriesConfig: SeriesConfig; +} +export function SelectedFilters({ seriesId, series, seriesConfig }: Props) { + const { setSeries } = useSeriesStorage(); + + const isPreview = !!useRouteMatch('/exploratory-view/preview'); + + const { reportDefinitions = {} } = series; + + const { labels } = seriesConfig; + + const filters: UrlFilter[] = series.filters ?? []; + + let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); + + const isConfigure = !!useRouteMatch('/exploratory-view/configure'); + + // we don't want to display report definition filters in new series view + if (isConfigure) { + definitionFilters = []; + } + + const { removeFilter } = useSeriesFilters({ seriesId, series }); + + const { indexPattern } = useAppIndexPatternContext(series.dataType); + + if ((filters.length === 0 && definitionFilters.length === 0) || !indexPattern) { + return null; + } + + return ( + + {filters.map(({ field, values, notValues }) => ( + + {(values ?? []).length > 0 && ( + + { + values?.forEach((val) => { + removeFilter({ field, value: val, negate: false }); + }); + }} + negate={false} + indexPattern={indexPattern} + /> + + )} + {(notValues ?? []).length > 0 && ( + + { + values?.forEach((val) => { + removeFilter({ field, value: val, negate: false }); + }); + }} + indexPattern={indexPattern} + /> + + )} + + ))} + + {definitionFilters.map(({ field, values }) => + values ? ( + + {}} + negate={false} + definitionFilter={true} + indexPattern={indexPattern} + /> + + ) : null + )} + + {(series.filters ?? []).length > 0 && !isPreview && ( + + { + setSeries(seriesId, { ...series, filters: undefined }); + }} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { + defaultMessage: 'Clear filters', + })} + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx new file mode 100644 index 0000000000000..85d65dcac6ac3 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx @@ -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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { EuiBasicTable, EuiSpacer, EuiText } from '@elastic/eui'; +import { SeriesFilter } from './columns/series_filter'; +import { SeriesConfig, SeriesUrl } from '../types'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { getDefaultConfigs } from '../configurations/default_configs'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { SeriesInfo } from './columns/series_info'; +import { SeriesDatePicker } from '../components/series_date_picker'; +import { NO_BREAK_DOWN_LABEL } from './columns/breakdowns'; + +interface EditItem { + id: number; + series: SeriesUrl; + seriesConfig: SeriesConfig; +} + +export function SeriesViewer() { + const { allSeries, reportType } = useSeriesStorage(); + + const columns = [ + { + name: '', + field: 'id', + width: '10%', + render: (seriesId: number, { seriesConfig, series }: EditItem) => ( + + ), + }, + { + name: i18n.translate('xpack.observability.expView.seriesEditor.name', { + defaultMessage: 'Name', + }), + field: 'id', + width: '15%', + render: (seriesId: number, { series }: EditItem) => {series.name}, + }, + { + name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { + defaultMessage: 'Filters', + }), + field: 'id', + width: '35%', + render: (seriesId: number, { series, seriesConfig }: EditItem) => ( + + ), + }, + { + name: i18n.translate('xpack.observability.expView.seriesEditor.breakdownBy', { + defaultMessage: 'Breakdown by', + }), + field: 'seriesId', + width: '10%', + render: (seriesId: number, { seriesConfig: { labels }, series }: EditItem) => ( + {series.breakdown ? labels[series.breakdown] : NO_BREAK_DOWN_LABEL} + ), + }, + { + name: i18n.translate('xpack.observability.expView.seriesEditor.time', { + defaultMessage: 'Time', + }), + width: '30%', + field: 'id', + render: (seriesId: number, { series }: EditItem) => ( + + ), + }, + ]; + + const { indexPatterns } = useAppIndexPatternContext(); + const items: EditItem[] = []; + + allSeries.forEach((series, seriesIndex) => { + if (indexPatterns[series.dataType] && !isEmpty(series.reportDefinitions)) { + items.push({ + series, + id: seriesIndex, + seriesConfig: getDefaultConfigs({ + reportType, + dataType: series.dataType, + indexPattern: indexPatterns[series.dataType], + }), + }); + } + }); + + if (items.length === 0 && allSeries.length > 0) { + return null; + } + + return ( + <> + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 717d98715453d..4bba0c221f3c5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -6,6 +6,7 @@ */ import { PaletteOutput } from 'src/plugins/charts/public'; +import { ExistsFilter, PhraseFilter } from '@kbn/es-query'; import { LastValueIndexPatternColumn, DateHistogramIndexPatternColumn, @@ -16,8 +17,7 @@ import { } from '../../../../../lens/public'; import { PersistableFilter } from '../../../../../lens/common'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; -import { ExistsFilter } from '../../../../../../../src/plugins/data/common/es_query/filters'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/public'; export const ReportViewTypes = { dist: 'data-distribution', @@ -42,7 +42,7 @@ export interface MetricOption { field?: string; label: string; description?: string; - columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN'; + columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN' | 'unique_count'; columnFilters?: ColumnFilter[]; timeScale?: string; } @@ -55,7 +55,7 @@ export interface SeriesConfig { defaultSeriesType: SeriesType; filterFields: Array; seriesTypes: SeriesType[]; - baseFilters?: PersistableFilter[] | ExistsFilter[]; + baseFilters?: Array; definitionFields: string[]; metricOptions?: MetricOption[]; labels: Record; @@ -69,6 +69,7 @@ export interface SeriesConfig { export type URLReportDefinition = Record; export interface SeriesUrl { + name: string; time: { to: string; from: string; @@ -76,12 +77,12 @@ export interface SeriesUrl { breakdown?: string; filters?: UrlFilter[]; seriesType?: SeriesType; - reportType: ReportViewType; operationType?: OperationType; dataType: AppDataType; reportDefinitions?: URLReportDefinition; selectedMetricField?: string; - isNew?: boolean; + hidden?: boolean; + color?: string; } export interface UrlFilter { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx new file mode 100644 index 0000000000000..e0b46102caba0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.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 React, { RefObject, useEffect, useState } from 'react'; + +import { EuiTabs, EuiTab, EuiButtonIcon } from '@elastic/eui'; +import { useHistory, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { SeriesEditor } from '../series_editor/series_editor'; +import { SeriesViewer } from '../series_viewer/series_viewer'; +import { PanelId } from '../exploratory_view'; + +const tabs = [ + { + id: 'preview' as const, + name: i18n.translate('xpack.observability.overview.exploratoryView.preview', { + defaultMessage: 'Preview', + }), + }, + { + id: 'configure' as const, + name: i18n.translate('xpack.observability.overview.exploratoryView.configureSeries', { + defaultMessage: 'Configure series', + }), + }, +]; + +type ViewTab = 'preview' | 'configure'; + +export function SeriesViews({ + seriesBuilderRef, + onSeriesPanelCollapse, +}: { + seriesBuilderRef: RefObject; + onSeriesPanelCollapse: (panel: PanelId) => void; +}) { + const params = useParams<{ mode: ViewTab }>(); + + const history = useHistory(); + + const [selectedTabId, setSelectedTabId] = useState('configure'); + + const onSelectedTabChanged = (id: ViewTab) => { + setSelectedTabId(id); + history.push('/exploratory-view/' + id); + }; + + useEffect(() => { + setSelectedTabId(params.mode); + }, [params.mode]); + + const renderTabs = () => { + return tabs.map((tab, index) => ( + onSelectedTabChanged(tab.id)} + isSelected={tab.id === selectedTabId} + key={index} + > + {tab.id === 'preview' && selectedTabId === 'preview' ? ( + + onSeriesPanelCollapse('seriesPanel')} + /> +  {tab.name} + + ) : ( + tab.name + )} + + )); + }; + + return ( +
+ {renderTabs()} + {selectedTabId === 'preview' && } + {selectedTabId === 'configure' && } +
+ ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx new file mode 100644 index 0000000000000..db1f23ad9b6e3 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.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 React, { useEffect, useState } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSwitch, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEqual } from 'lodash'; +import { + allSeriesKey, + convertAllShortSeries, + NEW_SERIES_KEY, + useSeriesStorage, +} from '../hooks/use_series_storage'; +import { SeriesUrl } from '../types'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { BuilderItem, getSeriesToEdit } from '../series_editor/series_editor'; +import { DEFAULT_TIME, ReportTypes } from '../configurations/constants'; + +export function ViewActions() { + const [editorItems, setEditorItems] = useState([]); + const { + getSeries, + allSeries, + setSeries, + storage, + reportType, + autoApply, + setAutoApply, + applyChanges, + } = useSeriesStorage(); + + const { loading, indexPatterns } = useAppIndexPatternContext(); + + useEffect(() => { + setEditorItems(getSeriesToEdit({ allSeries, indexPatterns, reportType })); + }, [allSeries, getSeries, indexPatterns, loading, reportType]); + + const addSeries = () => { + const prevSeries = allSeries?.[0]; + const name = `${NEW_SERIES_KEY}-${editorItems.length + 1}`; + const nextSeries = { name } as SeriesUrl; + + const nextSeriesId = allSeries.length; + + if (reportType === 'data-distribution') { + setSeries(nextSeriesId, { + ...nextSeries, + time: prevSeries?.time || DEFAULT_TIME, + } as SeriesUrl); + } else { + setSeries( + nextSeriesId, + prevSeries ? nextSeries : ({ ...nextSeries, time: DEFAULT_TIME } as SeriesUrl) + ); + } + }; + + const noChanges = isEqual(allSeries, convertAllShortSeries(storage.get(allSeriesKey) ?? [])); + + const isAddDisabled = + !reportType || + ((reportType === ReportTypes.CORE_WEB_VITAL || + reportType === ReportTypes.DEVICE_DISTRIBUTION) && + allSeries.length > 0); + + return ( + + + setAutoApply(!autoApply)} + compressed + /> + + {!autoApply && ( + + applyChanges()} isDisabled={autoApply || noChanges} fill> + {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { + defaultMessage: 'Apply changes', + })} + + + )} + + + addSeries()} isDisabled={isAddDisabled}> + {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { + defaultMessage: 'Add series', + })} + + + + + ); +} + +const AUTO_APPLY_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.autoApply', { + defaultMessage: 'Auto apply', +}); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx index fc562fa80e26d..0735df53888aa 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx @@ -6,15 +6,24 @@ */ import React, { useEffect, useState } from 'react'; -import { union } from 'lodash'; -import { EuiComboBox, EuiFormControlLayout, EuiComboBoxOptionOption } from '@elastic/eui'; +import { union, isEmpty } from 'lodash'; +import { + EuiComboBox, + EuiFormControlLayout, + EuiComboBoxOptionOption, + EuiFormRow, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { FieldValueSelectionProps } from './types'; export const ALL_VALUES_SELECTED = 'ALL_VALUES'; const formatOptions = (values?: string[], allowAllValuesSelection?: boolean) => { const uniqueValues = Array.from( - new Set(allowAllValuesSelection ? ['ALL_VALUES', ...(values ?? [])] : values) + new Set( + allowAllValuesSelection && (values ?? []).length > 0 + ? ['ALL_VALUES', ...(values ?? [])] + : values + ) ); return (uniqueValues ?? []).map((label) => ({ @@ -30,7 +39,9 @@ export function FieldValueCombobox({ loading, values, setQuery, + usePrependLabel = true, compressed = true, + required = true, allowAllValuesSelection, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -54,29 +65,35 @@ export function FieldValueCombobox({ onSelectionChange(selectedValuesN.map(({ label: lbl }) => lbl)); }; - return ( + const comboBox = ( + { + setQuery(searchVal); + }} + options={options} + selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))} + onChange={onChange} + isInvalid={required && isEmpty(selectedValue)} + /> + ); + + return usePrependLabel ? ( - { - setQuery(searchVal); - }} - options={options} - selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))} - onChange={onChange} - /> + {comboBox} + ) : ( + + {comboBox} + ); } diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx index f713af9768229..cee3ab8aea28b 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx @@ -70,6 +70,7 @@ export function FieldValueSelection({ values = [], selectedValue, excludedValue, + allowExclusions = true, compressed = true, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -173,8 +174,8 @@ export function FieldValueSelection({ }} options={options} onChange={onChange} + allowExclusions={allowExclusions} isLoading={loading && !query && options.length === 0} - allowExclusions={true} > {(list, search) => (
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx index 3c7d0851531b4..6671c43dd8c7b 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx @@ -7,14 +7,13 @@ import React from 'react'; import { FieldValueSuggestions } from './index'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitForElementToBeRemoved } from '@testing-library/react'; import * as searchHook from '../../../hooks/use_es_search'; import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; jest.setTimeout(30000); -// flaky https://github.com/elastic/kibana/issues/105784 -describe.skip('FieldValueSuggestions', () => { +describe('FieldValueSuggestions', () => { jest.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(1500); jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(1500); @@ -96,6 +95,7 @@ describe.skip('FieldValueSuggestions', () => { selectedValue={[]} filters={[]} asCombobox={false} + allowExclusions={true} /> ); @@ -108,6 +108,8 @@ describe.skip('FieldValueSuggestions', () => { expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(['US'], []); + await waitForElementToBeRemoved(() => screen.queryByText('Apply')); + rerender( { excludedValue={['Pak']} filters={[]} asCombobox={false} + allowExclusions={true} /> ); + fireEvent.click(screen.getByText('Service name')); + fireEvent.click(await screen.findByText('US')); fireEvent.click(await screen.findByText('Pak')); fireEvent.click(await screen.findByText('Apply')); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx index 54114c7604644..65e1d0932e4ed 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx @@ -28,7 +28,10 @@ export function FieldValueSuggestions({ singleSelection, compressed, asFilterButton, + usePrependLabel, allowAllValuesSelection, + required, + allowExclusions = true, asCombobox = true, onChange: onSelectionChange, }: FieldValueSuggestionsProps) { @@ -64,7 +67,10 @@ export function FieldValueSuggestions({ width={width} compressed={compressed} asFilterButton={asFilterButton} + usePrependLabel={usePrependLabel} + allowExclusions={allowExclusions} allowAllValuesSelection={allowAllValuesSelection} + required={required} /> ); } diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts index d857b39b074ac..73b3d78ce8700 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts @@ -23,7 +23,10 @@ interface CommonProps { compressed?: boolean; asFilterButton?: boolean; showCount?: boolean; + usePrependLabel?: boolean; + allowExclusions?: boolean; allowAllValuesSelection?: boolean; + required?: boolean; } export type FieldValueSuggestionsProps = CommonProps & { diff --git a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx index 01d727071770d..82392b5c23bf9 100644 --- a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx @@ -18,21 +18,25 @@ export function buildFilterLabel({ negate, }: { label: string; - value: string; + value: string | string[]; negate: boolean; field: string; indexPattern: IndexPattern; }) { const indexField = indexPattern.getFieldByName(field)!; - const filter = esFilters.buildPhraseFilter(indexField, value, indexPattern); + const filter = + value instanceof Array && value.length > 1 + ? esFilters.buildPhrasesFilter(indexField, value, indexPattern) + : esFilters.buildPhraseFilter(indexField, value, indexPattern); - filter.meta.value = value; + filter.meta.type = value instanceof Array && value.length > 1 ? 'phrases' : 'phrase'; + + filter.meta.value = value as string; filter.meta.key = label; filter.meta.alias = null; filter.meta.negate = negate; filter.meta.disabled = false; - filter.meta.type = 'phrase'; return filter; } @@ -40,10 +44,10 @@ export function buildFilterLabel({ interface Props { field: string; label: string; - value: string; + value: string | string[]; negate: boolean; - removeFilter: (field: string, value: string, notVal: boolean) => void; - invertFilter: (val: { field: string; value: string; negate: boolean }) => void; + removeFilter: (field: string, value: string | string[], notVal: boolean) => void; + invertFilter: (val: { field: string; value: string | string[]; negate: boolean }) => void; indexPattern: IndexPattern; allowExclusion?: boolean; } diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx index 9d557a40b7987..afc053604fcdf 100644 --- a/x-pack/plugins/observability/public/components/shared/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/index.tsx @@ -6,6 +6,7 @@ */ import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import type { CoreVitalProps, HeaderMenuPortalProps } from './types'; import type { FieldValueSuggestionsProps } from './field_value_suggestions/types'; @@ -26,7 +27,7 @@ const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); export function HeaderMenuPortal(props: HeaderMenuPortalProps) { return ( - + }> ); diff --git a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx index 82a0fc39b8519..198b4092b0ed6 100644 --- a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx +++ b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx @@ -7,7 +7,7 @@ import { useUiSetting } from '../../../../../src/plugins/kibana_react/public'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; -import { TimePickerQuickRange } from '../components/shared/exploratory_view/series_date_picker'; +import { TimePickerQuickRange } from '../components/shared/exploratory_view/components/series_date_picker'; export function useQuickTimeRanges() { const timePickerQuickRanges = useUiSetting( diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 7b1ef5b0bad7a..f2ff2a01ebed0 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -24,6 +24,7 @@ import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../src/plugins/data/public'; +import type { DiscoverStart } from '../../../../src/plugins/discover/public'; import type { HomePublicPluginSetup, HomePublicPluginStart, @@ -56,6 +57,7 @@ export interface ObservabilityPublicPluginsStart { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; lens: LensPublicStart; + discover: DiscoverStart; } export type ObservabilityPublicStart = ReturnType; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index f97e3fb996441..09d22496c98ff 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -7,6 +7,7 @@ import * as t from 'io-ts'; import React from 'react'; +import { Redirect } from 'react-router-dom'; import { alertStatusRt } from '../../common/typings'; import { ExploratoryViewPage } from '../components/shared/exploratory_view'; import { AlertsPage } from '../pages/alerts'; @@ -99,7 +100,20 @@ export const routes = { }), }, }, - '/exploratory-view': { + '/exploratory-view/': { + handler: () => { + return ; + }, + params: { + query: t.partial({ + rangeFrom: t.string, + rangeTo: t.string, + refreshPaused: jsonRt.pipe(t.boolean), + refreshInterval: jsonRt.pipe(t.number), + }), + }, + }, + '/exploratory-view/:mode': { handler: () => { return ; }, @@ -112,18 +126,4 @@ export const routes = { }), }, }, - // enable this to test multi series architecture - // '/exploratory-view/multi': { - // handler: () => { - // return ; - // }, - // params: { - // query: t.partial({ - // rangeFrom: t.string, - // rangeTo: t.string, - // refreshPaused: jsonRt.pipe(t.boolean), - // refreshInterval: jsonRt.pipe(t.number), - // }), - // }, - // }, }; diff --git a/x-pack/plugins/osquery/common/typed_json.ts b/x-pack/plugins/osquery/common/typed_json.ts index 8ce6907beb80b..7ef7469a5ebe7 100644 --- a/x-pack/plugins/osquery/common/typed_json.ts +++ b/x-pack/plugins/osquery/common/typed_json.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { DslQuery, Filter } from 'src/plugins/data/common'; - +import { DslQuery, Filter } from '@kbn/es-query'; import { JsonObject } from '@kbn/common-utils'; export type ESQuery = diff --git a/x-pack/plugins/osquery/public/saved_queries/use_create_saved_query.ts b/x-pack/plugins/osquery/public/saved_queries/use_create_saved_query.ts index 1d10d80bd6fbf..ddc25630079cd 100644 --- a/x-pack/plugins/osquery/public/saved_queries/use_create_saved_query.ts +++ b/x-pack/plugins/osquery/public/saved_queries/use_create_saved_query.ts @@ -36,16 +36,15 @@ export const useCreateSavedQuery = ({ withRedirect }: UseCreateSavedQueryProps) if (!currentUser) { throw new Error('CurrentUser is missing'); } - + // @ts-expect-error update types + const payloadId = payload.id; const conflictingEntries = await savedObjects.client.find({ type: savedQuerySavedObjectType, - // @ts-expect-error update types - search: payload.id, + search: payloadId, searchFields: ['id'], }); if (conflictingEntries.savedObjects.length) { - // @ts-expect-error update types - throw new Error(`Saved query with id ${payload.id} already exists.`); + throw new Error(`Saved query with id ${payloadId} already exists.`); } return savedObjects.client.create(savedQuerySavedObjectType, { // @ts-expect-error update types @@ -58,6 +57,12 @@ export const useCreateSavedQuery = ({ withRedirect }: UseCreateSavedQueryProps) }, { onError: (error) => { + if (error instanceof Error) { + return setErrorToast(error, { + title: 'Saved query creation error', + toastMessage: error.message, + }); + } // @ts-expect-error update types setErrorToast(error, { title: error.body.error, toastMessage: error.body.message }); }, diff --git a/x-pack/plugins/osquery/public/saved_queries/use_update_saved_query.ts b/x-pack/plugins/osquery/public/saved_queries/use_update_saved_query.ts index fe0d38648b23c..45b7a4c2d6b9a 100644 --- a/x-pack/plugins/osquery/public/saved_queries/use_update_saved_query.ts +++ b/x-pack/plugins/osquery/public/saved_queries/use_update_saved_query.ts @@ -37,15 +37,21 @@ export const useUpdateSavedQuery = ({ savedQueryId }: UseUpdateSavedQueryProps) throw new Error('CurrentUser is missing'); } + // @ts-expect-error update types + const payloadId = payload.id; const conflictingEntries = await savedObjects.client.find({ type: savedQuerySavedObjectType, - // @ts-expect-error update types - search: payload.id, + search: payloadId, searchFields: ['id'], }); - if (conflictingEntries.savedObjects.length) { - // @ts-expect-error update types - throw new Error(`Saved query with id ${payload.id} already exists.`); + const conflictingObjects = conflictingEntries.savedObjects; + // we some how have more than one object with the same id + const updateConflicts = + conflictingObjects.length > 1 || + // or the one we conflict with isn't the same one we are updating + (conflictingObjects.length && conflictingObjects[0].id !== savedQueryId); + if (updateConflicts) { + throw new Error(`Saved query with id ${payloadId} already exists.`); } return savedObjects.client.update(savedQuerySavedObjectType, savedQueryId, { @@ -57,6 +63,12 @@ export const useUpdateSavedQuery = ({ savedQueryId }: UseUpdateSavedQueryProps) }, { onError: (error) => { + if (error instanceof Error) { + return setErrorToast(error, { + title: 'Saved query update error', + toastMessage: error.message, + }); + } // @ts-expect-error update types setErrorToast(error, { title: error.body.error, toastMessage: error.body.message }); }, diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index c95c837c4959f..c9a763fae52fe 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -92,7 +92,6 @@ export const API_LIST_URL = `${API_BASE_URL}/jobs`; export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; export const API_GET_ILM_POLICY_STATUS = `${API_BASE_URL}/ilm_policy_status`; -export const API_CREATE_ILM_POLICY_URL = `${API_BASE_URL}/ilm_policy`; export const API_MIGRATE_ILM_POLICY_URL = `${API_BASE_URL}/deprecations/migrate_ilm_policy`; export const ILM_POLICY_NAME = 'kibana-reporting'; diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index 6a8f3a3e4e5ec..4bddfae96756d 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -2,6 +2,11 @@ "id": "reporting", "version": "8.0.0", "kibanaVersion": "kibana", + "owner": { + "name": "Kibana Reporting Services", + "githubTeam": "kibana-reporting-services" + }, + "description": "Reporting Services enables applications to feature reports that the user can automate with Watcher and download later.", "optionalPlugins": ["security", "spaces", "usageCollection"], "configPath": ["xpack", "reporting"], "requiredPlugins": [ diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx b/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx index 37857943774d4..4070f0d6d388d 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx @@ -9,6 +9,7 @@ import type { HttpSetup } from 'src/core/public'; import type { FunctionComponent } from 'react'; import React, { createContext, useContext } from 'react'; +import { useKibana } from '../../shared_imports'; import type { ReportingAPIClient } from './reporting_api_client'; interface ContextValue { @@ -19,9 +20,12 @@ interface ContextValue { const InternalApiClientContext = createContext(undefined); export const InternalApiClientClientProvider: FunctionComponent<{ - http: HttpSetup; apiClient: ReportingAPIClient; -}> = ({ http, apiClient, children }) => { +}> = ({ apiClient, children }) => { + const { + services: { http }, + } = useKibana(); + return ( {children} diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts index afd8222fd3831..0b697b333dddd 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts @@ -10,9 +10,11 @@ import { IlmPolicyStatusResponse } from '../../../common/types'; import { API_GET_ILM_POLICY_STATUS } from '../../../common/constants'; -import { useInternalApiClient } from './context'; +import { useKibana } from '../../shared_imports'; export const useCheckIlmPolicyStatus = (): UseRequestResponse => { - const { http } = useInternalApiClient(); + const { + services: { http }, + } = useKibana(); return useRequest(http, { path: API_GET_ILM_POLICY_STATUS, method: 'get' }); }; diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap index 4dac77d4c1db4..d0ed2d737b584 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap @@ -659,6 +659,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -849,6 +860,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -1039,6 +1061,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -1230,6 +1263,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -1387,6 +1431,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -1930,6 +1985,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -2120,6 +2186,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -2310,6 +2387,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -2501,6 +2589,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -2658,6 +2757,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -3201,6 +3311,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -3438,6 +3559,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -3628,6 +3760,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -3819,6 +3962,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -3976,6 +4130,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -4546,6 +4711,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -4785,6 +4961,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -4977,6 +5164,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -5170,6 +5368,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -5327,6 +5536,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -5870,6 +6090,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -6107,6 +6338,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -6297,6 +6539,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -6488,6 +6741,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -6645,6 +6909,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -7188,6 +7463,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -7425,6 +7711,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -7615,6 +7912,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -7806,6 +8114,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -7963,6 +8282,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -8506,6 +8836,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -8743,6 +9084,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -8933,6 +9285,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -9124,6 +9487,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -9281,6 +9655,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -9824,6 +10209,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -10061,6 +10457,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -10251,6 +10658,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -10442,6 +10860,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -10599,6 +11028,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -11142,6 +11582,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -11379,6 +11830,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -11569,6 +12031,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -11760,6 +12233,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, @@ -11917,6 +12401,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "total": [Function], } } + capabilities={ + Object { + "catalogue": Object {}, + "management": Object { + "data": Object { + "index_lifecycle_management": true, + }, + }, + "navLinks": Object {}, + } + } ilmPolicyContextValue={ Object { "isLoading": false, diff --git a/x-pack/plugins/reporting/public/management/mount_management_section.tsx b/x-pack/plugins/reporting/public/management/mount_management_section.tsx index 8d147628c6662..20ea2988f3b8b 100644 --- a/x-pack/plugins/reporting/public/management/mount_management_section.tsx +++ b/x-pack/plugins/reporting/public/management/mount_management_section.tsx @@ -15,6 +15,7 @@ import { ReportingAPIClient, InternalApiClientClientProvider } from '../lib/repo import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context'; import { ClientConfigType } from '../plugin'; import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports'; +import { KibanaContextProvider } from '../shared_imports'; import { ReportListing } from './report_listing'; export async function mountManagementSection( @@ -28,18 +29,22 @@ export async function mountManagementSection( ) { render( - - - - - + + + + + + + , params.element ); diff --git a/x-pack/plugins/reporting/public/management/report_delete_button.tsx b/x-pack/plugins/reporting/public/management/report_delete_button.tsx index 5200191184972..7009a653c1bf6 100644 --- a/x-pack/plugins/reporting/public/management/report_delete_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_delete_button.tsx @@ -92,7 +92,7 @@ export class ReportDeleteButton extends PureComponent { {intl.formatMessage( { id: 'xpack.reporting.listing.table.deleteReportButton', - defaultMessage: `Delete ({num})`, + defaultMessage: `Delete {num, plural, one {report} other {reports} }`, }, { num: jobsToDelete.length } )} diff --git a/x-pack/plugins/reporting/public/management/report_listing.test.tsx b/x-pack/plugins/reporting/public/management/report_listing.test.tsx index 0c9b85c2f8cbb..b2eb6f0029580 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.test.tsx @@ -11,13 +11,18 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { Observable } from 'rxjs'; import type { NotificationsSetup } from '../../../../../src/core/public'; -import { httpServiceMock, notificationServiceMock } from '../../../../../src/core/public/mocks'; +import { + applicationServiceMock, + httpServiceMock, + notificationServiceMock, +} from '../../../../../src/core/public/mocks'; import type { LocatorPublic, SharePluginSetup } from '../../../../../src/plugins/share/public'; import type { ILicense } from '../../../licensing/public'; import { IlmPolicyMigrationStatus, ReportApiJSON } from '../../common/types'; import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context'; import { Job } from '../lib/job'; import { InternalApiClientClientProvider, ReportingAPIClient } from '../lib/reporting_api_client'; +import { KibanaContextProvider } from '../shared_imports'; import { Props, ReportListing } from './report_listing'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { @@ -70,6 +75,7 @@ const mockPollConfig = { describe('ReportListing', () => { let httpService: ReturnType; + let applicationService: ReturnType; let ilmLocator: undefined | LocatorPublic; let urlService: SharePluginSetup['url']; let testBed: UnwrapPromise>; @@ -77,22 +83,21 @@ describe('ReportListing', () => { const createTestBed = registerTestBed( (props?: Partial) => ( - - - - - + + + + + + + ), { memoryRouter: { wrapComponent: false } } ); @@ -127,6 +132,12 @@ describe('ReportListing', () => { beforeEach(async () => { toasts = notificationServiceMock.createSetupContract().toasts; httpService = httpServiceMock.createSetupContract(); + applicationService = applicationServiceMock.createStartContract(); + applicationService.capabilities = { + catalogue: {}, + navLinks: {}, + management: { data: { index_lifecycle_management: true } }, + }; ilmLocator = ({ getUrl: jest.fn(), } as unknown) as LocatorPublic; @@ -255,5 +266,26 @@ describe('ReportListing', () => { expect(actions.hasIlmMigrationBanner()).toBe(true); expect(actions.hasIlmPolicyLink()).toBe(true); }); + + it('only shows the link to the ILM policy if UI capabilities allow it', async () => { + applicationService.capabilities = { + catalogue: {}, + navLinks: {}, + management: { data: { index_lifecycle_management: false } }, + }; + await runSetup(); + + expect(testBed.actions.hasIlmPolicyLink()).toBe(false); + + applicationService.capabilities = { + catalogue: {}, + navLinks: {}, + management: { data: { index_lifecycle_management: true } }, + }; + + await runSetup(); + + expect(testBed.actions.hasIlmPolicyLink()).toBe(true); + }); }); }); diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index 30c9325a0f34f..9ba0026999137 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -31,6 +31,7 @@ import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient, useInternalApiClient } from '../lib/reporting_api_client'; import { ClientConfigType } from '../plugin'; import type { SharePluginSetup } from '../shared_imports'; +import { useKibana } from '../shared_imports'; import { ReportDeleteButton, ReportDownloadButton, ReportErrorButton, ReportInfoButton } from './'; import { IlmPolicyLink } from './ilm_policy_link'; import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; @@ -39,6 +40,7 @@ import { ReportDiagnostic } from './report_diagnostic'; export interface Props { intl: InjectedIntl; apiClient: ReportingAPIClient; + capabilities: ApplicationStart['capabilities']; license$: LicensingPluginSetup['license$']; pollConfig: ClientConfigType['poll']; redirect: ApplicationStart['navigateToApp']; @@ -122,7 +124,7 @@ class ReportListingUi extends Component { } public render() { - const { ilmPolicyContextValue, urlService, navigateToUrl } = this.props; + const { ilmPolicyContextValue, urlService, navigateToUrl, capabilities } = this.props; const ilmLocator = urlService.locators.get('ILM_LOCATOR_ID'); const hasIlmPolicy = ilmPolicyContextValue.status !== 'policy-not-found'; const showIlmPolicyLink = Boolean(ilmLocator && hasIlmPolicy); @@ -145,19 +147,21 @@ class ReportListingUi extends Component { - {this.renderTable()} +
{this.renderTable()}
- - {ilmPolicyContextValue.isLoading ? ( - - ) : ( - showIlmPolicyLink && ( - - ) - )} - + {capabilities?.management?.data?.index_lifecycle_management && ( + + {ilmPolicyContextValue.isLoading ? ( + + ) : ( + showIlmPolicyLink && ( + + ) + )} + + )} @@ -485,6 +489,14 @@ class ReportListingUi extends Component { return ( + {this.state.selectedJobs.length > 0 && ( + + + {this.renderDeleteButton()} + + + + )} { onChange={this.onTableChange} data-test-subj="reportJobListing" /> - {this.state.selectedJobs.length > 0 ? this.renderDeleteButton() : null} ); } @@ -519,14 +530,20 @@ class ReportListingUi extends Component { const PrivateReportListing = injectI18n(ReportListingUi); export const ReportListing = ( - props: Omit + props: Omit ) => { const ilmPolicyStatusValue = useIlmPolicyStatus(); const { apiClient } = useInternalApiClient(); + const { + services: { + application: { capabilities }, + }, + } = useKibana(); return ( ); diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index fcbc4662c6e59..44ecc01bd1eb3 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -158,7 +158,11 @@ export class ReportingPublicPlugin getStartServices(), import('./management/mount_management_section'), ]); - return await mountManagementSection( + const { + chrome: { docTitle }, + } = start; + docTitle.change(this.title); + const umountAppCallback = await mountManagementSection( core, start, license$, @@ -167,6 +171,11 @@ export class ReportingPublicPlugin share.url, params ); + + return () => { + docTitle.reset(); + umountAppCallback(); + }; }, }); diff --git a/x-pack/plugins/reporting/public/shared_imports.ts b/x-pack/plugins/reporting/public/shared_imports.ts index 010da46c07401..02717351e315f 100644 --- a/x-pack/plugins/reporting/public/shared_imports.ts +++ b/x-pack/plugins/reporting/public/shared_imports.ts @@ -13,6 +13,12 @@ export type { export { useRequest, UseRequestResponse } from '../../../../src/plugins/es_ui_shared/public'; +export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; + +import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public'; +import { KibanaContext } from './types'; +export const useKibana = () => _useKibana(); + export type { SerializableState } from 'src/plugins/kibana_utils/common'; export type { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts b/x-pack/plugins/reporting/public/types.ts similarity index 53% rename from x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts rename to x-pack/plugins/reporting/public/types.ts index 07f1903fb70e1..cb1344bb982ec 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts +++ b/x-pack/plugins/reporting/public/types.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { HttpSetup, ApplicationStart } from 'src/core/public'; -export interface GetGenericComboBoxPropsReturn { - comboOptions: EuiComboBoxOptionOption[]; - labels: string[]; - selectedComboOptions: EuiComboBoxOptionOption[]; +export interface KibanaContext { + http: HttpSetup; + application: ApplicationStart; } diff --git a/x-pack/plugins/reporting/server/deprecations.ts b/x-pack/plugins/reporting/server/deprecations.ts deleted file mode 100644 index 61074fff012a2..0000000000000 --- a/x-pack/plugins/reporting/server/deprecations.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 { CoreSetup, DeprecationsDetails, RegisterDeprecationsConfig } from 'src/core/server'; -import { ReportingCore } from '.'; - -const deprecatedRole = 'reporting_user'; -const upgradableConfig = 'xpack.reporting.roles.enabled: false'; - -export async function registerDeprecations( - reporting: ReportingCore, - { deprecations: deprecationsService }: CoreSetup -) { - const deprecationsConfig: RegisterDeprecationsConfig = { - getDeprecations: async ({ esClient }) => { - const usingDeprecatedConfig = !reporting.getContract().usesUiCapabilities(); - const deprecations: DeprecationsDetails[] = []; - const { body: users } = await esClient.asCurrentUser.security.getUser(); - - const reportingUsers = Object.entries(users) - .filter(([username, user]) => user.roles.includes(deprecatedRole)) - .map(([, user]) => user.username); - const numReportingUsers = reportingUsers.length; - - if (numReportingUsers > 0) { - const usernames = reportingUsers.join('", "'); - deprecations.push({ - message: `The deprecated "${deprecatedRole}" role has been found for ${numReportingUsers} user(s): "${usernames}"`, - documentationUrl: 'https://www.elastic.co/guide/en/kibana/current/secure-reporting.html', - level: 'critical', - correctiveActions: { - manualSteps: [ - ...(usingDeprecatedConfig ? [`Set "${upgradableConfig}" in kibana.yml`] : []), - `Create one or more custom roles that provide Kibana application privileges to reporting features in **Management > Security > Roles**.`, - `Assign the custom role(s) as desired, and remove the "${deprecatedRole}" role from the user(s).`, - ], - }, - }); - } - - return deprecations; - }, - }; - - deprecationsService.registerDeprecations(deprecationsConfig); - - return deprecationsConfig; -} diff --git a/x-pack/plugins/reporting/server/deprecations/index.ts b/x-pack/plugins/reporting/server/deprecations/index.ts new file mode 100644 index 0000000000000..9ecb3b7ab88ad --- /dev/null +++ b/x-pack/plugins/reporting/server/deprecations/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { CoreSetup } from 'src/core/server'; +import { ReportingCore } from '../core'; + +import { getDeprecationsInfo as getIlmPolicyDeprecationsInfo } from './migrate_existing_indices_ilm_policy'; +import { getDeprecationsInfo as getReportingRoleDeprecationsInfo } from './reporting_role'; + +export const registerDeprecations = ({ + core, + reportingCore, +}: { + core: CoreSetup; + reportingCore: ReportingCore; +}) => { + core.deprecations.registerDeprecations({ + getDeprecations: async (ctx) => { + return [ + ...(await getIlmPolicyDeprecationsInfo(ctx, { reportingCore })), + ...(await getReportingRoleDeprecationsInfo(ctx, { reportingCore })), + ]; + }, + }); +}; diff --git a/x-pack/plugins/reporting/server/deprecations/migrage_existing_indices_ilm_policy.test.ts b/x-pack/plugins/reporting/server/deprecations/migrage_existing_indices_ilm_policy.test.ts new file mode 100644 index 0000000000000..485c4e62a208f --- /dev/null +++ b/x-pack/plugins/reporting/server/deprecations/migrage_existing_indices_ilm_policy.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { GetDeprecationsContext } from 'src/core/server'; +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; + +import { ReportingCore } from '../core'; +import { createMockConfigSchema, createMockReportingCore } from '../test_helpers'; + +import { getDeprecationsInfo } from './migrate_existing_indices_ilm_policy'; + +type ScopedClusterClientMock = ReturnType< + typeof elasticsearchServiceMock.createScopedClusterClient +>; + +const { createApiResponse } = elasticsearchServiceMock; + +describe("Migrate existing indices' ILM policy deprecations", () => { + let esClient: ScopedClusterClientMock; + let deprecationsCtx: GetDeprecationsContext; + let reportingCore: ReportingCore; + + beforeEach(async () => { + esClient = elasticsearchServiceMock.createScopedClusterClient(); + deprecationsCtx = { esClient, savedObjectsClient: savedObjectsClientMock.create() }; + reportingCore = await createMockReportingCore(createMockConfigSchema()); + }); + + const createIndexSettings = (lifecycleName: string) => ({ + aliases: {}, + mappings: {}, + settings: { + index: { + lifecycle: { + name: lifecycleName, + }, + }, + }, + }); + + it('returns deprecation information when reporting indices are not using the reporting ILM policy', async () => { + esClient.asInternalUser.indices.getSettings.mockResolvedValueOnce( + createApiResponse({ + body: { + indexA: createIndexSettings('not-reporting-lifecycle'), + indexB: createIndexSettings('kibana-reporting'), + }, + }) + ); + + expect(await getDeprecationsInfo(deprecationsCtx, { reportingCore })).toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "api": Object { + "method": "PUT", + "path": "/api/reporting/deprecations/migrate_ilm_policy", + }, + "manualSteps": Array [ + "Update all reporting indices to use the \\"kibana-reporting\\" policy using the index settings API.", + ], + }, + "level": "warning", + "message": "New reporting indices will be managed by the \\"kibana-reporting\\" provisioned ILM policy. You must edit this policy to manage the report lifecycle. This change targets all indices prefixed with \\".reporting-*\\".", + }, + ] + `); + }); + + it('does not return deprecations when all reporting indices are managed by the provisioned ILM policy', async () => { + esClient.asInternalUser.indices.getSettings.mockResolvedValueOnce( + createApiResponse({ + body: { + indexA: createIndexSettings('kibana-reporting'), + indexB: createIndexSettings('kibana-reporting'), + }, + }) + ); + + expect(await getDeprecationsInfo(deprecationsCtx, { reportingCore })).toMatchInlineSnapshot( + `Array []` + ); + + esClient.asInternalUser.indices.getSettings.mockResolvedValueOnce( + createApiResponse({ + body: {}, + }) + ); + + expect(await getDeprecationsInfo(deprecationsCtx, { reportingCore })).toMatchInlineSnapshot( + `Array []` + ); + }); +}); diff --git a/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.ts b/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.ts new file mode 100644 index 0000000000000..a3dd4205b9e65 --- /dev/null +++ b/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { DeprecationsDetails, GetDeprecationsContext } from 'src/core/server'; +import { API_MIGRATE_ILM_POLICY_URL, ILM_POLICY_NAME } from '../../common/constants'; +import { ReportingCore } from '../core'; +import { deprecations } from '../lib/deprecations'; + +interface ExtraDependencies { + reportingCore: ReportingCore; +} + +export const getDeprecationsInfo = async ( + { esClient }: GetDeprecationsContext, + { reportingCore }: ExtraDependencies +): Promise => { + const store = await reportingCore.getStore(); + const indexPattern = store.getReportingIndexPattern(); + + const migrationStatus = await deprecations.checkIlmMigrationStatus({ + reportingCore, + elasticsearchClient: esClient.asInternalUser, + }); + + if (migrationStatus !== 'ok') { + return [ + { + level: 'warning', + message: i18n.translate('xpack.reporting.deprecations.migrateIndexIlmPolicyActionMessage', { + defaultMessage: `New reporting indices will be managed by the "{reportingIlmPolicy}" provisioned ILM policy. You must edit this policy to manage the report lifecycle. This change targets all indices prefixed with "{indexPattern}".`, + values: { + reportingIlmPolicy: ILM_POLICY_NAME, + indexPattern, + }, + }), + correctiveActions: { + manualSteps: [ + i18n.translate( + 'xpack.reporting.deprecations.migrateIndexIlmPolicy.manualStepOneMessage', + { + defaultMessage: + 'Update all reporting indices to use the "{reportingIlmPolicy}" policy using the index settings API.', + values: { reportingIlmPolicy: ILM_POLICY_NAME }, + } + ), + ], + api: { + method: 'PUT', + path: API_MIGRATE_ILM_POLICY_URL, + }, + }, + }, + ]; + } + + return []; +}; diff --git a/x-pack/plugins/reporting/server/deprecations.test.ts b/x-pack/plugins/reporting/server/deprecations/reporting_role.test.ts similarity index 83% rename from x-pack/plugins/reporting/server/deprecations.test.ts rename to x-pack/plugins/reporting/server/deprecations/reporting_role.test.ts index cce4721b941a0..b52d51d3e9311 100644 --- a/x-pack/plugins/reporting/server/deprecations.test.ts +++ b/x-pack/plugins/reporting/server/deprecations/reporting_role.test.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { ReportingCore } from '.'; -import { registerDeprecations } from './deprecations'; -import { createMockConfigSchema, createMockReportingCore } from './test_helpers'; -import { coreMock, elasticsearchServiceMock } from 'src/core/server/mocks'; +import { ReportingCore } from '..'; +import { getDeprecationsInfo } from './reporting_role'; +import { createMockConfigSchema, createMockReportingCore } from '../test_helpers'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { GetDeprecationsContext, IScopedClusterClient } from 'kibana/server'; let reportingCore: ReportingCore; @@ -26,17 +26,22 @@ beforeEach(async () => { }); test('logs no deprecations when setup has no issues', async () => { - const { getDeprecations } = await registerDeprecations(reportingCore, coreMock.createSetup()); - expect(await getDeprecations(context)).toMatchInlineSnapshot(`Array []`); + expect( + await getDeprecationsInfo(context, { + reportingCore, + }) + ).toMatchInlineSnapshot(`Array []`); }); test('logs a plain message when only a reporting_user role issue is found', async () => { esClient.asCurrentUser.security.getUser = jest.fn().mockResolvedValue({ body: { reportron: { username: 'reportron', roles: ['kibana_admin', 'reporting_user'] } }, }); - - const { getDeprecations } = await registerDeprecations(reportingCore, coreMock.createSetup()); - expect(await getDeprecations(context)).toMatchInlineSnapshot(` + expect( + await getDeprecationsInfo(context, { + reportingCore, + }) + ).toMatchInlineSnapshot(` Array [ Object { "correctiveActions": Object { @@ -61,8 +66,11 @@ test('logs multiple entries when multiple reporting_user role issues are found', }, }); - const { getDeprecations } = await registerDeprecations(reportingCore, coreMock.createSetup()); - expect(await getDeprecations(context)).toMatchInlineSnapshot(` + expect( + await getDeprecationsInfo(context, { + reportingCore, + }) + ).toMatchInlineSnapshot(` Array [ Object { "correctiveActions": Object { @@ -87,8 +95,11 @@ test('logs an expanded message when a config issue and a reporting_user role iss const mockReportingConfig = createMockConfigSchema({ roles: { enabled: true } }); reportingCore = await createMockReportingCore(mockReportingConfig); - const { getDeprecations } = await registerDeprecations(reportingCore, coreMock.createSetup()); - expect(await getDeprecations(context)).toMatchInlineSnapshot(` + expect( + await getDeprecationsInfo(context, { + reportingCore, + }) + ).toMatchInlineSnapshot(` Array [ Object { "correctiveActions": Object { diff --git a/x-pack/plugins/reporting/server/deprecations/reporting_role.ts b/x-pack/plugins/reporting/server/deprecations/reporting_role.ts new file mode 100644 index 0000000000000..d5138043060a6 --- /dev/null +++ b/x-pack/plugins/reporting/server/deprecations/reporting_role.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { GetDeprecationsContext, DeprecationsDetails } from 'src/core/server'; +import { ReportingCore } from '..'; + +const deprecatedRole = 'reporting_user'; +const upgradableConfig = 'xpack.reporting.roles.enabled: false'; + +interface ExtraDependencies { + reportingCore: ReportingCore; +} + +export const getDeprecationsInfo = async ( + { esClient }: GetDeprecationsContext, + { reportingCore }: ExtraDependencies +): Promise => { + const usingDeprecatedConfig = !reportingCore.getContract().usesUiCapabilities(); + const deprecations: DeprecationsDetails[] = []; + const { body: users } = await esClient.asCurrentUser.security.getUser(); + + const reportingUsers = Object.entries(users) + .filter(([username, user]) => user.roles.includes(deprecatedRole)) + .map(([, user]) => user.username); + const numReportingUsers = reportingUsers.length; + + if (numReportingUsers > 0) { + const usernames = reportingUsers.join('", "'); + deprecations.push({ + message: `The deprecated "${deprecatedRole}" role has been found for ${numReportingUsers} user(s): "${usernames}"`, + documentationUrl: 'https://www.elastic.co/guide/en/kibana/current/secure-reporting.html', + level: 'critical', + correctiveActions: { + manualSteps: [ + ...(usingDeprecatedConfig ? [`Set "${upgradableConfig}" in kibana.yml`] : []), + `Create one or more custom roles that provide Kibana application privileges to reporting features in **Management > Security > Roles**.`, + `Assign the custom role(s) as desired, and remove the "${deprecatedRole}" role from the user(s).`, + ], + }, + }); + } + + return deprecations; +}; diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index dc0ddf27a53b3..185b47a980bfe 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -65,7 +65,10 @@ export class ReportingPlugin }); registerUiSettings(core); - registerDeprecations(reportingCore, core); + registerDeprecations({ + core, + reportingCore, + }); registerReportingUsageCollector(reportingCore, plugins); registerRoutes(reportingCore, this.logger); diff --git a/x-pack/plugins/reporting/server/routes/deprecations.ts b/x-pack/plugins/reporting/server/routes/deprecations.ts index 7a38faf60f6bb..0daa56274cc00 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations.ts @@ -5,6 +5,7 @@ * 2.0. */ import { errors } from '@elastic/elasticsearch'; +import { RequestHandler } from 'src/core/server'; import { API_MIGRATE_ILM_POLICY_URL, API_GET_ILM_POLICY_STATUS, @@ -18,42 +19,83 @@ import { IlmPolicyManager, LevelLogger as Logger } from '../lib'; export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Logger) => { const { router } = reporting.getPluginSetupDeps(); + const authzWrapper = (handler: RequestHandler): RequestHandler => { + return async (ctx, req, res) => { + const { security } = reporting.getPluginSetupDeps(); + if (!security) { + return handler(ctx, req, res); + } + + const { + core: { elasticsearch }, + } = ctx; + + const store = await reporting.getStore(); + + try { + const { body } = await elasticsearch.client.asCurrentUser.security.hasPrivileges({ + body: { + index: [ + { + privileges: ['manage'], // required to do anything with the reporting indices + names: [store.getReportingIndexPattern()], + }, + ], + }, + }); + + if (!body.has_all_requested) { + return res.notFound(); + } + } catch (e) { + return res.customError({ statusCode: e.statusCode, body: e.message }); + } + + return handler(ctx, req, res); + }; + }; + router.get( { path: API_GET_ILM_POLICY_STATUS, validate: false, }, - async ( - { - core: { - elasticsearch: { client: scopedClient }, + authzWrapper( + async ( + { + core: { + elasticsearch: { client: scopedClient }, + }, }, - }, - req, - res - ) => { - const checkIlmMigrationStatus = () => { - return deprecations.checkIlmMigrationStatus({ - reportingCore: reporting, - // We want to make the current status visible to all reporting users - elasticsearchClient: scopedClient.asInternalUser, - }); - }; - - try { - const response: IlmPolicyStatusResponse = { - status: await checkIlmMigrationStatus(), + req, + res + ) => { + const checkIlmMigrationStatus = () => { + return deprecations.checkIlmMigrationStatus({ + reportingCore: reporting, + // We want to make the current status visible to all reporting users + elasticsearchClient: scopedClient.asInternalUser, + }); }; - return res.ok({ body: response }); - } catch (e) { - return res.customError({ statusCode: e?.statusCode ?? 500, body: { message: e.message } }); + + try { + const response: IlmPolicyStatusResponse = { + status: await checkIlmMigrationStatus(), + }; + return res.ok({ body: response }); + } catch (e) { + return res.customError({ + statusCode: e?.statusCode ?? 500, + body: { message: e.message }, + }); + } } - } + ) ); router.put( { path: API_MIGRATE_ILM_POLICY_URL, validate: false }, - async ({ core: { elasticsearch } }, req, res) => { + authzWrapper(async ({ core: { elasticsearch } }, req, res) => { const store = await reporting.getStore(); const { client: { asCurrentUser: client }, @@ -105,6 +147,6 @@ export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Log throw err; } - } + }) ); }; 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 index 40e91171d1e03..0e0afd91134ea 100644 --- 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 @@ -11,6 +11,7 @@ import { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, ALERT_ID, + ALERT_REASON, ALERT_SEVERITY_LEVEL, ALERT_SEVERITY_VALUE, ALERT_START, @@ -55,6 +56,7 @@ export const technicalRuleFieldMap = { [ALERT_STATUS]: { type: 'keyword' }, [ALERT_EVALUATION_THRESHOLD]: { type: 'scaled_float', scaling_factor: 100 }, [ALERT_EVALUATION_VALUE]: { type: 'scaled_float', scaling_factor: 100 }, + [ALERT_REASON]: { type: 'keyword' }, } as const; export type TechnicalRuleFieldMaps = typeof technicalRuleFieldMap; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index 2a18f28710d0f..046b5bdddf6d8 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -44,7 +44,7 @@ type LifecycleAlertService< ActionGroupIds extends string = never > = (alert: { id: string; - fields: Record; + fields: Record & Partial>; }) => AlertInstance; export interface LifecycleAlertServices< @@ -141,7 +141,7 @@ export const createLifecycleExecutor = ( }) )(wrappedStateRt().decode(previousState)); - const currentAlerts: Record = {}; + const currentAlerts: Record> = {}; const timestamp = options.startedAt.toISOString(); @@ -182,12 +182,7 @@ export const createLifecycleExecutor = ( `Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStatesOfRecovered.length} recovered)` ); - const alertsDataMap: Record< - string, - { - [ALERT_ID]: string; - } - > = { + const alertsDataMap: Record> = { ...currentAlerts, }; @@ -297,27 +292,12 @@ export const createLifecycleExecutor = ( return event; }); - if (eventsToIndex.length) { - const alertEvents: Map = new Map(); - - for (const event of eventsToIndex) { - const uuid = event[ALERT_UUID]!; - let storedEvent = alertEvents.get(uuid); - if (!storedEvent) { - storedEvent = event; - } - alertEvents.set(uuid, { - ...storedEvent, - [EVENT_KIND]: 'signal', - }); - } + if (eventsToIndex.length > 0 && ruleDataClient.isWriteEnabled()) { logger.debug(`Preparing to index ${eventsToIndex.length} alerts.`); - if (ruleDataClient.isWriteEnabled()) { - await ruleDataClient.getWriter().bulk({ - body: eventsToIndex.flatMap((event) => [{ index: { _id: event[ALERT_UUID]! } }, event]), - }); - } + await ruleDataClient.getWriter().bulk({ + body: eventsToIndex.flatMap((event) => [{ index: { _id: event[ALERT_UUID]! } }, event]), + }); } const nextTrackedAlerts = Object.fromEntries( diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 27d4a5c9fd399..48a23a967059e 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -62,20 +62,21 @@ export const DEFAULT_INDICATOR_SOURCE_PATH = 'threatintel.indicator'; export const INDICATOR_DESTINATION_PATH = 'threat.indicator'; export enum SecurityPageName { - overview = 'overview', - detections = 'detections', + administration = 'administration', alerts = 'alerts', - rules = 'rules', + case = 'case', + detections = 'detections', + endpoints = 'endpoints', + eventFilters = 'event_filters', exceptions = 'exceptions', hosts = 'hosts', network = 'network', - timelines = 'timelines', - case = 'case', - administration = 'administration', - endpoints = 'endpoints', + overview = 'overview', policies = 'policies', + rules = 'rules', + timelines = 'timelines', trustedApps = 'trusted_apps', - eventFilters = 'event_filters', + ueba = 'ueba', } export const TIMELINES_PATH = '/timelines'; @@ -86,6 +87,7 @@ export const ALERTS_PATH = '/alerts'; export const RULES_PATH = '/rules'; export const EXCEPTIONS_PATH = '/exceptions'; export const HOSTS_PATH = '/hosts'; +export const UEBA_PATH = '/ueba'; export const NETWORK_PATH = '/network'; export const MANAGEMENT_PATH = '/administration'; export const ENDPOINTS_PATH = `${MANAGEMENT_PATH}/endpoints`; @@ -100,6 +102,7 @@ export const APP_RULES_PATH = `${APP_PATH}${RULES_PATH}`; export const APP_EXCEPTIONS_PATH = `${APP_PATH}${EXCEPTIONS_PATH}`; export const APP_HOSTS_PATH = `${APP_PATH}${HOSTS_PATH}`; +export const APP_UEBA_PATH = `${APP_PATH}${UEBA_PATH}`; export const APP_NETWORK_PATH = `${APP_PATH}${NETWORK_PATH}`; export const APP_TIMELINES_PATH = `${APP_PATH}${TIMELINES_PATH}`; export const APP_CASES_PATH = `${APP_PATH}${CASES_PATH}`; @@ -119,6 +122,11 @@ export const DEFAULT_INDEX_PATTERN = [ 'winlogbeat-*', ]; +export const DEFAULT_INDEX_PATTERN_EXPERIMENTAL = [ + // TODO: Steph/ueba TEMP for testing UEBA data + 'ml_host_risk_score_*', +]; + /** This Kibana Advanced Setting enables the `Security news` feed widget */ export const ENABLE_NEWS_FEED_SETTING = 'securitySolution:enableNewsFeed'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts index 1e7bcb0002dad..69f21e605627b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts @@ -11,12 +11,8 @@ import type { CreateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; import { buildExceptionFilter } from '@kbn/securitysolution-list-utils'; -import { - Filter, - IIndexPattern, - buildEsQuery, - EsQueryConfig, -} from '../../../../../src/plugins/data/common'; +import { Filter, EsQueryConfig, IndexPatternBase, buildEsQuery } from '@kbn/es-query'; + import { ESBoolQuery } from '../typed_json'; import { Query, Index, TimestampOverrideOrUndefined } from './schemas/common/schemas'; @@ -28,7 +24,7 @@ export const getQueryFilter = ( lists: Array, excludeExceptions: boolean = true ): ESBoolQuery => { - const indexPattern: IIndexPattern = { + const indexPattern: IndexPatternBase = { fields: [], title: index.join(), }; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index a9a81aa285af7..6d4a2b78840ea 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -11,11 +11,12 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; * A list of allowed values that can be used in `xpack.securitySolution.enableExperimental`. * This object is then used to validate and parse the value entered. */ -const allowedExperimentalValues = Object.freeze({ - trustedAppsByPolicyEnabled: false, +export const allowedExperimentalValues = Object.freeze({ metricsEntitiesEnabled: false, ruleRegistryEnabled: false, tGridEnabled: false, + trustedAppsByPolicyEnabled: false, + uebaEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts b/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts index 85d339970dc59..f7f3df31db237 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts @@ -5,12 +5,9 @@ * 2.0. */ -import { IIndexPattern } from 'src/plugins/data/public'; -import { - IEsSearchRequest, - IEsSearchResponse, - IFieldSubType, -} from '../../../../../../src/plugins/data/common'; +import { IFieldSubType } from '@kbn/es-query'; +import type { IIndexPattern } from 'src/plugins/data/public'; +import { IEsSearchRequest, IEsSearchResponse } from '../../../../../../src/plugins/data/common'; import { DocValueFields, Maybe } from '../common'; interface FieldInfo { diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 06d4a16699b8f..208579ffacabe 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -71,14 +71,27 @@ import { CtiEventEnrichmentStrategyResponse, CtiQueries, } from './cti'; +import { + HostRulesRequestOptions, + HostRulesStrategyResponse, + HostTacticsRequestOptions, + HostTacticsStrategyResponse, + RiskScoreRequestOptions, + RiskScoreStrategyResponse, + UebaQueries, + UserRulesRequestOptions, + UserRulesStrategyResponse, +} from './ueba'; export * from './hosts'; export * from './matrix_histogram'; export * from './network'; +export * from './ueba'; export type FactoryQueryTypes = | HostsQueries | HostsKpiQueries + | UebaQueries | NetworkQueries | NetworkKpiQueries | CtiQueries @@ -109,6 +122,14 @@ export type StrategyResponseType = T extends HostsQ ? HostsStrategyResponse : T extends HostsQueries.details ? HostDetailsStrategyResponse + : T extends UebaQueries.riskScore + ? RiskScoreStrategyResponse + : T extends UebaQueries.hostRules + ? HostRulesStrategyResponse + : T extends UebaQueries.userRules + ? UserRulesStrategyResponse + : T extends UebaQueries.hostTactics + ? HostTacticsStrategyResponse : T extends HostsQueries.overview ? HostsOverviewStrategyResponse : T extends HostsQueries.authentications @@ -199,6 +220,14 @@ export type StrategyRequestType = T extends HostsQu ? NetworkKpiUniqueFlowsRequestOptions : T extends NetworkKpiQueries.uniquePrivateIps ? NetworkKpiUniquePrivateIpsRequestOptions + : T extends UebaQueries.riskScore + ? RiskScoreRequestOptions + : T extends UebaQueries.hostRules + ? HostRulesRequestOptions + : T extends UebaQueries.userRules + ? UserRulesRequestOptions + : T extends UebaQueries.hostTactics + ? HostTacticsRequestOptions : T extends typeof MatrixHistogramQuery ? MatrixHistogramRequestOptions : T extends CtiQueries.eventEnrichment diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/common/index.ts new file mode 100644 index 0000000000000..f7406e32d1869 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/common/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Maybe } from '../../../common'; + +export enum RiskScoreFields { + hostName = 'host_name', + riskKeyword = 'risk_keyword', + riskScore = 'risk_score', +} +export interface RiskScoreItem { + _id?: Maybe; + [RiskScoreFields.hostName]: Maybe; + [RiskScoreFields.riskKeyword]: Maybe; + [RiskScoreFields.riskScore]: Maybe; +} +export enum HostRulesFields { + hits = 'hits', + riskScore = 'risk_score', + ruleName = 'rule_name', + ruleType = 'rule_type', +} +export interface HostRulesItem { + _id?: Maybe; + [HostRulesFields.hits]: Maybe; + [HostRulesFields.riskScore]: Maybe; + [HostRulesFields.ruleName]: Maybe; + [HostRulesFields.ruleType]: Maybe; +} +export enum UserRulesFields { + userName = 'user_name', + riskScore = 'risk_score', + rules = 'rules', + ruleCount = 'rule_count', +} +export enum HostTacticsFields { + hits = 'hits', + riskScore = 'risk_score', + tactic = 'tactic', + technique = 'technique', +} +export interface HostTacticsItem { + _id?: Maybe; + [HostTacticsFields.hits]: Maybe; + [HostTacticsFields.riskScore]: Maybe; + [HostTacticsFields.tactic]: Maybe; + [HostTacticsFields.technique]: Maybe; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_rules/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_rules/index.ts new file mode 100644 index 0000000000000..cb6469c6209a6 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_rules/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { HostRulesItem, HostRulesFields } from '../common'; +import { CursorType, Hit, Inspect, Maybe, PageInfoPaginated, SortField } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; + +export interface HostRulesHit extends Hit { + key: string; + doc_count: number; + risk_score: { + value?: number; + }; + rule_type: { + buckets?: Array<{ + key: string; + doc_count: number; + }>; + }; + rule_count: { + value: number; + }; +} + +export interface HostRulesEdges { + node: HostRulesItem; + cursor: CursorType; +} + +export interface HostRulesStrategyResponse extends IEsSearchResponse { + edges: HostRulesEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface HostRulesRequestOptions extends RequestOptionsPaginated { + defaultIndex: string[]; + hostName: string; +} + +export type HostRulesSortField = SortField; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_tactics/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_tactics/index.ts new file mode 100644 index 0000000000000..c55058dc6be04 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_tactics/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { HostTacticsItem, HostTacticsFields } from '../common'; +import { CursorType, Hit, Inspect, Maybe, PageInfoPaginated, SortField } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; +export interface HostTechniqueHit { + key: string; + doc_count: number; + risk_score: { + value?: number; + }; +} +export interface HostTacticsHit extends Hit { + key: string; + doc_count: number; + risk_score: { + value?: number; + }; + technique: { + buckets?: HostTechniqueHit[]; + }; + tactic_count: { + value: number; + }; +} + +export interface HostTacticsEdges { + node: HostTacticsItem; + cursor: CursorType; +} + +export interface HostTacticsStrategyResponse extends IEsSearchResponse { + edges: HostTacticsEdges[]; + techniqueCount: number; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface HostTacticsRequestOptions extends RequestOptionsPaginated { + defaultIndex: string[]; + hostName: string; +} + +export type HostTacticsSortField = SortField; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/index.ts new file mode 100644 index 0000000000000..1d166e36f6973 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './common'; +export * from './host_rules'; +export * from './host_tactics'; +export * from './risk_score'; +export * from './user_rules'; + +export enum UebaQueries { + hostRules = 'hostRules', + hostTactics = 'hostTactics', + riskScore = 'riskScore', + userRules = 'userRules', +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/risk_score/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/risk_score/index.ts new file mode 100644 index 0000000000000..14c1533755056 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/risk_score/index.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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { RiskScoreItem, RiskScoreFields } from '../common'; +import { CursorType, Hit, Inspect, Maybe, PageInfoPaginated, SortField } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; + +export interface RiskScoreHit extends Hit { + _source: { + '@timestamp': string; + }; + key: string; + doc_count: number; + risk_score: { + value?: number; + }; + risk_keyword: { + buckets?: Array<{ + key: string; + doc_count: number; + }>; + }; +} + +export interface RiskScoreEdges { + node: RiskScoreItem; + cursor: CursorType; +} + +export interface RiskScoreStrategyResponse extends IEsSearchResponse { + edges: RiskScoreEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface RiskScoreRequestOptions extends RequestOptionsPaginated { + defaultIndex: string[]; +} + +export type RiskScoreSortField = SortField; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/user_rules/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/user_rules/index.ts new file mode 100644 index 0000000000000..c7302c10fab3b --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/user_rules/index.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { HostRulesFields, UserRulesFields } from '../common'; +import { Hit, Inspect, Maybe, PageInfoPaginated, SearchHit, SortField } from '../../../common'; +import { HostRulesEdges, RequestOptionsPaginated } from '../..'; + +export interface RuleNameHit extends Hit { + key: string; + doc_count: number; + risk_score: { + value: number; + }; + rule_type: { + buckets?: Array<{ + key: string; + doc_count: number; + }>; + }; +} +export interface UserRulesHit extends Hit { + _source: { + '@timestamp': string; + }; + key: string; + doc_count: number; + risk_score: { + value: number; + }; + rule_count: { + value: number; + }; + rule_name: { + buckets?: RuleNameHit[]; + }; +} + +export interface UserRulesByUser { + _id?: Maybe; + [UserRulesFields.userName]: string; + [UserRulesFields.riskScore]: number; + [UserRulesFields.ruleCount]: number; + [UserRulesFields.rules]: HostRulesEdges[]; +} + +export interface UserRulesStrategyUserResponse { + [UserRulesFields.userName]: string; + [UserRulesFields.riskScore]: number; + edges: HostRulesEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; +} + +export interface UserRulesStrategyResponse extends IEsSearchResponse { + inspect?: Maybe; + data: UserRulesStrategyUserResponse[]; +} + +export interface UserRulesRequestOptions extends RequestOptionsPaginated { + defaultIndex: string[]; + hostName: string; +} + +export type UserRulesSortField = SortField; + +export interface UsersRulesHit extends SearchHit { + aggregations: { + user_data: { + buckets: UserRulesHit[]; + }; + }; +} diff --git a/x-pack/plugins/security_solution/common/typed_json.ts b/x-pack/plugins/security_solution/common/typed_json.ts index 8ce6907beb80b..1c42ab3a6fd24 100644 --- a/x-pack/plugins/security_solution/common/typed_json.ts +++ b/x-pack/plugins/security_solution/common/typed_json.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DslQuery, Filter } from 'src/plugins/data/common'; +import { DslQuery, Filter } from '@kbn/es-query'; import { JsonObject } from '@kbn/common-utils'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 05cf99195774b..e7c6464bc1546 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -308,6 +308,7 @@ export enum TimelineId { detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', + uebaPageExternalAlerts = 'ueba-page-external-alerts', active = 'timeline-1', casePage = 'timeline-case', test = 'test', // Reserved for testing purposes @@ -320,6 +321,7 @@ export const TimelineIdLiteralRt = runtimeTypes.union([ runtimeTypes.literal(TimelineId.detectionsRulesDetailsPage), runtimeTypes.literal(TimelineId.detectionsPage), runtimeTypes.literal(TimelineId.networkPageExternalAlerts), + runtimeTypes.literal(TimelineId.uebaPageExternalAlerts), runtimeTypes.literal(TimelineId.active), runtimeTypes.literal(TimelineId.test), ]); diff --git a/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts index 229bbcce87696..24de882bdfb90 100644 --- a/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts @@ -65,8 +65,8 @@ describe('Alert details with unmapped fields', () => { cy.get(TABLE_ROWS) .eq(expectedUnmmappedField.row) .within(() => { - cy.get(CELL_TEXT).eq(0).should('have.text', expectedUnmmappedField.field); - cy.get(CELL_TEXT).eq(1).should('have.text', expectedUnmmappedField.text); + cy.get(CELL_TEXT).eq(2).should('have.text', expectedUnmmappedField.field); + cy.get(CELL_TEXT).eq(4).should('have.text', expectedUnmmappedField.text); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 825cc7f8081e5..3b524bd252cdd 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -61,8 +61,8 @@ describe('Alert details with unmapped fields', () => { cy.get(TABLE_ROWS) .eq(expectedUnmmappedField.row) .within(() => { - cy.get(CELL_TEXT).eq(0).should('have.text', expectedUnmmappedField.field); - cy.get(CELL_TEXT).eq(1).should('have.text', expectedUnmmappedField.text); + cy.get(CELL_TEXT).eq(2).should('have.text', expectedUnmmappedField.field); + cy.get(CELL_TEXT).eq(4).should('have.text', expectedUnmmappedField.text); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts index 514c4b4e12bb3..095401ff31422 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts @@ -31,11 +31,9 @@ describe('CTI Link Panel', () => { cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 indicators'); cy.get(`${OVERVIEW_CTI_ENABLE_MODULE_BUTTON}`).should('exist'); - cy.get(`${OVERVIEW_CTI_ENABLE_MODULE_BUTTON}`).should( - 'have.attr', - 'href', - 'https://www.elastic.co/guide/en/beats/filebeat/master/filebeat-module-threatintel.html' - ); + cy.get(`${OVERVIEW_CTI_ENABLE_MODULE_BUTTON}`) + .should('have.attr', 'href') + .and('match', /filebeat-module-threatintel.html/); }); describe('enabled threat intel module', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/toggle_column.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/toggle_column.spec.ts index 786d153fd94d6..b44856a469cfb 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/toggle_column.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/toggle_column.spec.ts @@ -5,24 +5,18 @@ * 2.0. */ -import { - ID_HEADER_FIELD, - ID_TOGGLE_FIELD, - TIMESTAMP_HEADER_FIELD, - TIMESTAMP_TOGGLE_FIELD, -} from '../../screens/timeline'; +import { ID_HEADER_FIELD, TIMESTAMP_HEADER_FIELD } from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; import { loginAndWaitForPage } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { - checkIdToggleField, + clickIdToggleField, closeTimeline, createNewTimeline, - dragAndDropIdToggleFieldToTimeline, expandFirstTimelineEventDetails, populateTimeline, - uncheckTimestampToggleField, + clickTimestampToggleField, } from '../../tasks/timeline'; import { HOSTS_URL } from '../../urls/navigation'; @@ -44,33 +38,16 @@ describe('toggle column in timeline', () => { closeTimeline(); }); - it('displays a checked Toggle field checkbox for `@timestamp`, a default timeline column', () => { - expandFirstTimelineEventDetails(); - cy.get(TIMESTAMP_TOGGLE_FIELD).should('be.checked'); - }); - - it('displays an Unchecked Toggle field checkbox for `_id`, because it is NOT a default timeline column', () => { - expandFirstTimelineEventDetails(); - cy.get(ID_TOGGLE_FIELD).should('not.be.checked'); - }); - it('removes the @timestamp field from the timeline when the user un-checks the toggle', () => { expandFirstTimelineEventDetails(); - uncheckTimestampToggleField(); + clickTimestampToggleField(); cy.get(TIMESTAMP_HEADER_FIELD).should('not.exist'); }); it('adds the _id field to the timeline when the user checks the field', () => { expandFirstTimelineEventDetails(); - checkIdToggleField(); - - cy.get(ID_HEADER_FIELD).should('exist'); - }); - - it('adds the _id field to the timeline via drag and drop', () => { - expandFirstTimelineEventDetails(); - dragAndDropIdToggleFieldToTimeline(); + clickIdToggleField(); cy.get(ID_HEADER_FIELD).should('exist'); }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index af7a7bb5d4c71..e6f2fb30bede8 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -189,10 +189,10 @@ export const attachTimelineToExistingCase = () => { cy.get(ATTACH_TIMELINE_TO_EXISTING_CASE_ICON).click({ force: true }); }; -export const checkIdToggleField = () => { +export const clickIdToggleField = () => { cy.get(ID_HEADER_FIELD).should('not.exist'); - cy.get(ID_TOGGLE_FIELD).check({ + cy.get(ID_TOGGLE_FIELD).click({ force: true, }); }; @@ -294,10 +294,10 @@ export const unpinFirstEvent = () => { cy.get(PIN_EVENT).first().click({ force: true }); }; -export const uncheckTimestampToggleField = () => { +export const clickTimestampToggleField = () => { cy.get(TIMESTAMP_TOGGLE_FIELD).should('exist'); - cy.get(TIMESTAMP_TOGGLE_FIELD).uncheck({ force: true }); + cy.get(TIMESTAMP_TOGGLE_FIELD).click({ force: true }); }; export const dragAndDropIdToggleFieldToTimeline = () => { diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts index f125218b68c09..59af6737e495f 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts @@ -7,13 +7,14 @@ import { getDeepLinks } from '.'; import { Capabilities } from '../../../../../../src/core/public'; import { SecurityPageName } from '../types'; +import { mockGlobalState } from '../../common/mock'; describe('public search functions', () => { it('returns a subset of links for basic license, full set for platinum', () => { const basicLicense = 'basic'; const platinumLicense = 'platinum'; - const basicLinks = getDeepLinks(basicLicense); - const platinumLinks = getDeepLinks(platinumLicense); + const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense); + const platinumLinks = getDeepLinks(mockGlobalState.app.enableExperimental, platinumLicense); basicLinks.forEach((basicLink, index) => { const platinumLink = platinumLinks[index]; @@ -26,7 +27,7 @@ describe('public search functions', () => { it('returns case links for basic license with only read_cases capabilities', () => { const basicLicense = 'basic'; - const basicLinks = getDeepLinks(basicLicense, ({ + const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ siem: { read_cases: true, crud_cases: false }, } as unknown) as Capabilities); @@ -35,7 +36,7 @@ describe('public search functions', () => { it('returns case links with NO deepLinks for basic license with only read_cases capabilities', () => { const basicLicense = 'basic'; - const basicLinks = getDeepLinks(basicLicense, ({ + const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ siem: { read_cases: true, crud_cases: false }, } as unknown) as Capabilities); @@ -46,7 +47,7 @@ describe('public search functions', () => { it('returns case links with deepLinks for basic license with crud_cases capabilities', () => { const basicLicense = 'basic'; - const basicLinks = getDeepLinks(basicLicense, ({ + const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ siem: { read_cases: true, crud_cases: true }, } as unknown) as Capabilities); @@ -57,7 +58,7 @@ describe('public search functions', () => { it('returns NO case links for basic license with NO read_cases capabilities', () => { const basicLicense = 'basic'; - const basicLinks = getDeepLinks(basicLicense, ({ + const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ siem: { read_cases: false, crud_cases: false }, } as unknown) as Capabilities); @@ -66,17 +67,38 @@ describe('public search functions', () => { it('returns case links for basic license with undefined capabilities', () => { const basicLicense = 'basic'; - const basicLinks = getDeepLinks(basicLicense, undefined); + const basicLinks = getDeepLinks( + mockGlobalState.app.enableExperimental, + basicLicense, + undefined + ); expect(basicLinks.some((l) => l.id === SecurityPageName.case)).toBeTruthy(); }); it('returns case deepLinks for basic license with undefined capabilities', () => { const basicLicense = 'basic'; - const basicLinks = getDeepLinks(basicLicense, undefined); + const basicLinks = getDeepLinks( + mockGlobalState.app.enableExperimental, + basicLicense, + undefined + ); expect( (basicLinks.find((l) => l.id === SecurityPageName.case)?.deepLinks?.length ?? 0) > 0 ).toBeTruthy(); }); + + it('returns NO ueba link when enableExperimental.uebaEnabled === false', () => { + const deepLinks = getDeepLinks(mockGlobalState.app.enableExperimental); + expect(deepLinks.some((l) => l.id === SecurityPageName.ueba)).toBeFalsy(); + }); + + it('returns ueba link when enableExperimental.uebaEnabled === true', () => { + const deepLinks = getDeepLinks({ + ...mockGlobalState.app.enableExperimental, + uebaEnabled: true, + }); + expect(deepLinks.some((l) => l.id === SecurityPageName.ueba)).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index f5cec592c7abf..871f1a01e3de0 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -27,6 +27,7 @@ import { TIMELINES, CASE, MANAGE, + UEBA, } from '../translations'; import { OVERVIEW_PATH, @@ -40,7 +41,9 @@ import { ENDPOINTS_PATH, TRUSTED_APPS_PATH, EVENT_FILTERS_PATH, + UEBA_PATH, } from '../../../common/constants'; +import { ExperimentalFeatures } from '../../../common/experimental_features'; export const topDeepLinks: AppDeepLink[] = [ { @@ -90,6 +93,18 @@ export const topDeepLinks: AppDeepLink[] = [ ], order: 9003, }, + { + id: SecurityPageName.ueba, + title: UEBA, + path: UEBA_PATH, + navLinkStatus: AppNavLinkStatus.visible, + keywords: [ + i18n.translate('xpack.securitySolution.search.ueba', { + defaultMessage: 'Users & Entities', + }), + ], + order: 9004, + }, { id: SecurityPageName.timelines, title: TIMELINES, @@ -100,7 +115,7 @@ export const topDeepLinks: AppDeepLink[] = [ defaultMessage: 'Timelines', }), ], - order: 9004, + order: 9005, }, { id: SecurityPageName.case, @@ -112,7 +127,7 @@ export const topDeepLinks: AppDeepLink[] = [ defaultMessage: 'Cases', }), ], - order: 9005, + order: 9006, }, { id: SecurityPageName.administration, @@ -254,6 +269,9 @@ const nestedDeepLinks: SecurityDeepLinks = { }, ], }, + [SecurityPageName.ueba]: { + base: [], + }, [SecurityPageName.timelines]: { base: [ { @@ -316,18 +334,22 @@ const nestedDeepLinks: SecurityDeepLinks = { /** * A function that generates the plugin deepLinks + * @param enableExperimental ExperimentalFeatures arg * @param licenseType optional string for license level, if not provided basic is assumed. + * @param capabilities optional arg for app start capabilities */ export function getDeepLinks( + enableExperimental: ExperimentalFeatures, licenseType?: LicenseType, capabilities?: ApplicationStart['capabilities'] ): AppDeepLink[] { return topDeepLinks .filter( (deepLink) => - deepLink.id !== SecurityPageName.case || - capabilities == null || - (deepLink.id === SecurityPageName.case && capabilities.siem.read_cases === true) + (deepLink.id !== SecurityPageName.case && deepLink.id !== SecurityPageName.ueba) || // is not cases or ueba + (deepLink.id === SecurityPageName.case && + (capabilities == null || capabilities.siem.read_cases === true)) || // is cases with at least read only caps + (deepLink.id === SecurityPageName.ueba && enableExperimental.uebaEnabled) // is ueba with ueba feature flag enabled ) .map((deepLink) => { const deepLinkId = deepLink.id as SecurityDeepLinkName; @@ -370,11 +392,13 @@ export function isPremiumLicense(licenseType?: LicenseType): boolean { export function updateGlobalNavigation({ capabilities, updater$, + enableExperimental, }: { capabilities: ApplicationStart['capabilities']; updater$: Subject; + enableExperimental: ExperimentalFeatures; }) { - const deepLinks = getDeepLinks(undefined, capabilities); + const deepLinks = getDeepLinks(enableExperimental, undefined, capabilities); const updatedDeepLinks = deepLinks.map((link) => { switch (link.id) { case SecurityPageName.case: diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts index d6f8516d43a72..686dafca76d99 100644 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts @@ -24,6 +24,7 @@ import { APP_ENDPOINTS_PATH, APP_TRUSTED_APPS_PATH, APP_EVENT_FILTERS_PATH, + APP_UEBA_PATH, SecurityPageName, } from '../../../common/constants'; @@ -70,6 +71,13 @@ export const navTabs: SecurityNav = { disabled: false, urlKey: 'network', }, + [SecurityPageName.ueba]: { + id: SecurityPageName.ueba, + name: i18n.UEBA, + href: APP_UEBA_PATH, + disabled: false, + urlKey: 'ueba', + }, [SecurityPageName.timelines]: { id: SecurityPageName.timelines, name: i18n.TIMELINES, diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 81437ec9ec6f6..e880da57cf374 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -10,7 +10,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { Redirect, Route, Switch } from 'react-router-dom'; import { OVERVIEW_PATH } from '../../common/constants'; -import { NotFoundPage } from '../app/404'; +import { NotFoundPage } from './404'; import { SecurityApp } from './app'; import { RenderAppProps } from './types'; @@ -43,6 +43,8 @@ export const renderApp = ({ ...subPlugins.exceptions.routes, ...subPlugins.hosts.routes, ...subPlugins.network.routes, + // will be undefined if enabledExperimental.uebaEnabled === false + ...(subPlugins.ueba != null ? subPlugins.ueba.routes : []), ...subPlugins.timelines.routes, ...subPlugins.cases.routes, ...subPlugins.management.routes, diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 027789713a2ae..c3cf11f35211e 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -19,6 +19,10 @@ export const NETWORK = i18n.translate('xpack.securitySolution.navigation.network defaultMessage: 'Network', }); +export const UEBA = i18n.translate('xpack.securitySolution.navigation.ueba', { + defaultMessage: 'Users & Entities', +}); + export const RULES = i18n.translate('xpack.securitySolution.navigation.rules', { defaultMessage: 'Rules', }); diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 8056c4092091c..490ff8936c18c 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -54,19 +54,21 @@ export interface SecuritySubPlugin { export type SecuritySubPluginKeyStore = | 'hosts' | 'network' + | 'ueba' | 'timeline' | 'hostList' | 'alertList' | 'management'; export type SecurityDeepLinkName = - | SecurityPageName.overview + | SecurityPageName.administration + | SecurityPageName.case | SecurityPageName.detections | SecurityPageName.hosts | SecurityPageName.network + | SecurityPageName.overview | SecurityPageName.timelines - | SecurityPageName.case - | SecurityPageName.administration; + | SecurityPageName.ueba; interface SecurityDeepLink { base: AppDeepLink[]; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx deleted file mode 100644 index a175a9b847c71..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useMemo, useCallback } from 'react'; -import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; - -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { getGenericComboBoxProps } from './helpers'; -import { GetGenericComboBoxPropsReturn } from './types'; - -interface OperatorProps { - placeholder: string; - selectedField: IFieldType | undefined; - indexPattern: IIndexPattern | undefined; - isLoading: boolean; - isDisabled: boolean; - isClearable: boolean; - fieldTypeFilter?: string[]; - fieldInputWidth?: number; - isRequired?: boolean; - onChange: (a: IFieldType[]) => void; -} - -/** - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * NOTE: This has deviated from the copy and will have to be reconciled. - */ -export const FieldComponent: React.FC = ({ - placeholder, - selectedField, - indexPattern, - isLoading = false, - isDisabled = false, - isClearable = false, - isRequired = false, - fieldTypeFilter = [], - fieldInputWidth, - onChange, -}): JSX.Element => { - const [touched, setIsTouched] = useState(false); - - const { availableFields, selectedFields } = useMemo( - () => getComboBoxFields(indexPattern, selectedField, fieldTypeFilter), - [indexPattern, selectedField, fieldTypeFilter] - ); - - const { comboOptions, labels, selectedComboOptions } = useMemo( - () => getComboBoxProps({ availableFields, selectedFields }), - [availableFields, selectedFields] - ); - - const handleValuesChange = useCallback( - (newOptions: EuiComboBoxOptionOption[]): void => { - const newValues: IFieldType[] = newOptions.map( - ({ label }) => availableFields[labels.indexOf(label)] - ); - onChange(newValues); - }, - [availableFields, labels, onChange] - ); - - const handleTouch = useCallback((): void => { - setIsTouched(true); - }, [setIsTouched]); - - return ( - - ); -}; - -FieldComponent.displayName = 'Field'; - -interface ComboBoxFields { - availableFields: IFieldType[]; - selectedFields: IFieldType[]; -} - -const getComboBoxFields = ( - indexPattern: IIndexPattern | undefined, - selectedField: IFieldType | undefined, - fieldTypeFilter: string[] -): ComboBoxFields => { - const existingFields = getExistingFields(indexPattern); - const selectedFields = getSelectedFields(selectedField); - const availableFields = getAvailableFields(existingFields, selectedFields, fieldTypeFilter); - - return { availableFields, selectedFields }; -}; - -const getComboBoxProps = (fields: ComboBoxFields): GetGenericComboBoxPropsReturn => { - const { availableFields, selectedFields } = fields; - - return getGenericComboBoxProps({ - options: availableFields, - selectedOptions: selectedFields, - getLabel: (field) => field.name, - }); -}; - -const getExistingFields = (indexPattern: IIndexPattern | undefined): IFieldType[] => { - return indexPattern != null ? indexPattern.fields : []; -}; - -const getSelectedFields = (selectedField: IFieldType | undefined): IFieldType[] => { - return selectedField ? [selectedField] : []; -}; - -const getAvailableFields = ( - existingFields: IFieldType[], - selectedFields: IFieldType[], - fieldTypeFilter: string[] -): IFieldType[] => { - const fieldsByName = new Map(); - - existingFields.forEach((f) => fieldsByName.set(f.name, f)); - selectedFields.forEach((f) => fieldsByName.set(f.name, f)); - - const uniqueFields = Array.from(fieldsByName.values()); - - if (fieldTypeFilter.length > 0) { - return uniqueFields.filter(({ type }) => fieldTypeFilter.includes(type)); - } - - return uniqueFields; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx deleted file mode 100644 index 38d103fe65130..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx +++ /dev/null @@ -1,425 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount, ReactWrapper } from 'enzyme'; -import { EuiSuperSelect, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { act } from '@testing-library/react'; - -import { - fields, - getField, -} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { AutocompleteFieldMatchComponent } from './field_value_match'; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; - -jest.mock('./hooks/use_field_value_autocomplete'); - -describe('AutocompleteFieldMatchComponent', () => { - let wrapper: ReactWrapper; - - const getValueSuggestionsMock = jest - .fn() - .mockResolvedValue([false, true, ['value 3', 'value 4'], jest.fn()]); - - beforeEach(() => { - (useFieldValueAutocomplete as jest.Mock).mockReturnValue([ - false, - true, - ['value 1', 'value 2'], - getValueSuggestionsMock, - ]); - }); - - afterEach(() => { - jest.clearAllMocks(); - wrapper.unmount(); - }); - - test('it renders row label if one passed in', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatchLabel"] label').at(0).text() - ).toEqual('Row Label'); - }); - - test('it renders disabled if "isDisabled" is true', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatch"] input').prop('disabled') - ).toBeTruthy(); - }); - - test('it renders loading if "isLoading" is true', () => { - wrapper = mount( - - ); - wrapper.find('[data-test-subj="valuesAutocompleteMatch"] button').at(0).simulate('click'); - expect( - wrapper - .find('EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteMatch-optionsList"]') - .prop('isLoading') - ).toBeTruthy(); - }); - - test('it allows user to clear values if "isClearable" is true', () => { - wrapper = mount( - - ); - - expect( - wrapper - .find('[data-test-subj="comboBoxInput"]') - .hasClass('euiComboBox__inputWrap-isClearable') - ).toBeTruthy(); - }); - - test('it correctly displays selected value', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatch"] EuiComboBoxPill').at(0).text() - ).toEqual('126.45.211.34'); - }); - - test('it invokes "onChange" when new value created', async () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - ((wrapper.find(EuiComboBox).props() as unknown) as { - onCreateOption: (a: string) => void; - }).onCreateOption('126.45.211.34'); - - expect(mockOnChange).toHaveBeenCalledWith('126.45.211.34'); - }); - - test('it invokes "onChange" when new value selected', async () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - ((wrapper.find(EuiComboBox).props() as unknown) as { - onChange: (a: EuiComboBoxOptionOption[]) => void; - }).onChange([{ label: 'value 1' }]); - - expect(mockOnChange).toHaveBeenCalledWith('value 1'); - }); - - test('it refreshes autocomplete with search query when new value searched', () => { - wrapper = mount( - - ); - act(() => { - ((wrapper.find(EuiComboBox).props() as unknown) as { - onSearchChange: (a: string) => void; - }).onSearchChange('value 1'); - }); - - expect(useFieldValueAutocomplete).toHaveBeenCalledWith({ - selectedField: getField('machine.os.raw'), - operatorType: 'match', - query: 'value 1', - fieldValue: '', - indexPattern: { - id: '1234', - title: 'logstash-*', - fields, - }, - }); - }); - - describe('boolean type', () => { - const valueSuggestionsMock = jest.fn().mockResolvedValue([false, false, [], jest.fn()]); - - beforeEach(() => { - (useFieldValueAutocomplete as jest.Mock).mockReturnValue([ - false, - false, - [], - valueSuggestionsMock, - ]); - }); - - test('it displays only two options - "true" or "false"', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatchBoolean"]').exists() - ).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="valuesAutocompleteMatchBoolean"]').at(0).prop('options') - ).toEqual([ - { - inputDisplay: 'true', - value: 'true', - }, - { - inputDisplay: 'false', - value: 'false', - }, - ]); - }); - - test('it invokes "onChange" with "true" when selected', () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - ((wrapper.find(EuiSuperSelect).props() as unknown) as { - onChange: (a: string) => void; - }).onChange('true'); - - expect(mockOnChange).toHaveBeenCalledWith('true'); - }); - - test('it invokes "onChange" with "false" when selected', () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - ((wrapper.find(EuiSuperSelect).props() as unknown) as { - onChange: (a: string) => void; - }).onChange('false'); - - expect(mockOnChange).toHaveBeenCalledWith('false'); - }); - }); - - describe('number type', () => { - const valueSuggestionsMock = jest.fn().mockResolvedValue([false, false, [], jest.fn()]); - - beforeEach(() => { - (useFieldValueAutocomplete as jest.Mock).mockReturnValue([ - false, - false, - [], - valueSuggestionsMock, - ]); - }); - - test('it number input when field type is number', () => { - wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="valueAutocompleteFieldMatchNumber"]').exists() - ).toBeTruthy(); - }); - - test('it invokes "onChange" with numeric value when inputted', () => { - const mockOnChange = jest.fn(); - wrapper = mount( - - ); - - wrapper - .find('[data-test-subj="valueAutocompleteFieldMatchNumber"] input') - .at(0) - .simulate('change', { target: { value: '8' } }); - - expect(mockOnChange).toHaveBeenCalledWith('8'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx deleted file mode 100644 index 21d1d9b4b31aa..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ /dev/null @@ -1,285 +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, { useCallback, useMemo, useState, useEffect } from 'react'; -import { - EuiSuperSelect, - EuiFormRow, - EuiFieldNumber, - EuiComboBoxOptionOption, - EuiComboBox, -} from '@elastic/eui'; -import { uniq } from 'lodash'; - -import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; -import { paramIsValid, getGenericComboBoxProps } from './helpers'; - -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; - -interface AutocompleteFieldMatchProps { - placeholder: string; - selectedField: IFieldType | undefined; - selectedValue: string | undefined; - indexPattern: IIndexPattern | undefined; - isLoading: boolean; - isDisabled: boolean; - isClearable: boolean; - isRequired?: boolean; - fieldInputWidth?: number; - rowLabel?: string; - onChange: (arg: string) => void; - onError?: (arg: boolean) => void; -} - -/** - * There is a copy of this within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - */ -export const AutocompleteFieldMatchComponent: React.FC = ({ - placeholder, - rowLabel, - selectedField, - selectedValue, - indexPattern, - isLoading, - isDisabled = false, - isClearable = false, - isRequired = false, - fieldInputWidth, - onChange, - onError, -}): JSX.Element => { - const [searchQuery, setSearchQuery] = useState(''); - const [touched, setIsTouched] = useState(false); - const [error, setError] = useState(undefined); - const [isLoadingSuggestions, isSuggestingValues, suggestions] = useFieldValueAutocomplete({ - selectedField, - operatorType: OperatorTypeEnum.MATCH, - fieldValue: selectedValue, - query: searchQuery, - indexPattern, - }); - const getLabel = useCallback((option: string): string => option, []); - const optionsMemo = useMemo((): string[] => { - const valueAsStr = String(selectedValue); - return selectedValue != null && selectedValue.trim() !== '' - ? uniq([valueAsStr, ...suggestions]) - : suggestions; - }, [suggestions, selectedValue]); - const selectedOptionsMemo = useMemo((): string[] => { - const valueAsStr = String(selectedValue); - return selectedValue ? [valueAsStr] : []; - }, [selectedValue]); - - const handleError = useCallback( - (err: string | undefined): void => { - setError((existingErr): string | undefined => { - const oldErr = existingErr != null; - const newErr = err != null; - if (oldErr !== newErr && onError != null) { - onError(newErr); - } - - return err; - }); - }, - [setError, onError] - ); - - const { comboOptions, labels, selectedComboOptions } = useMemo( - (): GetGenericComboBoxPropsReturn => - getGenericComboBoxProps({ - options: optionsMemo, - selectedOptions: selectedOptionsMemo, - getLabel, - }), - [optionsMemo, selectedOptionsMemo, getLabel] - ); - - const handleValuesChange = useCallback( - (newOptions: EuiComboBoxOptionOption[]): void => { - const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]); - handleError(undefined); - onChange(newValue ?? ''); - }, - [handleError, labels, onChange, optionsMemo] - ); - - const handleSearchChange = useCallback( - (searchVal: string): void => { - if (searchVal !== '' && selectedField != null) { - const err = paramIsValid(searchVal, selectedField, isRequired, touched); - handleError(err); - - setSearchQuery(searchVal); - } - }, - [handleError, isRequired, selectedField, touched] - ); - - const handleCreateOption = useCallback( - (option: string): boolean | undefined => { - const err = paramIsValid(option, selectedField, isRequired, touched); - handleError(err); - - if (err != null) { - // Explicitly reject the user's input - return false; - } else { - onChange(option); - } - }, - [isRequired, onChange, selectedField, touched, handleError] - ); - - const handleNonComboBoxInputChange = (event: React.ChangeEvent): void => { - const newValue = event.target.value; - onChange(newValue); - }; - - const handleBooleanInputChange = (newOption: string): void => { - onChange(newOption); - }; - - const setIsTouchedValue = useCallback((): void => { - setIsTouched(true); - - const err = paramIsValid(selectedValue, selectedField, isRequired, true); - handleError(err); - }, [setIsTouched, handleError, selectedValue, selectedField, isRequired]); - - const inputPlaceholder = useMemo((): string => { - if (isLoading || isLoadingSuggestions) { - return i18n.LOADING; - } else if (selectedField == null) { - return i18n.SELECT_FIELD_FIRST; - } else { - return placeholder; - } - }, [isLoading, selectedField, isLoadingSuggestions, placeholder]); - - const isLoadingState = useMemo((): boolean => isLoading || isLoadingSuggestions, [ - isLoading, - isLoadingSuggestions, - ]); - - useEffect((): void => { - setError(undefined); - if (onError != null) { - onError(false); - } - }, [selectedField, onError]); - - const defaultInput = useMemo((): JSX.Element => { - return ( - - - - ); - }, [ - comboOptions, - error, - fieldInputWidth, - handleCreateOption, - handleSearchChange, - handleValuesChange, - inputPlaceholder, - isClearable, - isDisabled, - isLoadingState, - rowLabel, - selectedComboOptions, - selectedField, - setIsTouchedValue, - ]); - - if (!isSuggestingValues && selectedField != null) { - switch (selectedField.type) { - case 'number': - return ( - - 0 - ? parseFloat(selectedValue) - : selectedValue ?? '' - } - onChange={handleNonComboBoxInputChange} - data-test-subj="valueAutocompleteFieldMatchNumber" - style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}} - fullWidth - /> - - ); - case 'boolean': - return ( - - - - ); - default: - return defaultInput; - } - } else { - return defaultInput; - } -}; - -AutocompleteFieldMatchComponent.displayName = 'AutocompleteFieldMatch'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts deleted file mode 100644 index 1618de245365d..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import '../../../common/mock/match_media'; -import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; - -import * as i18n from './translations'; -import { checkEmptyValue, paramIsValid, getGenericComboBoxProps } from './helpers'; - -describe('helpers', () => { - // @ts-ignore - moment.suppressDeprecationWarnings = true; - - describe('#checkEmptyValue', () => { - test('returns no errors if no field has been selected', () => { - const isValid = checkEmptyValue('', undefined, true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns error string if user has touched a required input and left empty', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, true); - - expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); - }); - - test('returns no errors if required input is empty but user has not yet touched it', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty', () => { - const isValid = checkEmptyValue(undefined, getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty string', () => { - const isValid = checkEmptyValue('', getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns null if input value is not empty string or undefined', () => { - const isValid = checkEmptyValue('hellooo', getField('@timestamp'), false, true); - - expect(isValid).toBeNull(); - }); - }); - - describe('#paramIsValid', () => { - test('returns no errors if no field has been selected', () => { - const isValid = paramIsValid('', undefined, true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns error string if user has touched a required input and left empty', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), true, true); - - expect(isValid).toEqual(i18n.FIELD_REQUIRED_ERR); - }); - - test('returns no errors if required input is empty but user has not yet touched it', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), true, false); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty', () => { - const isValid = paramIsValid(undefined, getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if user has touched an input that is not required and left empty string', () => { - const isValid = paramIsValid('', getField('@timestamp'), false, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type date and value is valid', () => { - const isValid = paramIsValid( - '1994-11-05T08:15:30-05:00', - getField('@timestamp'), - false, - true - ); - - expect(isValid).toBeUndefined(); - }); - - test('returns errors if filed is of type date and value is not valid', () => { - const isValid = paramIsValid('1593478826', getField('@timestamp'), false, true); - - expect(isValid).toEqual(i18n.DATE_ERR); - }); - - test('returns no errors if field is of type number and value is an integer', () => { - const isValid = paramIsValid('4', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type number and value is a float', () => { - const isValid = paramIsValid('4.3', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns no errors if field is of type number and value is a long', () => { - const isValid = paramIsValid('-9223372036854775808', getField('bytes'), true, true); - - expect(isValid).toBeUndefined(); - }); - - test('returns errors if field is of type number and value is "hello"', () => { - const isValid = paramIsValid('hello', getField('bytes'), true, true); - - expect(isValid).toEqual(i18n.NUMBER_ERR); - }); - - test('returns errors if field is of type number and value is "123abc"', () => { - const isValid = paramIsValid('123abc', getField('bytes'), true, true); - - expect(isValid).toEqual(i18n.NUMBER_ERR); - }); - }); - - describe('#getGenericComboBoxProps', () => { - test('it returns empty arrays if "options" is empty array', () => { - const result = getGenericComboBoxProps({ - options: [], - selectedOptions: ['option1'], - getLabel: (t: string) => t, - }); - - expect(result).toEqual({ comboOptions: [], labels: [], selectedComboOptions: [] }); - }); - - test('it returns formatted props if "options" array is not empty', () => { - const result = getGenericComboBoxProps({ - options: ['option1', 'option2', 'option3'], - selectedOptions: [], - getLabel: (t: string) => t, - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [], - }); - }); - - test('it does not return "selectedOptions" items that do not appear in "options"', () => { - const result = getGenericComboBoxProps({ - options: ['option1', 'option2', 'option3'], - selectedOptions: ['option4'], - getLabel: (t: string) => t, - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [], - }); - }); - - test('it return "selectedOptions" items that do appear in "options"', () => { - const result = getGenericComboBoxProps({ - options: ['option1', 'option2', 'option3'], - selectedOptions: ['option2'], - getLabel: (t: string) => t, - }); - - expect(result).toEqual({ - comboOptions: [ - { - label: 'option1', - }, - { - label: 'option2', - }, - { - label: 'option3', - }, - ], - labels: ['option1', 'option2', 'option3'], - selectedComboOptions: [ - { - label: 'option2', - }, - ], - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts deleted file mode 100644 index 890f1e6755834..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import dateMath from '@elastic/datemath'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; - -import { IFieldType } from '../../../../../../../src/plugins/data/common'; - -import { GetGenericComboBoxPropsReturn } from './types'; -import * as i18n from './translations'; - -/** - * Determines if empty value is ok - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - */ -export const checkEmptyValue = ( - param: string | undefined, - field: IFieldType | undefined, - isRequired: boolean, - touched: boolean -): string | undefined | null => { - if (isRequired && touched && (param == null || param.trim() === '')) { - return i18n.FIELD_REQUIRED_ERR; - } - - if ( - field == null || - (isRequired && !touched) || - (!isRequired && (param == null || param === '')) - ) { - return undefined; - } - - return null; -}; - -/** - * Very basic validation for values - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * @param param the value being checked - * @param field the selected field - * @param isRequired whether or not an empty value is allowed - * @param touched has field been touched by user - * @returns undefined if valid, string with error message if invalid - */ -export const paramIsValid = ( - param: string | undefined, - field: IFieldType | undefined, - isRequired: boolean, - touched: boolean -): string | undefined => { - if (field == null) { - return undefined; - } - - const emptyValueError = checkEmptyValue(param, field, isRequired, touched); - if (emptyValueError !== null) { - return emptyValueError; - } - - switch (field.type) { - case 'date': - const moment = dateMath.parse(param ?? ''); - const isDate = Boolean(moment && moment.isValid()); - return isDate ? undefined : i18n.DATE_ERR; - case 'number': - const isNum = param != null && param.trim() !== '' && !isNaN(+param); - return isNum ? undefined : i18n.NUMBER_ERR; - default: - return undefined; - } -}; - -/** - * Determines the options, selected values and option labels for EUI combo box - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - * @param options options user can select from - * @param selectedOptions user selection if any - * @param getLabel helper function to know which property to use for labels - */ -export function getGenericComboBoxProps({ - options, - selectedOptions, - getLabel, -}: { - options: T[]; - selectedOptions: T[]; - getLabel: (value: T) => string; -}): GetGenericComboBoxPropsReturn { - const newLabels = options.map(getLabel); - const newComboOptions: EuiComboBoxOptionOption[] = newLabels.map((label) => ({ label })); - const newSelectedComboOptions = selectedOptions - .map(getLabel) - .filter((option) => { - return newLabels.indexOf(option) !== -1; - }) - .map((option) => { - return newComboOptions[newLabels.indexOf(option)]; - }); - - return { - comboOptions: newComboOptions, - labels: newLabels, - selectedComboOptions: newSelectedComboOptions, - }; -} diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts deleted file mode 100644 index e0bdbf2603dc3..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ /dev/null @@ -1,325 +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 { act, renderHook } from '@testing-library/react-hooks'; - -import { - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn, - useFieldValueAutocomplete, -} from './use_field_value_autocomplete'; -import { useKibana } from '../../../../common/lib/kibana'; -import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub'; -import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; - -jest.mock('../../../../common/lib/kibana'); - -describe('useFieldValueAutocomplete', () => { - const onErrorMock = jest.fn(); - const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); - - beforeEach(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - data: { - autocomplete: { - getValueSuggestions: getValueSuggestionsMock, - }, - }, - }, - }); - }); - - afterEach(() => { - onErrorMock.mockClear(); - getValueSuggestionsMock.mockClear(); - }); - - test('initializes hook', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: undefined, - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: undefined, - query: '', - }) - ); - await waitForNextUpdate(); - - expect(result.current).toEqual([false, true, [], result.current[3]]); - }); - }); - - test('does not call autocomplete service if "operatorType" is "exists"', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('machine.os'), - operatorType: OperatorTypeEnum.EXISTS, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, true, [], result.current[3]]; - - expect(getValueSuggestionsMock).not.toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('does not call autocomplete service if "selectedField" is undefined', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: undefined, - operatorType: OperatorTypeEnum.EXISTS, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, true, [], result.current[3]]; - - expect(getValueSuggestionsMock).not.toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('does not call autocomplete service if "indexPattern" is undefined', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('machine.os'), - operatorType: OperatorTypeEnum.EXISTS, - fieldValue: '', - indexPattern: undefined, - query: '', - }) - ); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, true, [], result.current[3]]; - - expect(getValueSuggestionsMock).not.toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('it uses full path name for nested fields to fetch suggestions', async () => { - const suggestionsMock = jest.fn().mockResolvedValue([]); - - (useKibana as jest.Mock).mockReturnValue({ - services: { - data: { - autocomplete: { - getValueSuggestions: suggestionsMock, - }, - }, - }, - }); - await act(async () => { - const signal = new AbortController().signal; - const { waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: { ...getField('nestedField.child'), name: 'child' }, - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(suggestionsMock).toHaveBeenCalledWith({ - field: { ...getField('nestedField.child'), name: 'nestedField.child' }, - indexPattern: { - fields: [ - { - aggregatable: true, - esTypes: ['integer'], - filterable: true, - name: 'response', - searchable: true, - type: 'number', - }, - ], - id: '1234', - title: 'logstash-*', - }, - query: '', - signal, - }); - }); - }); - - test('returns "isSuggestingValues" of false if field type is boolean', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('ssl'), - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, false, [], result.current[3]]; - - expect(getValueSuggestionsMock).not.toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('returns "isSuggestingValues" of false to note that autocomplete service is not in use if no autocomplete suggestions available', async () => { - const suggestionsMock = jest.fn().mockResolvedValue([]); - - (useKibana as jest.Mock).mockReturnValue({ - services: { - data: { - autocomplete: { - getValueSuggestions: suggestionsMock, - }, - }, - }, - }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('bytes'), - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [false, false, [], result.current[3]]; - - expect(suggestionsMock).toHaveBeenCalled(); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('returns suggestions', async () => { - await act(async () => { - const signal = new AbortController().signal; - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('@tags'), - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [ - false, - true, - ['value 1', 'value 2'], - result.current[3], - ]; - - expect(getValueSuggestionsMock).toHaveBeenCalledWith({ - field: getField('@tags'), - indexPattern: stubIndexPatternWithFields, - query: '', - signal, - }); - expect(result.current).toEqual(expectedResult); - }); - }); - - test('returns new suggestions on subsequent calls', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseFieldValueAutocompleteProps, - UseFieldValueAutocompleteReturn - >(() => - useFieldValueAutocomplete({ - selectedField: getField('@tags'), - operatorType: OperatorTypeEnum.MATCH, - fieldValue: '', - indexPattern: stubIndexPatternWithFields, - query: '', - }) - ); - // Note: initial `waitForNextUpdate` is hook initialization - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(result.current[3]).not.toBeNull(); - - // Added check for typescripts sake, if null, - // would not reach below logic as test would stop above - if (result.current[3] != null) { - result.current[3]({ - fieldSelected: getField('@tags'), - value: 'hello', - patterns: stubIndexPatternWithFields, - searchQuery: '', - }); - } - - await waitForNextUpdate(); - - const expectedResult: UseFieldValueAutocompleteReturn = [ - false, - true, - ['value 1', 'value 2'], - result.current[3], - ]; - - expect(getValueSuggestionsMock).toHaveBeenCalledTimes(2); - expect(result.current).toEqual(expectedResult); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts deleted file mode 100644 index 0fc4a663b7e11..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.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. - */ - -import { useEffect, useState, useRef } from 'react'; -import { debounce } from 'lodash'; - -import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { useKibana } from '../../../../common/lib/kibana'; - -interface FuncArgs { - fieldSelected: IFieldType | undefined; - value: string | string[] | undefined; - searchQuery: string; - patterns: IIndexPattern | undefined; -} - -type Func = (args: FuncArgs) => void; - -export type UseFieldValueAutocompleteReturn = [boolean, boolean, string[], Func | null]; - -export interface UseFieldValueAutocompleteProps { - selectedField: IFieldType | undefined; - operatorType: OperatorTypeEnum; - fieldValue: string | string[] | undefined; - query: string; - indexPattern: IIndexPattern | undefined; -} - -/** - * Hook for using the field value autocomplete service - * There is a copy within: - * x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks.ts - * - * TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378 - */ -export const useFieldValueAutocomplete = ({ - selectedField, - operatorType, - fieldValue, - query, - indexPattern, -}: UseFieldValueAutocompleteProps): UseFieldValueAutocompleteReturn => { - const { services } = useKibana(); - const [isLoading, setIsLoading] = useState(false); - const [isSuggestingValues, setIsSuggestingValues] = useState(true); - const [suggestions, setSuggestions] = useState([]); - const updateSuggestions = useRef(null); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const fetchSuggestions = debounce( - async ({ fieldSelected, value, searchQuery, patterns }: FuncArgs) => { - try { - if (isSubscribed) { - if (fieldSelected == null || patterns == null) { - return; - } - - if (fieldSelected.type === 'boolean') { - setIsSuggestingValues(false); - return; - } - - setIsLoading(true); - - const field = - fieldSelected.subType != null && fieldSelected.subType.nested != null - ? { - ...fieldSelected, - name: `${fieldSelected.subType.nested.path}.${fieldSelected.name}`, - } - : fieldSelected; - - const newSuggestions = await services.data.autocomplete.getValueSuggestions({ - indexPattern: patterns, - field, - query: searchQuery, - signal: abortCtrl.signal, - }); - - if (newSuggestions.length === 0) { - setIsSuggestingValues(false); - } - - setIsLoading(false); - setSuggestions([...newSuggestions]); - } - } catch (error) { - if (isSubscribed) { - setSuggestions([]); - setIsLoading(false); - } - } - }, - 500 - ); - - if (operatorType !== OperatorTypeEnum.EXISTS) { - fetchSuggestions({ - fieldSelected: selectedField, - value: fieldValue, - searchQuery: query, - patterns: indexPattern, - }); - } - - updateSuggestions.current = fetchSuggestions; - - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [services.data.autocomplete, selectedField, operatorType, fieldValue, indexPattern, query]); - - return [isLoading, isSuggestingValues, suggestions, updateSuggestions.current]; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md b/x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md deleted file mode 100644 index 2bf1867c008d2..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md +++ /dev/null @@ -1,122 +0,0 @@ -# Autocomplete Fields - -Need an input that shows available index fields? Or an input that autocompletes based on a selected indexPattern field? Bingo! That's what these components are for. They are generalized enough so that they can be reused throughout and repurposed based on your needs. - -All three of the available components rely on Eui's combo box. - -## useFieldValueAutocomplete - -This hook uses the kibana `services.data.autocomplete.getValueSuggestions()` service to return possible autocomplete fields based on the passed in `indexPattern` and `selectedField`. - -## FieldComponent - -This component can be used to display available indexPattern fields. It requires an indexPattern to be passed in and will show an error state if value is not one of the available indexPattern fields. Users will be able to select only one option. - -The `onChange` handler is passed `IFieldType[]`. - -```js - -``` - -## OperatorComponent - -This component can be used to display available operators. If you want to pass in your own operators, you can use `operatorOptions` prop. If a `operatorOptions` is provided, those will be used and it will ignore any of the built in logic that determines which operators to show. The operators within `operatorOptions` will still need to be of type `OperatorOption`. - -If no `operatorOptions` is provided, then the following behavior is observed: - -- if `selectedField` type is `boolean`, only `is`, `is not`, `exists`, `does not exist` operators will show -- if `selectedField` type is `nested`, only `is` operator will show -- if not one of the above, all operators will show (see `operators.ts`) - -The `onChange` handler is passed `OperatorOption[]`. - -```js - -``` - -## AutocompleteFieldExistsComponent - -This field value component is used when the selected operator is `exists` or `does not exist`. When these operators are selected, they are equivalent to using a wildcard. The combo box will be displayed as disabled. - -```js - -``` - -## AutocompleteFieldListsComponent - -This component can be used to display available large value lists - when operator selected is `is in list` or `is not in list`. It relies on hooks from the `lists` plugin. Users can only select one list and an error is shown if value is not one of available lists. - -The `selectedValue` should be the `id` of the selected list. - -This component relies on `selectedField` to render available lists. The reason being that it relies on the `selectedField` type to determine which lists to show as each large value list has a type as well. So if a user selects a field of type `ip`, it will only display lists of type `ip`. - -The `onChange` handler is passed `ListSchema`. - -```js - -``` - -## AutocompleteFieldMatchComponent - -This component can be used to allow users to select one single value. It uses the autocomplete hook to display any autocomplete options based on the passed in `indexPattern`, but also allows a user to add their own value. - -It does some minor validation, assuring that field value is a date if `selectedField` type is `date`, a number if `selectedField` type is `number`, an ip if `selectedField` type is `ip`. - -The `onChange` handler is passed selected `string`. - -```js - -``` - -## AutocompleteFieldMatchAnyComponent - -This component can be used to allow users to select multiple values. It uses the autocomplete hook to display any autocomplete options based on the passed in `indexPattern`, but also allows a user to add their own values. - -It does some minor validation, assuring that field values are a date if `selectedField` type is `date`, numbers if `selectedField` type is `number`, ips if `selectedField` type is `ip`. - -The `onChange` handler is passed selected `string[]`. - -```js - -``` diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts deleted file mode 100644 index 084f4b0698aac..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const LOADING = i18n.translate('xpack.securitySolution.autocomplete.loadingDescription', { - defaultMessage: 'Loading...', -}); - -export const SELECT_FIELD_FIRST = i18n.translate( - 'xpack.securitySolution.autocomplete.selectField', - { - defaultMessage: 'Please select a field first...', - } -); - -export const FIELD_REQUIRED_ERR = i18n.translate( - 'xpack.securitySolution.autocomplete.fieldRequiredError', - { - defaultMessage: 'Value cannot be empty', - } -); - -export const NUMBER_ERR = i18n.translate('xpack.securitySolution.autocomplete.invalidNumberError', { - defaultMessage: 'Not a valid number', -}); - -export const DATE_ERR = i18n.translate('xpack.securitySolution.autocomplete.invalidDateError', { - defaultMessage: 'Not a valid date', -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx index 841aa6840cc0b..501ef78d550f9 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -6,13 +6,11 @@ */ import { EuiBasicTableColumn, EuiSpacer, EuiHorizontalRule, EuiTitle, EuiText } from '@elastic/eui'; -import { get, getOr, find } from 'lodash/fp'; +import { get, getOr, find, isEmpty } from 'lodash/fp'; import React, { useMemo } from 'react'; import styled from 'styled-components'; import * as i18n from './translations'; -import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { BrowserFields } from '../../../../common/search_strategy/index_fields'; import { ALERTS_HEADERS_RISK_SCORE, @@ -25,6 +23,7 @@ import { TIMESTAMP, } from '../../../detections/components/alerts_table/translations'; import { + AGENT_STATUS_FIELD_NAME, IP_FIELD_TYPE, SIGNAL_RULE_NAME_FIELD_NAME, } from '../../../timelines/components/timeline/body/renderers/constants'; @@ -35,12 +34,21 @@ import { useRuleWithFallback } from '../../../detections/containers/detection_en import { MarkdownRenderer } from '../markdown_editor'; import { LineClamp } from '../line_clamp'; import { endpointAlertCheck } from '../../utils/endpoint_alert_check'; +import { getEmptyValue } from '../empty_value'; +import { ActionCell } from './table/action_cell'; +import { FieldValueCell } from './table/field_value_cell'; +import { TimelineEventsDetailsItem } from '../../../../common'; +import { EventFieldsData } from './types'; export const Indent = styled.div` padding: 0 8px; word-break: break-word; `; +const StyledEmptyComponent = styled.div` + padding: ${(props) => `${props.theme.eui.paddingSizes.xs} 0`}; +`; + const fields = [ { id: 'signal.status', label: SIGNAL_STATUS }, { id: '@timestamp', label: TIMESTAMP }, @@ -52,7 +60,7 @@ const fields = [ { id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY }, { id: 'signal.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE }, { id: 'host.name' }, - { id: 'agent.status' }, + { id: 'agent.id', overrideField: AGENT_STATUS_FIELD_NAME, label: i18n.AGENT_STATUS }, { id: 'user.name' }, { id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, { id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, @@ -76,22 +84,43 @@ const networkFields = [ ]; const getDescription = ({ - contextId, + data, eventId, - fieldName, - value, - fieldType = '', + fieldFromBrowserField, linkValue, -}: AlertSummaryRow['description']) => ( - -); + timelineId, + values, +}: AlertSummaryRow['description']) => { + if (isEmpty(values)) { + return {getEmptyValue()}; + } + + const eventFieldsData = { + ...data, + ...(fieldFromBrowserField ? fieldFromBrowserField : {}), + } as EventFieldsData; + return ( + <> + + + + ); +}; const getSummaryRows = ({ data, @@ -120,25 +149,45 @@ const getSummaryRows = ({ return data != null ? tableFields.reduce((acc, item) => { + const initialDescription = { + contextId: timelineId, + eventId, + value: null, + fieldType: 'string', + linkValue: undefined, + timelineId, + }; const field = data.find((d) => d.field === item.id); if (!field) { - return acc; + return [ + ...acc, + { + title: item.label ?? item.id, + description: initialDescription, + }, + ]; } + const linkValueField = item.linkField != null && data.find((d) => d.field === item.linkField); const linkValue = getOr(null, 'originalValue.0', linkValueField); const value = getOr(null, 'originalValue.0', field); - const category = field.category; - const fieldType = get(`${category}.fields.${field.field}.type`, browserFields) as string; + const category = field.category ?? ''; + const fieldName = field.field ?? ''; + + const browserField = get([category, 'fields', fieldName], browserFields); const description = { - contextId: timelineId, - eventId, - fieldName: item.id, - value, - fieldType: item.fieldType ?? fieldType, + ...initialDescription, + data: { ...field, ...(item.overrideField ? { field: item.overrideField } : {}) }, + values: field.values, linkValue: linkValue ?? undefined, + fieldFromBrowserField: browserField, }; + if (item.id === 'agent.id' && !endpointAlertCheck({ data })) { + return acc; + } + if (item.id === 'signal.threshold_result.terms') { try { const terms = getOr(null, 'originalValue', field); @@ -149,14 +198,14 @@ const getSummaryRows = ({ title: `${entry.field} [threshold]`, description: { ...description, - value: entry.value, + values: [entry.value], }, }; } ); return [...acc, ...thresholdTerms]; } catch (err) { - return acc; + return [...acc]; } } @@ -169,7 +218,7 @@ const getSummaryRows = ({ title: ALERTS_HEADERS_THRESHOLD_CARDINALITY, description: { ...description, - value: `count(${parsedValue.field}) == ${parsedValue.value}`, + values: [`count(${parsedValue.field}) == ${parsedValue.value}`], }, }, ]; @@ -205,28 +254,6 @@ const AlertSummaryViewComponent: React.FC<{ timelineId, ]); - const isEndpointAlert = useMemo(() => { - return endpointAlertCheck({ data }); - }, [data]); - - const endpointId = useMemo(() => { - const findAgentId = find({ category: 'agent', field: 'agent.id' }, data)?.values; - return findAgentId ? findAgentId[0] : ''; - }, [data]); - - const agentStatusRow = { - title: i18n.AGENT_STATUS, - description: { - contextId: timelineId, - eventId, - fieldName: 'agent.status', - value: endpointId, - linkValue: undefined, - }, - }; - - const summaryRowsWithAgentStatus = [...summaryRows, agentStatusRow]; - const ruleId = useMemo(() => { const item = data.find((d) => d.field === 'signal.rule.id'); return Array.isArray(item?.originalValue) @@ -238,11 +265,7 @@ const AlertSummaryViewComponent: React.FC<{ return ( <> - + {maybeRule?.note && ( <> diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx index 198277e1fb941..9d254b0d27f6b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx @@ -12,9 +12,10 @@ import { useMountAppended } from '../../utils/use_mount_appended'; import { mockBrowserFields } from '../../containers/source/mock'; import { EventFieldsData } from './types'; +jest.mock('../../lib/kibana'); interface Column { field: string; - name: string; + name: string | JSX.Element; sortable: boolean; render: (field: string, data: EventFieldsData) => JSX.Element; } @@ -42,39 +43,71 @@ describe('getColumns', () => { }); }); - describe('column checkbox', () => { - let checkboxColumn: Column; + describe('column actions', () => { + let actionsColumn: Column; const mockDataToUse = mockBrowserFields.agent; + const testValue = 'testValue'; const testData = { type: 'someType', category: 'agent', + field: 'agent.id', ...mockDataToUse, } as EventFieldsData; beforeEach(() => { - checkboxColumn = getColumns(defaultProps)[0] as Column; + actionsColumn = getColumns(defaultProps)[0] as Column; }); - test('should be enabled when the field does not exist', () => { - const testField = 'nonExistingField'; - const wrapper = mount( - {checkboxColumn.render(testField, testData)} - ) as ReactWrapper; - expect( - wrapper.find(`[data-test-subj="toggle-field-${testField}"]`).first().prop('disabled') - ).toBe(false); + describe('filter in', () => { + test('it renders a filter for (+) button', () => { + const wrapper = mount( + {actionsColumn.render(testValue, testData)} + ) as ReactWrapper; + + expect(wrapper.find('[data-test-subj="hover-actions-filter-for"]').exists()).toBeTruthy(); + }); + }); + + describe('filter out', () => { + test('it renders a filter out (-) button', () => { + const wrapper = mount( + {actionsColumn.render(testValue, testData)} + ) as ReactWrapper; + + expect(wrapper.find('[data-test-subj="hover-actions-filter-out"]').exists()).toBeTruthy(); + }); }); - test('should be enabled when the field does exist', () => { - const testField = mockDataToUse.fields - ? Object.keys(mockDataToUse.fields)[0] - : 'agent.hostname'; - const wrapper = mount( - {checkboxColumn.render(testField, testData)} - ) as ReactWrapper; - expect( - wrapper.find(`[data-test-subj="toggle-field-${testField}"]`).first().prop('disabled') - ).toBe(false); + describe('add to timeline', () => { + test('it renders an add to timeline button', () => { + const wrapper = mount( + {actionsColumn.render(testValue, testData)} + ) as ReactWrapper; + + expect(wrapper.find('[data-test-subj="hover-actions-add-timeline"]').exists()).toBeTruthy(); + }); + }); + + describe('column toggle', () => { + test('it renders a column toggle button', () => { + const wrapper = mount( + {actionsColumn.render(testValue, testData)} + ) as ReactWrapper; + + expect( + wrapper.find('[data-test-subj="hover-actions-toggle-column"]').exists() + ).toBeTruthy(); + }); + }); + + describe('copy', () => { + test('it renders a copy button', () => { + const wrapper = mount( + {actionsColumn.render(testValue, testData)} + ) as ReactWrapper; + + expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBeTruthy(); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index 1be05cc560552..b8c10cf506476 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -7,37 +7,19 @@ /* eslint-disable react/display-name */ -import { - EuiCheckbox, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPanel, - EuiToolTip, - EuiIconTip, - EuiText, -} from '@elastic/eui'; -import { get, isEmpty } from 'lodash'; +import { EuiPanel, EuiText } from '@elastic/eui'; +import { get } from 'lodash'; import memoizeOne from 'memoize-one'; import React from 'react'; import styled from 'styled-components'; -import { onFocusReFocusDraggable } from '../../../../../timelines/public'; import { BrowserFields } from '../../containers/source'; -import { DragEffects } from '../drag_and_drop/draggable_wrapper'; -import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; -import { DRAG_TYPE_FIELD, getDroppableId } from '../drag_and_drop/helpers'; -import { DraggableFieldBadge } from '../draggables/field_badge'; -import { DraggableFieldsBrowserField } from '../../../timelines/components/fields_browser/field_items'; -import { OverflowField } from '../tables/helpers'; -import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; -import { MESSAGE_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants'; -import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; -import { getIconFromType, getExampleText } from './helpers'; import * as i18n from './translations'; import { EventFieldsData } from './types'; import { ColumnHeaderOptions } from '../../../../common'; +import { FieldValueCell } from './table/field_value_cell'; +import { FieldNameCell } from './table/field_name_cell'; +import { ActionCell } from './table/action_cell'; const HoverActionsContainer = styled(EuiPanel)` align-items: center; @@ -53,13 +35,6 @@ const HoverActionsContainer = styled(EuiPanel)` HoverActionsContainer.displayName = 'HoverActionsContainer'; -const FullWidthFlexGroup = styled(EuiFlexGroup)` - width: 100%; -`; - -const FullWidthFlexItem = styled(EuiFlexItem)` - width: 100%; -`; export const getFieldFromBrowserField = memoizeOne( (keys: string[], browserFields: BrowserFields): BrowserFields => get(browserFields, keys), (newArgs, lastArgs) => newArgs[0].join() === lastArgs[0].join() @@ -84,37 +59,46 @@ export const getColumns = ({ getLinkValue: (field: string) => string | null; }) => [ { - field: 'field', - name: '', + field: 'values', + name: ( + + {i18n.ACTIONS} + + ), sortable: false, truncateText: false, - width: '30px', - render: (field: string, data: EventFieldsData) => { - const label = data.isObjectArray ? i18n.NESTED_COLUMN(field) : i18n.VIEW_COLUMN(field); + width: '180px', + render: (values: string[] | null | undefined, data: EventFieldsData) => { + const label = data.isObjectArray + ? i18n.NESTED_COLUMN(data.field) + : i18n.VIEW_COLUMN(data.field); + const fieldFromBrowserField = getFieldFromBrowserField( + [data.category, 'fields', data.field], + browserFields + ); return ( - - c.id === field) !== -1} - data-test-subj={`toggle-field-${field}`} - data-colindex={1} - id={field} - onChange={() => - toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }) - } - disabled={data.isObjectArray && data.type !== 'geo_point'} - /> - + ); }, }, { field: 'field', - name: i18n.FIELD, + className: 'eventFieldsTable__fieldNameCell', + name: ( + + {i18n.FIELD} + + ), sortable: true, truncateText: false, render: (field: string, data: EventFieldsData) => { @@ -123,67 +107,23 @@ export const getColumns = ({ browserFields ); return ( - - - - - - - - {(data.isObjectArray && data.type !== 'geo_point') || fieldFromBrowserField == null ? ( - {field} - ) : ( - ( -
- - - -
- )} - > - -
- )} -
- {!isEmpty(data.description) && ( - - - - )} -
+ ); }, }, { field: 'values', - name: i18n.VALUE, + className: 'eventFieldsTable__fieldValueCell', + name: ( + + {i18n.VALUE} + + ), sortable: true, truncateText: false, render: (values: string[] | null | undefined, data: EventFieldsData) => { @@ -192,57 +132,15 @@ export const getColumns = ({ browserFields ); return ( - - {values != null && - values.map((value, i) => { - if (fieldFromBrowserField == null) { - return {value}; - } - return ( - -
- {data.field === MESSAGE_FIELD_NAME ? ( - - ) : ( - - )} -
-
- ); - })} -
+ ); }, }, - { - field: 'valuesConcatenated', - name: i18n.BLANK, - render: () => null, - sortable: false, - truncateText: true, - width: '1px', - }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index f323a8c8b4a08..3af97615d6153 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -63,7 +63,7 @@ describe('EventDetails', () => { }); describe('tabs', () => { - ['Table', 'JSON View'].forEach((tab) => { + ['Table', 'JSON'].forEach((tab) => { test(`it renders the ${tab} tab`, () => { expect( wrapper @@ -82,7 +82,7 @@ describe('EventDetails', () => { }); describe('alerts tabs', () => { - ['Overview', 'Threat Intel', 'Table', 'JSON View'].forEach((tab) => { + ['Overview', 'Threat Intel', 'Table', 'JSON'].forEach((tab) => { test(`it renders the ${tab} tab`, () => { const expectedCopy = tab === 'Threat Intel' ? `${tab} (1)` : tab; expect( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx index 555b67da953d6..80c014771ae68 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import '../../mock/match_media'; import { mockDetailItemData, mockDetailItemDataId } from '../../mock/mock_detail_item'; import { TestProviders } from '../../mock/test_providers'; -import { timelineActions } from '../../../timelines/store/timeline'; import { EventFieldsBrowser } from './event_fields_browser'; import { mockBrowserFields } from '../../containers/source/mock'; import { useMountAppended } from '../../utils/use_mount_appended'; @@ -43,7 +42,7 @@ describe('EventFieldsBrowser', () => { const mount = useMountAppended(); describe('column headers', () => { - ['Field', 'Value'].forEach((header) => { + ['Actions', 'Field', 'Value'].forEach((header) => { test(`it renders the ${header} column header`, () => { const wrapper = mount( @@ -57,7 +56,7 @@ describe('EventFieldsBrowser', () => { ); - expect(wrapper.find('thead').containsMatchingElement({header})).toBeTruthy(); + expect(wrapper.find('thead').contains(header)).toBeTruthy(); }); }); }); @@ -82,12 +81,10 @@ describe('EventFieldsBrowser', () => { }); }); - describe('toggle column checkbox', () => { + describe('Hover Actions', () => { const eventId = 'pEMaMmkBUV60JmNWmWVi'; - test('it renders an UNchecked checkbox for a field that is not a member of columnHeaders', () => { - const field = 'agent.id'; - + test('it renders a filter for (+) button', () => { const wrapper = mount( { ); - expect(wrapper.find(`[data-test-subj="toggle-field-${field}"]`).first().props().checked).toBe( - false - ); + expect(wrapper.find('[data-test-subj="hover-actions-filter-for"]').exists()).toBeTruthy(); }); - test('it renders an checked checkbox for a field that is a member of columnHeaders', () => { - const field = '@timestamp'; - + test('it renders a filter out (-) button', () => { const wrapper = mount( { ); - expect(wrapper.find(`[data-test-subj="toggle-field-${field}"]`).first().props().checked).toBe( - true - ); + expect(wrapper.find('[data-test-subj="hover-actions-filter-out"]').exists()).toBeTruthy(); }); - test('it invokes toggleColumn when the checkbox is clicked', () => { - const field = '@timestamp'; + test('it renders an add to timeline button', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="hover-actions-add-timeline"]').exists()).toBeTruthy(); + }); + test('it renders a column toggle button', () => { const wrapper = mount( { ); - wrapper - .find(`[data-test-subj="toggle-field-${field}"]`) - .find(`input[type="checkbox"]`) - .first() - .simulate('change', { - target: { checked: true }, - }); - wrapper.update(); + expect( + wrapper.find('[data-test-subj="hover-actions-toggle-column"]').first().exists() + ).toBeTruthy(); + }); - expect(mockDispatch).toBeCalledWith( - timelineActions.removeColumn({ - columnId: '@timestamp', - id: 'test', - }) + test('it renders a copy button', () => { + const wrapper = mount( + + + ); + + expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBeTruthy(); }); }); @@ -219,7 +228,7 @@ describe('EventFieldsBrowser', () => { .find('[data-euiicon-type]') .last() .prop('data-euiicon-type') - ).toEqual('iInCircle'); + ).toEqual('tokenDate'); }); }); @@ -257,12 +266,7 @@ describe('EventFieldsBrowser', () => { ); expect( - wrapper - .find('.euiTableRow') - .find('.euiTableRowCell') - .at(1) - .find('EuiIconTip') - .prop('content') + wrapper.find('[data-test-subj="field-name-cell"]').at(0).find('EuiToolTip').prop('content') ).toContain( 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events. Example: 2016-05-23T08:05:34.853Z' ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index 3ad7e9aef19dc..fa792cda46034 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -42,13 +42,11 @@ const TableWrapper = styled.div` display: flex; flex: 1; overflow: hidden; - > div { display: flex; flex-direction: column; flex: 1; overflow: hidden; - > .euiFlexGroup:first-of-type { flex: 0; } @@ -59,32 +57,86 @@ const TableWrapper = styled.div` const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` flex: 1; overflow: auto; - &::-webkit-scrollbar { height: ${({ theme }) => theme.eui.euiScrollBar}; width: ${({ theme }) => theme.eui.euiScrollBar}; } - &::-webkit-scrollbar-thumb { background-clip: content-box; background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; } - &::-webkit-scrollbar-corner, &::-webkit-scrollbar-track { background-color: transparent; } + + .eventFieldsTable__fieldIcon { + padding-top: ${({ theme }) => parseFloat(theme.eui.euiSizeXS) * 1.5}px; + } + + .eventFieldsTable__fieldName { + line-height: ${({ theme }) => theme.eui.euiLineHeight}; + padding: ${({ theme }) => theme.eui.euiSizeXS}; + } + + // TODO: Use this logic from discover + /* .eventFieldsTable__multiFieldBadge { + font: ${({ theme }) => theme.eui.euiFont}; + } */ + + .eventFieldsTable__tableRow { + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; + + .eventFieldsTable__hoverActionButtons { + &:focus-within { + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + opacity: 1; + } + } + } + &:hover { + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + opacity: 1; + } + } + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + // TODO: Using this logic from discover + /* @include euiBreakpoint('m', 'l', 'xl') { + opacity: 0; + } */ + opacity: 0; + &:focus { + opacity: 1; + } + } + } + + .eventFieldsTable__actionCell, + .eventFieldsTable__fieldNameCell { + align-items: flex-start; + padding: ${({ theme }) => theme.eui.euiSizeXS}; + } + + .eventFieldsTable__fieldValue { + display: inline-block; + word-break: break-all; + word-wrap: break-word; + white-space: pre-wrap; + line-height: ${({ theme }) => theme.eui.euiLineHeight}; + color: ${({ theme }) => theme.eui.euiColorFullShade}; + vertical-align: top; + } `; /** * This callback, invoked via `EuiInMemoryTable`'s `rowProps, assigns * attributes to every ``. */ -const getAriaRowindex = (timelineEventsDetailsItem: TimelineEventsDetailsItem) => - timelineEventsDetailsItem.ariaRowindex != null - ? { 'data-rowindex': timelineEventsDetailsItem.ariaRowindex } - : {}; /** Renders a table view or JSON view of the `ECS` `data` */ export const EventFieldsBrowser = React.memo( @@ -144,6 +196,15 @@ export const EventFieldsBrowser = React.memo( [columnHeaders, dispatch, timelineId] ); + const onSetRowProps = useCallback(({ ariaRowindex, field }: TimelineEventsDetailsItem) => { + const rowIndex = ariaRowindex != null ? { 'data-rowindex': ariaRowindex } : {}; + return { + ...rowIndex, + className: 'eventFieldsTable__tableRow', + 'data-test-subj': `event-fields-table-row-${field}`, + }; + }, []); + const onUpdateColumns = useCallback( (columns) => dispatch(timelineActions.updateColumns({ id: timelineId, columns })), [dispatch, timelineId] @@ -218,7 +279,7 @@ export const EventFieldsBrowser = React.memo( items={items} columns={columns} pagination={false} - rowProps={getAriaRowindex} + rowProps={onSetRowProps} search={search} sorting={false} /> diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx index 2b300789c4d14..ecfa243f89246 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx @@ -23,7 +23,7 @@ import { } from '../../../timelines/components/timeline/body/constants'; import * as i18n from './translations'; -import { ColumnHeaderOptions } from '../../../../common'; +import { ColumnHeaderOptions, TimelineEventsDetailsItem } from '../../../../common'; /** * Defines the behavior of the search input that appears above the table of data @@ -55,12 +55,12 @@ export interface Item { export interface AlertSummaryRow { title: string; description: { - contextId: string; + data: TimelineEventsDetailsItem; eventId: string; - fieldName: string; - value: string; - fieldType: string; + fieldFromBrowserField?: Readonly>>; linkValue: string | undefined; + timelineId: string; + values: string[] | null | undefined; }; } @@ -213,7 +213,7 @@ export const getSummaryColumns = ( field: 'title', truncateText: false, render: getTitle, - width: '160px', + width: '33%', name: '', }, { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx new file mode 100644 index 0000000000000..f5cf600e281ad --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useState, useRef } from 'react'; +import { getDraggableId } from '@kbn/securitysolution-t-grid'; +import { HoverActions } from '../../hover_actions'; +import { useActionCellDataProvider } from './use_action_cell_data_provider'; +import { EventFieldsData } from '../types'; +import { useGetTimelineId } from '../../drag_and_drop/draggable_wrapper_hover_content'; +import { ColumnHeaderOptions } from '../../../../../common/types/timeline'; +import { BrowserField } from '../../../containers/source'; + +interface Props { + contextId: string; + data: EventFieldsData; + disabled?: boolean; + eventId: string; + fieldFromBrowserField?: Readonly>>; + getLinkValue?: (field: string) => string | null; + linkValue?: string | null | undefined; + onFilterAdded?: () => void; + timelineId?: string; + toggleColumn?: (column: ColumnHeaderOptions) => void; + values: string[] | null | undefined; +} + +export const ActionCell: React.FC = React.memo( + ({ + contextId, + data, + eventId, + fieldFromBrowserField, + getLinkValue, + linkValue, + onFilterAdded, + timelineId, + toggleColumn, + values, + }) => { + const actionCellConfig = useActionCellDataProvider({ + contextId, + eventId, + field: data.field, + fieldFormat: data.format, + fieldFromBrowserField, + fieldType: data.type, + isObjectArray: data.isObjectArray, + linkValue: (getLinkValue && getLinkValue(data.field)) ?? linkValue, + values, + }); + + const draggableRef = useRef(null); + const [showTopN, setShowTopN] = useState(false); + const [goGetTimelineId, setGoGetTimelineId] = useState(false); + const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId); + const [hoverActionsOwnFocus] = useState(false); + + const toggleTopN = useCallback(() => { + setShowTopN((prevShowTopN) => { + const newShowTopN = !prevShowTopN; + return newShowTopN; + }); + }, []); + + const draggableIds = actionCellConfig?.idList.map((id) => getDraggableId(id)); + return ( + + ); + } +); + +ActionCell.displayName = 'ActionCell'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_name_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_name_cell.tsx new file mode 100644 index 0000000000000..e62d7f90b9f1d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_name_cell.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiText, EuiToolTip } from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import * as i18n from '../translations'; +import { FieldIcon } from '../../../../../../../../src/plugins/kibana_react/public'; +import { IndexPatternField } from '../../../../../../../../src/plugins/data/public'; +import { getExampleText } from '../helpers'; +import { BrowserField } from '../../../containers/source'; +import { EventFieldsData } from '../types'; +import { getFieldTypeName } from './get_field_type_name'; + +export interface FieldNameCellProps { + data: EventFieldsData; + field: string; + fieldFromBrowserField: Readonly>>; + fieldMapping?: IndexPatternField; + scripted?: boolean; +} +export const FieldNameCell = React.memo( + ({ data, field, fieldMapping, scripted }: FieldNameCellProps) => { + const typeName = getFieldTypeName(data.type); + // TODO: We don't have fieldMapping or isMultiField until kibana indexPatterns is implemented. Will default to field for now + const displayName = fieldMapping && fieldMapping.displayName ? fieldMapping.displayName : field; + const defaultTooltip = displayName !== field ? `${field} (${displayName})` : field; + // TODO: Remove. This is what was used to show the plaintext fieldName vs the tooltip one + // const showPlainTextName = + // (data.isObjectArray && data.type !== 'geo_point') || fieldFromBrowserField == null; + const isMultiField = !!fieldMapping?.spec?.subType?.multi; + return ( + <> + + + + + + + + {field} + + + + {isMultiField && ( + + + {i18n.MULTI_FIELD_BADGE} + + + )} + + + ); + } +); + +FieldNameCell.displayName = 'FieldNameCell'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx new file mode 100644 index 0000000000000..2ac0ca23ca8c1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiText } from '@elastic/eui'; +import { BrowserField } from '../../../containers/source'; +import { OverflowField } from '../../tables/helpers'; +import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; +import { MESSAGE_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants'; +import { EventFieldsData } from '../types'; + +export interface FieldValueCellProps { + contextId: string; + data: EventFieldsData; + eventId: string; + fieldFromBrowserField?: Readonly>>; + getLinkValue?: (field: string) => string | null; + linkValue?: string | null | undefined; + values: string[] | null | undefined; +} + +export const FieldValueCell = React.memo( + ({ + contextId, + data, + eventId, + fieldFromBrowserField, + getLinkValue, + linkValue, + values, + }: FieldValueCellProps) => { + return ( +
+ {values != null && + values.map((value, i) => { + if (fieldFromBrowserField == null) { + return ( + + {value} + + ); + } + return ( +
+ {data.field === MESSAGE_FIELD_NAME ? ( + + ) : ( + + )} +
+ ); + })} +
+ ); + } +); + +FieldValueCell.displayName = 'FieldValueCell'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/get_field_type_name.ts b/x-pack/plugins/security_solution/public/common/components/event_details/table/get_field_type_name.ts new file mode 100644 index 0000000000000..cd753b7e76e95 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/get_field_type_name.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export function getFieldTypeName(type: string) { + switch (type) { + case 'boolean': + return i18n.translate('xpack.securitySolution.fieldNameIcons.booleanAriaLabel', { + defaultMessage: 'Boolean field', + }); + case 'conflict': + return i18n.translate('xpack.securitySolution.fieldNameIcons.conflictFieldAriaLabel', { + defaultMessage: 'Conflicting field', + }); + case 'date': + return i18n.translate('xpack.securitySolution.fieldNameIcons.dateFieldAriaLabel', { + defaultMessage: 'Date field', + }); + case 'geo_point': + return i18n.translate('xpack.securitySolution.fieldNameIcons.geoPointFieldAriaLabel', { + defaultMessage: 'Geo point field', + }); + case 'geo_shape': + return i18n.translate('xpack.securitySolution.fieldNameIcons.geoShapeFieldAriaLabel', { + defaultMessage: 'Geo shape field', + }); + case 'ip': + return i18n.translate('xpack.securitySolution.fieldNameIcons.ipAddressFieldAriaLabel', { + defaultMessage: 'IP address field', + }); + case 'murmur3': + return i18n.translate('xpack.securitySolution.fieldNameIcons.murmur3FieldAriaLabel', { + defaultMessage: 'Murmur3 field', + }); + case 'number': + return i18n.translate('xpack.securitySolution.fieldNameIcons.numberFieldAriaLabel', { + defaultMessage: 'Number field', + }); + case 'source': + // Note that this type is currently not provided, type for _source is undefined + return i18n.translate('xpack.securitySolution.fieldNameIcons.sourceFieldAriaLabel', { + defaultMessage: 'Source field', + }); + case 'string': + return i18n.translate('xpack.securitySolution.fieldNameIcons.stringFieldAriaLabel', { + defaultMessage: 'String field', + }); + case 'nested': + return i18n.translate('xpack.securitySolution.fieldNameIcons.nestedFieldAriaLabel', { + defaultMessage: 'Nested field', + }); + default: + return i18n.translate('xpack.securitySolution.fieldNameIcons.unknownFieldAriaLabel', { + defaultMessage: 'Unknown field', + }); + } +} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts b/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts new file mode 100644 index 0000000000000..24f660e3315c5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable complexity */ + +import { escapeDataProviderId } from '@kbn/securitysolution-t-grid'; +import { isArray, isEmpty, isString } from 'lodash/fp'; +import { + AGENT_STATUS_FIELD_NAME, + EVENT_MODULE_FIELD_NAME, + EVENT_URL_FIELD_NAME, + GEO_FIELD_TYPE, + HOST_NAME_FIELD_NAME, + IP_FIELD_TYPE, + MESSAGE_FIELD_NAME, + REFERENCE_URL_FIELD_NAME, + RULE_REFERENCE_FIELD_NAME, + SIGNAL_RULE_NAME_FIELD_NAME, + SIGNAL_STATUS_FIELD_NAME, +} from '../../../../timelines/components/timeline/body/renderers/constants'; +import { BYTES_FORMAT } from '../../../../timelines/components/timeline/body/renderers/bytes'; +import { EVENT_DURATION_FIELD_NAME } from '../../../../timelines/components/duration'; +import { PORT_NAMES } from '../../../../network/components/port'; +import { INDICATOR_REFERENCE } from '../../../../../common/cti/constants'; +import { BrowserField } from '../../../containers/source'; + +export interface UseActionCellDataProvider { + contextId?: string; + eventId?: string; + field: string; + fieldFormat?: string; + fieldFromBrowserField?: Readonly>>; + fieldType?: string; + isObjectArray?: boolean; + linkValue?: string | null; + values: string[] | null | undefined; +} + +export const useActionCellDataProvider = ({ + contextId, + eventId, + field, + fieldFormat, + fieldFromBrowserField, + fieldType, + isObjectArray, + linkValue, + values, +}: UseActionCellDataProvider): { idList: string[]; stringValues: string[] } | null => { + if (values === null || values === undefined) return null; + + const stringifiedValues: string[] = []; + const arrayValues = Array.isArray(values) ? values : [values]; + + const idList: string[] = arrayValues.reduce((memo, value, index) => { + let id = null; + let valueAsString: string = isString(value) ? value : `${values}`; + if (fieldFromBrowserField == null) { + stringifiedValues.push(valueAsString); + return memo; + } + const appendedUniqueId = `${contextId}-${eventId}-${field}-${index}-${value}-${eventId}-${field}-${value}`; + if (isObjectArray || fieldType === GEO_FIELD_TYPE || [MESSAGE_FIELD_NAME].includes(field)) { + stringifiedValues.push(valueAsString); + return memo; + } else if (fieldType === IP_FIELD_TYPE) { + id = `formatted-ip-data-provider-${contextId}-${field}-${value}-${eventId}`; + if (isString(value) && !isEmpty(value)) { + try { + const addresses = JSON.parse(value); + if (isArray(addresses)) { + valueAsString = addresses.join(','); + } + } catch (_) { + // Default to keeping the existing string value + } + } + } else if (PORT_NAMES.some((portName) => field === portName)) { + id = `port-default-draggable-${appendedUniqueId}`; + } else if (field === EVENT_DURATION_FIELD_NAME) { + id = `duration-default-draggable-${appendedUniqueId}`; + } else if (field === HOST_NAME_FIELD_NAME) { + id = `event-details-value-default-draggable-${appendedUniqueId}`; + } else if (fieldFormat === BYTES_FORMAT) { + id = `bytes-default-draggable-${appendedUniqueId}`; + } else if (field === SIGNAL_RULE_NAME_FIELD_NAME) { + id = `event-details-value-default-draggable-${appendedUniqueId}-${linkValue}`; + } else if (field === EVENT_MODULE_FIELD_NAME) { + id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`; + } else if (field === SIGNAL_STATUS_FIELD_NAME) { + id = `alert-details-value-default-draggable-${appendedUniqueId}`; + } else if (field === AGENT_STATUS_FIELD_NAME) { + const valueToUse = typeof value === 'string' ? value : ''; + id = `event-details-value-default-draggable-${appendedUniqueId}`; + valueAsString = valueToUse; + } else if ( + [ + RULE_REFERENCE_FIELD_NAME, + REFERENCE_URL_FIELD_NAME, + EVENT_URL_FIELD_NAME, + INDICATOR_REFERENCE, + ].includes(field) + ) { + id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`; + } else { + id = `event-details-value-default-draggable-${appendedUniqueId}`; + } + stringifiedValues.push(valueAsString); + memo.push(escapeDataProviderId(id)); + return memo; + }, [] as string[]); + + return { + idList, + stringValues: stringifiedValues, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index 56d5009c34d72..98fd0c61a5393 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -34,7 +34,7 @@ export const TABLE = i18n.translate('xpack.securitySolution.eventDetails.table', }); export const JSON_VIEW = i18n.translate('xpack.securitySolution.eventDetails.jsonView', { - defaultMessage: 'JSON View', + defaultMessage: 'JSON', }); export const FIELD = i18n.translate('xpack.securitySolution.eventDetails.field', { @@ -83,3 +83,21 @@ export const NESTED_COLUMN = (field: string) => export const AGENT_STATUS = i18n.translate('xpack.securitySolution.detections.alerts.agentStatus', { defaultMessage: 'Agent status', }); + +export const MULTI_FIELD_TOOLTIP = i18n.translate( + 'xpack.securitySolution.eventDetails.multiFieldTooltipContent', + { + defaultMessage: 'Multi-fields can have multiple values per field', + } +); + +export const MULTI_FIELD_BADGE = i18n.translate( + 'xpack.securitySolution.eventDetails.multiFieldBadge', + { + defaultMessage: 'multi-field', + } +); + +export const ACTIONS = i18n.translate('xpack.securitySolution.eventDetails.table.actions', { + defaultMessage: 'Actions', +}); 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 bf6a94c53b477..af8058e25adc6 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 @@ -185,7 +185,7 @@ describe('Exception helpers', () => { meta: {}, name: 'some name', namespace_type: 'single', - os_types: ['linux'], + os_types: [], tags: ['user added string for a tag', 'malware'], type: 'simple', }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx index e3e9ba1bfa132..1d5094021c3d3 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx @@ -157,7 +157,7 @@ describe('ExceptionDetails', () => { }); test('it renders the operating system if one is specified in the exception item', () => { - const exceptionItem = getExceptionListItemSchemaMock(); + const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); const wrapper = mount( { }); test('it renders the exception item creator', () => { - const exceptionItem = getExceptionListItemSchemaMock(); + const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); const wrapper = mount( { }); test('it renders the exception item creation timestamp', () => { - const exceptionItem = getExceptionListItemSchemaMock(); + const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); const wrapper = mount( { }); test('it renders the description if one is included on the exception item', () => { - const exceptionItem = getExceptionListItemSchemaMock(); + const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); const wrapper = mount( { }); test('it renders with Name and Modified info when showName and showModified props are true', () => { - const exceptionItem = getExceptionListItemSchemaMock(); + const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); exceptionItem.comments = []; const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx index 634c7975a13a9..d67f526fa9bdc 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx @@ -154,7 +154,7 @@ describe('Exception viewer helpers', () => { describe('#getDescriptionListContent', () => { test('it returns formatted description list with os if one is specified', () => { - const payload = getExceptionListItemSchemaMock(); + const payload = getExceptionListItemSchemaMock({ os_types: ['linux'] }); payload.description = ''; const result = getDescriptionListContent(payload); const expected: DescriptionListItem[] = [ @@ -176,7 +176,7 @@ describe('Exception viewer helpers', () => { }); test('it returns formatted description list with a description if one specified', () => { - const payload = getExceptionListItemSchemaMock(); + const payload = getExceptionListItemSchemaMock({ os_types: ['linux'] }); payload.description = 'Im a description'; const result = getDescriptionListContent(payload); const expected: DescriptionListItem[] = [ @@ -202,7 +202,7 @@ describe('Exception viewer helpers', () => { }); test('it returns just user and date created if no other fields specified', () => { - const payload = getExceptionListItemSchemaMock(); + const payload = getExceptionListItemSchemaMock({ os_types: ['linux'] }); payload.description = ''; const result = getDescriptionListContent(payload); const expected: DescriptionListItem[] = [ @@ -224,7 +224,10 @@ describe('Exception viewer helpers', () => { }); test('it returns Modified By/On info. when `includeModified` is true', () => { - const result = getDescriptionListContent(getExceptionListItemSchemaMock(), true); + const result = getDescriptionListContent( + getExceptionListItemSchemaMock({ os_types: ['linux'] }), + true + ); expect(result).toEqual([ { description: 'Linux', @@ -254,7 +257,11 @@ describe('Exception viewer helpers', () => { }); test('it returns Name when `includeName` is true', () => { - const result = getDescriptionListContent(getExceptionListItemSchemaMock(), false, true); + const result = getDescriptionListContent( + getExceptionListItemSchemaMock({ os_types: ['linux'] }), + false, + true + ); expect(result).toEqual([ { description: 'some name', diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx index dea19e1366875..46d05d9712227 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx @@ -77,6 +77,7 @@ export interface HeaderPageProps extends HeaderProps { children?: React.ReactNode; draggableArguments?: DraggableArguments; hideSourcerer?: boolean; + sourcererScope?: SourcererScopeName; subtitle?: SubtitleProps['items']; subtitle2?: SubtitleProps['items']; title: TitleProp; @@ -115,6 +116,7 @@ const HeaderPageComponent: React.FC = ({ draggableArguments, hideSourcerer = false, isLoading, + sourcererScope = SourcererScopeName.default, subtitle, subtitle2, title, @@ -145,7 +147,7 @@ const HeaderPageComponent: React.FC = ({ {children} )} - {!hideSourcerer && } + {!hideSourcerer && } {/* Manually add a 'padding-bottom' to header */} diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx index fba9dd7346004..0fc8a74084521 100644 --- a/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React from 'react'; -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { EuiButtonIcon, EuiPopover, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { StatefulTopN } from '../../top_n'; import { TimelineId } from '../../../../../common/types/timeline'; @@ -44,17 +44,31 @@ export const ShowTopNButton: React.FC = React.memo( ? SourcererScopeName.detections : SourcererScopeName.default; const { browserFields, indexPattern } = useSourcererScope(activeScope); - + const button = useMemo( + () => ( + + ), + [field, onClick] + ); return showTopN ? ( - + + + ) : ( = React.memo( /> } > - + {button} ); } diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx index bd5d78fd4e85f..31bdf78626e7c 100644 --- a/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx @@ -39,7 +39,8 @@ export const AdditionalContent = styled.div` AdditionalContent.displayName = 'AdditionalContent'; const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean }>` - padding: ${(props) => (props.$showTopN ? 'none' : props.theme.eui.paddingSizes.s)}; + padding: ${(props) => `0 ${props.theme.eui.paddingSizes.s}`}; + display: flex; &:focus-within { .timelines__hoverActionButton, @@ -57,11 +58,7 @@ const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean }>` .timelines__hoverActionButton, .securitySolution__hoverActionButton { - // TODO: Using this logic from discover - /* @include euiBreakpoint('m', 'l', 'xl') { - opacity: 0; - } */ - opacity: 0; + opacity: ${(props) => (props.$showTopN ? 1 : 0)}; &:focus { opacity: 1; @@ -72,17 +69,17 @@ const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean }>` interface Props { additionalContent?: React.ReactNode; dataType?: string; - draggableId?: DraggableId; + draggableIds?: DraggableId[]; field: string; - isObjectArray: boolean; goGetTimelineId?: (args: boolean) => void; + isObjectArray: boolean; onFilterAdded?: () => void; ownFocus: boolean; showTopN: boolean; timelineId?: string | null; toggleColumn?: (column: ColumnHeaderOptions) => void; toggleTopN: () => void; - value?: string[] | string | null; + values?: string[] | string | null; } /** Returns a value for the `disabled` prop of `EuiFocusTrap` */ @@ -104,7 +101,7 @@ export const HoverActions: React.FC = React.memo( ({ additionalContent = null, dataType, - draggableId, + draggableIds, field, goGetTimelineId, isObjectArray, @@ -114,7 +111,7 @@ export const HoverActions: React.FC = React.memo( timelineId, toggleColumn, toggleTopN, - value, + values, }) => { const kibana = useKibana(); const { timelines } = kibana.services; @@ -172,17 +169,21 @@ export const HoverActions: React.FC = React.memo( : SourcererScopeName.default; const { browserFields } = useSourcererScope(activeScope); - const handleStartDragToTimeline = useGetHandleStartDragToTimeline({ draggableId, field }); + const handleStartDragToTimeline = (() => { + const handleStartDragToTimelineFns = draggableIds?.map((draggableId) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + return useGetHandleStartDragToTimeline({ draggableId, field }); + }); + return () => handleStartDragToTimelineFns?.forEach((dragFn) => dragFn()); + })(); - const handleFilterForValue = useCallback( - () => filterForValueFn({ field, value, filterManager, onFilterAdded }), - [filterForValueFn, field, value, filterManager, onFilterAdded] - ); + const handleFilterForValue = useCallback(() => { + filterForValueFn({ field, value: values, filterManager, onFilterAdded }); + }, [filterForValueFn, field, values, filterManager, onFilterAdded]); - const handleFilterOutValue = useCallback( - () => filterOutValueFn({ field, value, filterManager, onFilterAdded }), - [filterOutValueFn, field, value, filterManager, onFilterAdded] - ); + const handleFilterOutValue = useCallback(() => { + filterOutValueFn({ field, value: values, filterManager, onFilterAdded }); + }, [filterOutValueFn, field, values, filterManager, onFilterAdded]); const handleToggleColumn = useCallback( () => (toggleColumn ? columnToggleFn({ toggleColumn, field }) : null), @@ -252,7 +253,6 @@ export const HoverActions: React.FC = React.memo( break; } }, - [ addToTimelineKeyboardShortcut, columnToggleKeyboardShortcut, @@ -268,7 +268,7 @@ export const HoverActions: React.FC = React.memo( ] ); - const showFilters = !showTopN && value != null; + const showFilters = values != null; return ( @@ -287,40 +287,44 @@ export const HoverActions: React.FC = React.memo( {showFilters && ( <> )} {toggleColumn && ( )} - {showFilters && draggableId != null && ( + {showFilters && draggableIds != null && ( )} {allowTopN({ @@ -328,17 +332,25 @@ export const HoverActions: React.FC = React.memo( fieldName: field, }) && ( )} - {!showTopN && ( - + {showFilters && ( + )} diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_ueba.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_ueba.tsx new file mode 100644 index 0000000000000..614ddf698d6b7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_ueba.tsx @@ -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 { UebaTableType } from '../../../ueba/store/model'; +import { UEBA_PATH } from '../../../../common/constants'; +import { appendSearch } from './helpers'; + +export const getUebaUrl = (search?: string) => `${UEBA_PATH}${appendSearch(search)}`; + +export const getTabsOnUebaUrl = (tabName: UebaTableType, search?: string) => + `/${tabName}${appendSearch(search)}`; + +export const getUebaDetailsUrl = (detailName: string, search?: string) => + `/${detailName}${appendSearch(search)}`; + +export const getTabsOnUebaDetailsUrl = ( + detailName: string, + tabName: UebaTableType, + search?: string +) => `/${detailName}/${tabName}${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 0b6b77aab00e4..cc0fdb3923dce 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -42,6 +42,7 @@ import { isUrlInvalid } from '../../utils/validators'; import * as i18n from './translations'; import { SecurityPageName } from '../../../app/types'; +import { getUebaDetailsUrl } from '../link_to/redirect_to_ueba'; export const DEFAULT_NUMBER_OF_LINK = 5; @@ -61,6 +62,45 @@ export const PortContainer = styled.div` `; // Internal Links +const UebaDetailsLinkComponent: React.FC<{ + children?: React.ReactNode; + hostName: string; + isButton?: boolean; +}> = ({ children, hostName, isButton }) => { + const { formatUrl, search } = useFormatUrl(SecurityPageName.ueba); + const { navigateToApp } = useKibana().services.application; + const goToUebaDetails = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.ueba, + path: getUebaDetailsUrl(encodeURIComponent(hostName), search), + }); + }, + [hostName, navigateToApp, search] + ); + + return isButton ? ( + + {children ? children : hostName} + + ) : ( + + {children ? children : hostName} + + ); +}; + +export const UebaDetailsLink = React.memo(UebaDetailsLinkComponent); + const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName: string; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 4ad26533cb58c..aae97d90cb4b8 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -15,6 +15,7 @@ import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/p import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../../cases/pages/utils'; import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../../timelines/pages'; +import { getBreadcrumbs as getUebaBreadcrumbs } from '../../../../ueba/pages/details/utils'; import { getBreadcrumbs as getAdminBreadcrumbs } from '../../../../management/common/breadcrumbs'; import { SecurityPageName } from '../../../../app/types'; import { @@ -23,6 +24,7 @@ import { NetworkRouteSpyState, TimelineRouteSpyState, AdministrationRouteSpyState, + UebaRouteSpyState, } from '../../../utils/route/types'; import { getAppOverviewUrl } from '../../link_to'; @@ -60,6 +62,9 @@ const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpySt const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => spyState != null && spyState.pageName === SecurityPageName.hosts; +const isUebaRoutes = (spyState: RouteSpyState): spyState is UebaRouteSpyState => + spyState != null && spyState.pageName === SecurityPageName.ueba; + const isTimelinesRoutes = (spyState: RouteSpyState): spyState is TimelineRouteSpyState => spyState != null && spyState.pageName === SecurityPageName.timelines; @@ -124,6 +129,25 @@ export const getBreadcrumbsForRoute = ( ), ]; } + if (isUebaRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'ueba', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + + return [ + siemRootBreadcrumb, + ...getUebaBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ), + getUrlForApp + ), + ]; + } if (isRulesRoutes(spyState) && object.navTabs) { const tempNav: SearchNavTab = { urlKey: SecurityPageName.rules, isDetailPage: false }; let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx index 2ca0d878078aa..4d9a8a704dde5 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import deepEqual from 'fast-deep-equal'; -import { useNavigation } from '../../../lib/kibana/hooks'; +import { useNavigation } from '../../../lib/kibana'; import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../lib/telemetry'; import { TabNavigationProps, TabNavigationItemProps } from './types'; @@ -84,7 +84,6 @@ export const TabNavigationComponent: React.FC = ({ () => Object.values(navTabs).map((tab) => { const isSelected = selectedTabId === tab.id; - return ( ; -} export interface TabNavigationComponentProps { pageName: string; tabName: SiemRouteType | undefined; @@ -43,22 +39,30 @@ export interface NavTab { urlKey?: UrlStateType; pageId?: SecurityPageName; } + export type SecurityNavKey = - | SecurityPageName.overview + | SecurityPageName.administration + | SecurityPageName.alerts + | SecurityPageName.case + | SecurityPageName.endpoints + | SecurityPageName.eventFilters + | SecurityPageName.exceptions | SecurityPageName.hosts | SecurityPageName.network - | SecurityPageName.alerts + | SecurityPageName.overview | SecurityPageName.rules - | SecurityPageName.exceptions | SecurityPageName.timelines - | SecurityPageName.case - | SecurityPageName.administration - | SecurityPageName.endpoints | SecurityPageName.trustedApps - | SecurityPageName.eventFilters; + | SecurityPageName.ueba; export type SecurityNav = Record; +export type GenericNavRecord = Record; + +export interface SecuritySolutionTabNavigationProps { + display?: 'default' | 'condensed'; + navTabs: GenericNavRecord; +} export type GetUrlForApp = ( appId: string, options?: { deepLinkId?: string; path?: string; absolute?: boolean } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index af88aacb7602a..4bd5a43684792 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -16,10 +16,12 @@ import { TimelineTabs } from '../../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { UrlInputsModel } from '../../../store/inputs/model'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; +import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; jest.mock('../../../lib/kibana/kibana_react'); jest.mock('../../../lib/kibana'); jest.mock('../../../hooks/use_selector'); +jest.mock('../../../hooks/use_experimental_features'); jest.mock('../../../utils/route/use_route_spy'); describe('useSecuritySolutionNavigation', () => { @@ -70,6 +72,7 @@ describe('useSecuritySolutionNavigation', () => { ]; beforeEach(() => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); (useDeepEqualSelector as jest.Mock).mockReturnValue({ urlState: mockUrlState }); (useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy); (useKibana as jest.Mock).mockReturnValue({ @@ -231,6 +234,17 @@ describe('useSecuritySolutionNavigation', () => { `); }); + // TODO: Steph/ueba remove when no longer experimental + it('should include ueba when feature flag is on', async () => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(() => + useSecuritySolutionNavigation() + ); + + // @ts-ignore possibly undefined, but if undefined we want this test to fail + expect(result.current.items[2].items[2].id).toEqual(SecurityPageName.ueba); + }); + describe('Permission gated routes', () => { describe('cases', () => { it('should display the cases navigation item when the user has read permissions', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx index 39c6885e8dff5..5165a903bbde1 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx @@ -13,6 +13,8 @@ import { makeMapStateToProps } from '../../url_state/helpers'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; import { navTabs } from '../../../../app/home/home_navigations'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; +import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; +import { GenericNavRecord } from '../types'; /** * @description - This hook provides the structure necessary by the KibanaPageTemplate for rendering the primary security_solution side navigation. @@ -29,6 +31,12 @@ export const useSecuritySolutionNavigation = () => { const { detailName, flowTarget, pageName, pathName, search, state, tabName } = routeProps; + const uebaEnabled = useIsExperimentalFeatureEnabled('uebaEnabled'); + let enabledNavTabs: GenericNavRecord = (navTabs as unknown) as GenericNavRecord; + if (!uebaEnabled) { + const { ueba, ...rest } = enabledNavTabs; + enabledNavTabs = rest; + } useEffect(() => { if (pathName || pageName) { setBreadcrumbs( @@ -36,7 +44,7 @@ export const useSecuritySolutionNavigation = () => { detailName, filters: urlState.filters, flowTarget, - navTabs, + navTabs: enabledNavTabs, pageName, pathName, query: urlState.query, @@ -65,12 +73,13 @@ export const useSecuritySolutionNavigation = () => { tabName, getUrlForApp, navigateToUrl, + enabledNavTabs, ]); return usePrimaryNavigation({ query: urlState.query, filters: urlState.filters, - navTabs, + navTabs: enabledNavTabs, pageName, sourcerer: urlState.sourcerer, savedQuery: urlState.savedQuery, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index fffe59fceff41..feeeacf6124e8 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -20,7 +20,6 @@ export const usePrimaryNavigationItems = ({ ...urlStateProps }: PrimaryNavigationItemsProps): Array> => { const { navigateTo, getAppUrl } = useNavigation(); - const getSideNav = useCallback( (tab: NavTab) => { const { id, name, disabled } = tab; @@ -62,7 +61,6 @@ export const usePrimaryNavigationItems = ({ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { const hasCasesReadPermissions = useGetUserCasesPermissions()?.read; - return useMemo( () => [ { @@ -76,7 +74,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { }, { ...securityNavGroup.explore, - items: [navTabs.hosts, navTabs.network], + items: [navTabs.hosts, navTabs.network, ...(navTabs.ueba != null ? [navTabs.ueba] : [])], }, { ...securityNavGroup.investigate, diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx index 3d0be80e3d58c..f5828c9f65db9 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx @@ -46,6 +46,9 @@ import { useStateToaster } from '../toasters'; import * as i18n from './translations'; import { Panel } from '../panel'; import { InspectButtonContainer } from '../inspect'; +import { RiskScoreColumns } from '../../../ueba/components/risk_score_table'; +import { HostRulesColumns } from '../../../ueba/components/host_rules_table'; +import { HostTacticsColumns } from '../../../ueba/components/host_tactics_table'; const DEFAULT_DATA_TEST_SUBJ = 'paginated-table'; @@ -74,6 +77,8 @@ declare type HostsTableColumnsTest = [ declare type BasicTableColumns = | AuthTableColumns + | HostRulesColumns + | HostTacticsColumns | HostsTableColumns | HostsTableColumnsTest | NetworkDnsColumns @@ -82,6 +87,8 @@ declare type BasicTableColumns = | NetworkTopCountriesColumnsNetworkDetails | NetworkTopNFlowColumns | NetworkTopNFlowColumnsNetworkDetails + | NetworkHttpColumns + | RiskScoreColumns | TlsColumns | UncommonProcessTableColumns | UsersColumns; @@ -97,7 +104,8 @@ export interface BasicTableProps { headerSupplement?: React.ReactElement; headerTitle: string | React.ReactElement; headerTooltip?: string; - headerUnit: string | React.ReactElement; + headerUnit?: string | React.ReactElement; + headerSubtitle?: string | React.ReactElement; id?: string; itemsPerRow?: ItemsPerRow[]; isInspect?: boolean; @@ -136,6 +144,7 @@ const PaginatedTableComponent: FC = ({ headerTitle, headerTooltip, headerUnit, + headerSubtitle, id, isInspect, itemsPerRow, @@ -248,8 +257,12 @@ const PaginatedTableComponent: FC = ({ = 0 ? headerCount.toLocaleString() : 0} ${headerUnit}` + !loadingInitial && headerSubtitle + ? `${i18n.SHOWING}: ${headerSubtitle}` + : headerUnit && + `${i18n.SHOWING}: ${ + headerCount >= 0 ? headerCount.toLocaleString() : 0 + } ${headerUnit}` } title={headerTitle} tooltip={headerTooltip} diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx index 49cfd841b7f8a..49bd7824d6100 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx @@ -9,8 +9,8 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; +import { FieldComponent } from '@kbn/securitysolution-autocomplete'; import { IFieldType, IndexPattern } from '../../../../../../../src/plugins/data/common'; -import { FieldComponent } from '../autocomplete/field'; import { FormattedEntry, Entry } from './types'; import * as i18n from './translations'; import { getEntryOnFieldChange, getEntryOnThreatFieldChange } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts index 6107b61638888..edf09a52006fd 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts @@ -26,12 +26,13 @@ export enum CONSTANTS { } export type UrlStateType = - | 'case' + | 'administration' | 'alerts' - | 'rules' + | 'case' | 'exceptions' | 'host' | 'network' | 'overview' + | 'rules' | 'timeline' - | 'administration'; + | 'ueba'; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts index 63511c54d28db..e6f79d3d24ae0 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts @@ -19,7 +19,7 @@ import { UrlInputsModel } from '../../store/inputs/model'; import { TimelineUrl } from '../../../timelines/store/timeline/model'; import { RouteSpyState } from '../../utils/route/types'; import { DispatchUpdateTimeline } from '../../../timelines/components/open_timeline/types'; -import { NavTab } from '../navigation/types'; +import { SecurityNav } from '../navigation/types'; import { CONSTANTS, UrlStateType } from './constants'; import { SourcererScopePatterns } from '../../store/sourcerer/model'; @@ -66,6 +66,14 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.timerange, CONSTANTS.timeline, ], + ueba: [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.sourcerer, + CONSTANTS.timerange, + CONSTANTS.timeline, + ], administration: [], network: [ CONSTANTS.appQuery, @@ -124,7 +132,7 @@ export interface UrlState { export type KeyUrlState = keyof UrlState; export interface UrlStateProps { - navTabs: Record; + navTabs: SecurityNav; indexPattern?: IIndexPattern; mapToUrlState?: (value: string) => UrlState; onChange?: (urlState: UrlState, previousUrlState: UrlState) => void; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index 002c40fc9d428..d804f350a7f79 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -14,8 +14,8 @@ import { SourcererScopeName } from '../../store/sourcerer/model'; import { useIndexFields } from '../source'; import { useUserInfo } from '../../../detections/components/user_info'; import { timelineSelectors } from '../../../timelines/store/timeline'; -import { ALERTS_PATH, RULES_PATH } from '../../../../common/constants'; -import { TimelineId } from '../../../../common/types/timeline'; +import { ALERTS_PATH, RULES_PATH, UEBA_PATH } from '../../../../common/constants'; +import { TimelineId } from '../../../../common'; import { useDeepEqualSelector } from '../../hooks/use_selector'; export const useInitSourcerer = ( @@ -57,8 +57,7 @@ export const useInitSourcerer = ( !loadingSignalIndex && signalIndexName != null && signalIndexNameSelector == null && - (activeTimeline == null || - (activeTimeline != null && activeTimeline.savedObjectId == null)) && + (activeTimeline == null || activeTimeline.savedObjectId == null) && initialTimelineSourcerer.current ) { initialTimelineSourcerer.current = false; @@ -70,8 +69,7 @@ export const useInitSourcerer = ( ); } else if ( signalIndexNameSelector != null && - (activeTimeline == null || - (activeTimeline != null && activeTimeline.savedObjectId == null)) && + (activeTimeline == null || activeTimeline.savedObjectId == null) && initialTimelineSourcerer.current ) { initialTimelineSourcerer.current = false; @@ -124,15 +122,14 @@ export const useInitSourcerer = ( export const useSourcererScope = (scope: SourcererScopeName = SourcererScopeName.default) => { const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); - const SourcererScope = useDeepEqualSelector((state) => sourcererScopeSelector(state, scope)); - return SourcererScope; + return useDeepEqualSelector((state) => sourcererScopeSelector(state, scope)); }; export const getScopeFromPath = ( pathname: string ): SourcererScopeName.default | SourcererScopeName.detections => { return matchPath(pathname, { - path: [ALERTS_PATH, `${RULES_PATH}/id/:id`], + path: [ALERTS_PATH, `${RULES_PATH}/id/:id`, `${UEBA_PATH}/:id`], strict: false, }) == null ? SourcererScopeName.default diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts index 247b7624914cf..9a6b8c54f2bc6 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts @@ -14,8 +14,8 @@ import { const allowedExperimentalValues = getExperimentalAllowedValues(); -export const useIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => { - return useSelector(({ app: { enableExperimental } }: State) => { +export const useIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => + useSelector(({ app: { enableExperimental } }: State) => { if (!enableExperimental || !(feature in enableExperimental)) { throw new Error( `Invalid enable value ${feature}. Allowed values are: ${allowedExperimentalValues.join( @@ -25,4 +25,3 @@ export const useIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatu } return enableExperimental[feature]; }); -}; diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index 44a100e27e95b..f8a77d97b8700 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -172,7 +172,7 @@ const createCoreStartMock = ( ): ReturnType => { const coreStart = coreMock.createStart({ basePath: '/mock' }); - const deepLinkPaths = getDeepLinkPaths(getDeepLinks()); + const deepLinkPaths = getDeepLinkPaths(getDeepLinks(mockGlobalState.app.enableExperimental)); // Mock the certain APP Ids returned by `application.getUrlForApp()` coreStart.application.getUrlForApp.mockImplementation((appId, { deepLinkId, path } = {}) => { diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/http_handler_mock_factory.ts b/x-pack/plugins/security_solution/public/common/mock/endpoint/http_handler_mock_factory.ts index c064b1808734d..12a7de96ba0d1 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/http_handler_mock_factory.ts +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/http_handler_mock_factory.ts @@ -25,7 +25,7 @@ export type ResponseProviderCallback any = (...args: * * @example * type FleetSetupResponseProvidersMock = ResponseProvidersInterface<{ - * fleetSetup: () => PostIngestSetupResponse; + * fleetSetup: () => PostFleetSetupResponse; * }>; */ export type ResponseProvidersInterface< diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index ffbfd1a5123ad..8130a7058700d 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -13,6 +13,9 @@ import { NetworkTopTablesFields, NetworkTlsFields, NetworkUsersFields, + RiskScoreFields, + HostRulesFields, + HostTacticsFields, } from '../../../common/search_strategy'; import { State } from '../store'; @@ -25,12 +28,14 @@ import { DEFAULT_INDEX_PATTERN, } from '../../../common/constants'; import { networkModel } from '../../network/store'; +import { uebaModel } from '../../ueba/store'; import { TimelineType, TimelineStatus, TimelineTabs } from '../../../common/types/timeline'; import { mockManagementState } from '../../management/store/reducer'; import { ManagementState } from '../../management/types'; import { initialSourcererState, SourcererScopeName } from '../store/sourcerer/model'; import { mockBrowserFields, mockDocValueFields } from '../containers/source/mock'; import { mockIndexPattern } from './index_pattern'; +import { allowedExperimentalValues } from '../../../common/experimental_features'; export const mockGlobalState: State = { app: { @@ -39,12 +44,7 @@ export const mockGlobalState: State = { { id: 'error-id-1', title: 'title-1', message: ['error-message-1'] }, { id: 'error-id-2', title: 'title-2', message: ['error-message-2'] }, ], - enableExperimental: { - trustedAppsByPolicyEnabled: false, - metricsEntitiesEnabled: false, - ruleRegistryEnabled: false, - tGridEnabled: false, - }, + enableExperimental: allowedExperimentalValues, }, hosts: { page: { @@ -164,6 +164,36 @@ export const mockGlobalState: State = { }, }, }, + ueba: { + page: { + queries: { + [uebaModel.UebaTableType.riskScore]: { + activePage: 0, + limit: 10, + sort: { field: RiskScoreFields.riskScore, direction: Direction.desc }, + }, + }, + }, + details: { + queries: { + [uebaModel.UebaTableType.hostRules]: { + activePage: 0, + limit: 10, + sort: { field: HostRulesFields.riskScore, direction: Direction.desc }, + }, + [uebaModel.UebaTableType.hostTactics]: { + activePage: 0, + limit: 10, + sort: { field: HostTacticsFields.riskScore, direction: Direction.desc }, + }, + [uebaModel.UebaTableType.userRules]: { + activePage: 0, + limit: 10, + sort: { field: HostRulesFields.riskScore, direction: Direction.desc }, + }, + }, + }, + }, inputs: { global: { timerange: { diff --git a/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts b/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts index 224d38c8462f2..d19d6ee734654 100644 --- a/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts +++ b/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; +import { IIndexPattern } from '../../../../../../src/plugins/data/common'; export const mockIndexPattern: IIndexPattern = { fields: [ diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 806031b07e0c9..f271f49e56a29 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FilterStateStore } from '../../../../../../src/plugins/data/common/es_query/filters/meta_filter'; +import { FilterStateStore } from '../../../../../../src/plugins/data/common'; import { TimelineId, diff --git a/x-pack/plugins/security_solution/public/common/mock/utils.ts b/x-pack/plugins/security_solution/public/common/mock/utils.ts index e0f8e651a5821..0d9e2f4f367ec 100644 --- a/x-pack/plugins/security_solution/public/common/mock/utils.ts +++ b/x-pack/plugins/security_solution/public/common/mock/utils.ts @@ -12,6 +12,7 @@ import { tGridReducer } from '../../../../timelines/public'; import { hostsReducer } from '../../hosts/store'; import { networkReducer } from '../../network/store'; +import { uebaReducer } from '../../ueba/store'; import { timelineReducer } from '../../timelines/store/timeline/reducer'; import { managementReducer } from '../../management/store/reducer'; import { ManagementPluginReducer } from '../../management'; @@ -52,6 +53,7 @@ const combineTimelineReducer = reduceReducers( export const SUB_PLUGINS_REDUCER: SubPluginsInitReducer = { hosts: hostsReducer, network: networkReducer, + ueba: uebaReducer, timeline: combineTimelineReducer, /** * These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture, diff --git a/x-pack/plugins/security_solution/public/common/store/app/model.ts b/x-pack/plugins/security_solution/public/common/store/app/model.ts index 2888867167c14..2c4ddb703f6a0 100644 --- a/x-pack/plugins/security_solution/public/common/store/app/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/app/model.ts @@ -27,5 +27,5 @@ export type ErrorModel = Error[]; export interface AppModel { notesById: NotesById; errors: ErrorState; - enableExperimental?: ExperimentalFeatures; + enableExperimental: ExperimentalFeatures; } diff --git a/x-pack/plugins/security_solution/public/common/store/app/reducer.ts b/x-pack/plugins/security_solution/public/common/store/app/reducer.ts index 20c9b0e14dbd9..5b0a2330a408d 100644 --- a/x-pack/plugins/security_solution/public/common/store/app/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/app/reducer.ts @@ -17,6 +17,13 @@ export type AppState = AppModel; export const initialAppState: AppState = { notesById: {}, errors: [], + enableExperimental: { + trustedAppsByPolicyEnabled: false, + metricsEntitiesEnabled: false, + ruleRegistryEnabled: false, + tGridEnabled: false, + uebaEnabled: false, + }, }; interface UpdateNotesByIdParams { diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.ts b/x-pack/plugins/security_solution/public/common/store/reducer.ts index c2ef2563fe63e..d5633ee84d6d4 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.ts @@ -14,6 +14,7 @@ import { sourcererReducer, sourcererModel } from './sourcerer'; import { HostsPluginReducer } from '../../hosts/store'; import { NetworkPluginReducer } from '../../network/store'; +import { UebaPluginReducer } from '../../ueba/store'; import { TimelinePluginReducer } from '../../timelines/store/timeline'; import { SecuritySubPlugins } from '../../app/types'; @@ -24,6 +25,7 @@ import { KibanaIndexPatterns } from './sourcerer/model'; import { ExperimentalFeatures } from '../../../common/experimental_features'; export type SubPluginsInitReducer = HostsPluginReducer & + UebaPluginReducer & NetworkPluginReducer & TimelinePluginReducer & ManagementPluginReducer; diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts index fa1eec9ee0e82..26497c7f6ee3b 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { DocValueFields } from '../../../../common/search_strategy/common'; import { BrowserFields, diff --git a/x-pack/plugins/security_solution/public/common/store/types.ts b/x-pack/plugins/security_solution/public/common/store/types.ts index 21e833abe1f9b..6943b4cf73117 100644 --- a/x-pack/plugins/security_solution/public/common/store/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/types.ts @@ -18,10 +18,12 @@ import { HostsPluginState } from '../../hosts/store'; import { DragAndDropState } from './drag_and_drop/reducer'; import { TimelinePluginState } from '../../timelines/store/timeline'; import { NetworkPluginState } from '../../network/store'; +import { UebaPluginState } from '../../ueba/store'; import { ManagementPluginState } from '../../management'; export type StoreState = HostsPluginState & NetworkPluginState & + UebaPluginState & TimelinePluginState & ManagementPluginState & { app: AppState; diff --git a/x-pack/plugins/security_solution/public/common/utils/route/types.ts b/x-pack/plugins/security_solution/public/common/utils/route/types.ts index 189e68d1c55bb..c6d5852881850 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/types.ts +++ b/x-pack/plugins/security_solution/public/common/utils/route/types.ts @@ -15,8 +15,14 @@ import { HostsTableType } from '../../../hosts/store/model'; import { NetworkRouteType } from '../../../network/pages/navigation/types'; import { AdministrationSubTab as AdministrationType } from '../../../management/types'; import { FlowTarget } from '../../../../common/search_strategy'; +import { UebaTableType } from '../../../ueba/store/model'; -export type SiemRouteType = HostsTableType | NetworkRouteType | TimelineType | AdministrationType; +export type SiemRouteType = + | HostsTableType + | NetworkRouteType + | TimelineType + | AdministrationType + | UebaTableType; export interface RouteSpyState { pageName: string; detailName: string | undefined; @@ -32,6 +38,9 @@ export interface HostRouteSpyState extends RouteSpyState { tabName: HostsTableType | undefined; } +export interface UebaRouteSpyState extends RouteSpyState { + tabName: UebaTableType | undefined; +} export interface NetworkRouteSpyState extends RouteSpyState { tabName: NetworkRouteType | undefined; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 601e0509009ce..245aa67d677be 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -12,7 +12,7 @@ import { getOr, isEmpty } from 'lodash/fp'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import type { Filter } from '../../../../../../../src/plugins/data/common/es_query/filters'; +import { FilterStateStore, Filter } from '@kbn/es-query'; import { KueryFilterQueryKind, TimelineId, @@ -49,7 +49,6 @@ import { DataProvider, QueryOperator, } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { esFilters } from '../../../../../../../src/plugins/data/public'; import { getTimelineTemplate } from '../../../timelines/containers/api'; export const getUpdateAlertsQuery = (eventIds: Readonly) => { @@ -283,7 +282,7 @@ export const buildAlertsKqlFilter = ( params: alertIds, }, $state: { - store: esFilters.FilterStateStore.APP_STATE, + store: FilterStateStore.APP_STATE, }, }, ]; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 9f59e3763ffbc..b1881d29ec10d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -23,7 +23,10 @@ import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { TimelineId } from '../../../../../common/types/timeline'; -import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; +import { + DEFAULT_INDEX_PATTERN, + DEFAULT_INDEX_PATTERN_EXPERIMENTAL, +} from '../../../../../common/constants'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { timelineActions } from '../../../../timelines/store/timeline'; import { EventsTdContent } from '../../../../timelines/components/timeline/styles'; @@ -49,6 +52,7 @@ import { AlertData, EcsHit } from '../../../../common/components/exceptions/type import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; import { EventFiltersModal } from '../../../../management/pages/event_filters/view/components/modal'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; interface AlertContextMenuProps { ariaLabel?: string; @@ -84,6 +88,8 @@ const AlertContextMenuComponent: React.FC = ({ [ecsRowData] ); + // TODO: Steph/ueba remove when past experimental + const uebaEnabled = useIsExperimentalFeatureEnabled('uebaEnabled'); const isEvent = useMemo(() => indexOf(ecsRowData.event?.kind, 'event') !== -1, [ecsRowData]); const ruleIndices = useMemo((): string[] => { if ( @@ -93,9 +99,11 @@ const AlertContextMenuComponent: React.FC = ({ ) { return ecsRowData.signal.rule.index; } else { - return DEFAULT_INDEX_PATTERN; + return uebaEnabled + ? [...DEFAULT_INDEX_PATTERN, ...DEFAULT_INDEX_PATTERN_EXPERIMENTAL] + : DEFAULT_INDEX_PATTERN; } - }, [ecsRowData]); + }, [ecsRowData.signal?.rule, uebaEnabled]); const { addWarning } = useAppToasts(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx index 503a568f13744..16caed9086e61 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx @@ -7,10 +7,10 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFormRow } from '@elastic/eui'; +import { FieldComponent } from '@kbn/securitysolution-autocomplete'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { FieldComponent } from '../../../../common/components/autocomplete/field'; import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; -import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; interface AutocompleteFieldProps { dataTestSubj: string; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index c02f7992a9b92..eef18a502c270 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -20,10 +20,10 @@ import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { noop } from 'lodash/fp'; import { RiskScoreMapping } from '@kbn/securitysolution-io-ts-alerting-types'; +import { FieldComponent } from '@kbn/securitysolution-autocomplete'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { AboutStepRiskScore } from '../../../pages/detection_engine/rules/types'; -import { FieldComponent } from '../../../../common/components/autocomplete/field'; import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx index 8b8c9441e7eae..d4fbdc31fbcae 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx @@ -24,6 +24,11 @@ import { SeverityMapping, SeverityMappingItem, } from '@kbn/securitysolution-io-ts-alerting-types'; +import { + FieldComponent, + AutocompleteFieldMatchComponent, +} from '@kbn/securitysolution-autocomplete'; + import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { SeverityOptionItem } from '../step_about_rule/data'; @@ -32,8 +37,7 @@ import { IFieldType, IIndexPattern, } from '../../../../../../../../src/plugins/data/common/index_patterns'; -import { FieldComponent } from '../../../../common/components/autocomplete/field'; -import { AutocompleteFieldMatchComponent } from '../../../../common/components/autocomplete/field_value_match'; +import { useKibana } from '../../../../common/lib/kibana'; const NestedContent = styled.div` margin-left: 24px; @@ -68,6 +72,7 @@ export const SeverityField = ({ isDisabled, options, }: SeverityFieldProps) => { + const { services } = useKibana(); const { value, isMappingChecked, mapping } = field.value; const { setValue } = field; @@ -254,6 +259,7 @@ export const SeverityField = ({ { // TODO: Once we are past experimental phase this code should be removed const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); + // TODO: Steph/ueba remove when past experimental + const uebaEnabled = useIsExperimentalFeatureEnabled('uebaEnabled'); + // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); const { @@ -348,7 +355,14 @@ const RuleDetailsPageComponent = () => { ), [ruleDetailTab, setRuleDetailTab] ); - + const ruleIndices = useMemo( + () => + rule?.index ?? + (uebaEnabled + ? [...DEFAULT_INDEX_PATTERN, ...DEFAULT_INDEX_PATTERN_EXPERIMENTAL] + : DEFAULT_INDEX_PATTERN), + [rule?.index, uebaEnabled] + ); const handleRefresh = useCallback(() => { if (fetchRuleStatus != null && ruleId != null) { fetchRuleStatus(ruleId); @@ -732,7 +746,7 @@ const RuleDetailsPageComponent = () => { ( export const isDetectionsPath = (pathname: string): boolean => { return !!matchPath(pathname, { - path: `(${ALERTS_PATH}|${RULES_PATH}|${EXCEPTIONS_PATH})`, + path: `(${ALERTS_PATH}|${RULES_PATH}|${UEBA_PATH}|${EXCEPTIONS_PATH})`, strict: false, }); }; diff --git a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx index 47026cbec49ad..430c77b9422d8 100644 --- a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx +++ b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx @@ -16,6 +16,7 @@ import { Exceptions } from './exceptions'; import { Hosts } from './hosts'; import { Network } from './network'; +import { Ueba } from './ueba'; import { Overview } from './overview'; import { Rules } from './rules'; @@ -31,6 +32,7 @@ const subPluginClasses = { Exceptions, Hosts, Network, + Ueba, Overview, Rules, Timelines, 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 d44ce7a136fdf..b974dfebd4eb1 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 @@ -115,7 +115,6 @@ describe('When on the Event Filters List Page', () => { expect(eventMeta).toEqual([ 'some name', - 'Linux', 'April 20th 2020 @ 11:25:31', 'some user', 'April 20th 2020 @ 11:25:31', diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 137fef1641501..ee5ca84c6e13f 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -58,16 +58,21 @@ import { SecuritySolutionUiConfigType } from './common/types'; import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension'; -import { parseExperimentalConfigValue } from '../common/experimental_features'; +import { + ExperimentalFeatures, + parseExperimentalConfigValue, +} from '../common/experimental_features'; import type { TimelineState } from '../../timelines/public'; import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension'; export class Plugin implements IPlugin { - private kibanaVersion: string; + readonly kibanaVersion: string; private config: SecuritySolutionUiConfigType; + readonly experimentalFeatures: ExperimentalFeatures; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); + this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental || []); this.kibanaVersion = initializerContext.env.packageInfo.version; } private appUpdater$ = new Subject(); @@ -151,7 +156,7 @@ export class Plugin implements IPlugin { const [coreStart, startPlugins] = await core.getStartServices(); const subPlugins = await this.startSubPlugins(this.storage, coreStart, startPlugins); @@ -231,7 +236,11 @@ export class Plugin implements IPlugin ({ navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks(currentLicense.type, core.application.capabilities), + deepLinks: getDeepLinks( + this.experimentalFeatures, + currentLicense.type, + core.application.capabilities + ), })); } }); @@ -239,6 +248,7 @@ export class Plugin implements IPlugin { if (!this._store) { - const experimentalFeatures = parseExperimentalConfigValue( - this.config.enableExperimental || [] - ); const defaultIndicesName = coreStart.uiSettings.get(DEFAULT_INDEX_KEY); const [ { createStore, createInitialState }, @@ -359,7 +370,7 @@ export class Plugin implements IPlugin>; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx index 7c38474d39dba..2c85f1547dbeb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx @@ -8,6 +8,7 @@ import { isEmpty, get } from 'lodash/fp'; import memoizeOne from 'memoize-one'; +import { EsQueryConfig, Filter, Query } from '@kbn/es-query'; import { handleSkipFocus, elementOrChildrenHasFocus, @@ -24,12 +25,7 @@ import { EXISTS_OPERATOR, } from './data_providers/data_provider'; import { BrowserFields } from '../../../common/containers/source'; -import { - IIndexPattern, - Query, - EsQueryConfig, - Filter, -} from '../../../../../../../src/plugins/data/public'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { EVENTS_TABLE_CLASS_NAME } from './styles'; diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index f9bea764c5987..326a6973db53b 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -37,6 +37,7 @@ import { Overview } from './overview'; import { Rules } from './rules'; import { Timelines } from './timelines'; import { Management } from './management'; +import { Ueba } from './ueba'; import { LicensingPluginStart, LicensingPluginSetup } from '../../licensing/public'; import { DashboardStart } from '../../../../src/plugins/dashboard/public'; @@ -91,6 +92,8 @@ export interface SubPlugins { cases: Cases; hosts: Hosts; network: Network; + // TODO: Steph/ueba require ueba once no longer experimental + ueba?: Ueba; overview: Overview; timelines: Timelines; management: Management; @@ -104,6 +107,8 @@ export interface StartedSubPlugins { cases: ReturnType; hosts: ReturnType; network: ReturnType; + // TODO: Steph/ueba require ueba once no longer experimental + ueba?: ReturnType; overview: ReturnType; timelines: ReturnType; management: ReturnType; diff --git a/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/columns.tsx b/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/columns.tsx new file mode 100644 index 0000000000000..4289b7d2c62da --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/columns.tsx @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + DragEffects, + DraggableWrapper, +} from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; +import { HostRulesColumns } from './'; + +import * as i18n from './translations'; +import { HostRulesFields } from '../../../../common'; + +export const getHostRulesColumns = (): HostRulesColumns => [ + { + field: `node.${HostRulesFields.ruleName}`, + name: i18n.NAME, + truncateText: false, + hideForMobile: false, + render: (ruleName) => { + if (ruleName != null && ruleName.length > 0) { + const id = escapeDataProviderId(`ueba-table-ruleName-${ruleName}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + ruleName + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: `node.${HostRulesFields.ruleType}`, + name: i18n.RULE_TYPE, + truncateText: false, + hideForMobile: false, + render: (ruleType) => { + if (ruleType != null && ruleType.length > 0) { + const id = escapeDataProviderId(`ueba-table-ruleType-${ruleType}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + ruleType + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: `node.${HostRulesFields.riskScore}`, + name: i18n.RISK_SCORE, + truncateText: false, + hideForMobile: false, + render: (riskScore) => { + if (riskScore != null) { + const id = escapeDataProviderId(`ueba-table-riskScore-${riskScore}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + riskScore + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: `node.${HostRulesFields.hits}`, + name: i18n.HITS, + truncateText: false, + hideForMobile: false, + sortable: false, + render: (hits) => { + if (hits != null) { + return hits; + } + return getEmptyTagValue(); + }, + }, +]; diff --git a/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/index.tsx b/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/index.tsx new file mode 100644 index 0000000000000..3d369a56a7bc0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/index.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; + +import { + Columns, + Criteria, + PaginatedTable, + SortingBasicTable, +} from '../../../common/components/paginated_table'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaActions, uebaModel, uebaSelectors } from '../../store'; +import { getHostRulesColumns } from './columns'; +import * as i18n from './translations'; +import { + HostRulesEdges, + HostRulesItem, + HostRulesSortField, + HostRulesFields, +} from '../../../../common'; +import { Direction } from '../../../../common/search_strategy'; +import { HOST_RULES } from '../../pages/translations'; +import { rowItems } from '../utils'; + +interface HostRulesTableProps { + data: HostRulesEdges[]; + fakeTotalCount: number; + headerTitle?: string; + headerSupplement?: React.ReactElement; + id: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: uebaModel.UebaType; + tableType: uebaModel.UebaTableType.hostRules | uebaModel.UebaTableType.userRules; +} + +export type HostRulesColumns = [ + Columns, + Columns, + Columns, + Columns +]; + +const getSorting = (sortField: HostRulesFields, direction: Direction): SortingBasicTable => ({ + field: getNodeField(sortField), + direction, +}); + +const HostRulesTableComponent: React.FC = ({ + data, + fakeTotalCount, + headerTitle, + headerSupplement, + id, + isInspect, + loading, + loadPage, + showMorePagesIndicator, + tableType, + totalCount, + type, +}) => { + const dispatch = useDispatch(); + const { activePage, limit, sort } = useDeepEqualSelector(uebaSelectors.hostRulesSelector()); + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + uebaActions.updateTableLimit({ + uebaType: type, + limit: newLimit, + tableType, + }) + ), + [tableType, type, dispatch] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + uebaActions.updateTableActivePage({ + activePage: newPage, + uebaType: type, + tableType, // this will need to become unique for each user table in the group + }) + ), + [tableType, type, dispatch] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const newSort: HostRulesSortField = { + field: getSortField(criteria.sort.field), + direction: criteria.sort.direction as Direction, + }; + if (newSort.direction !== sort.direction || newSort.field !== sort.field) { + // dispatch( + // uebaActions.updateHostRulesSort({ + // sort, + // uebaType: type, + // }) + // ); TODO: Steph/ueba implement sorting + } + } + }, + [sort] + ); + + const columns = useMemo(() => getHostRulesColumns(), []); + + const sorting = useMemo(() => getSorting(sort.field, sort.direction), [sort]); + const headerProps = useMemo( + () => + tableType === uebaModel.UebaTableType.userRules && headerTitle && headerSupplement + ? { + headerTitle, + headerSupplement, + } + : { headerTitle: HOST_RULES }, + [headerSupplement, headerTitle, tableType] + ); + return ( + + ); +}; + +HostRulesTableComponent.displayName = 'HostRulesTableComponent'; + +const getSortField = (field: string): HostRulesFields => { + switch (field) { + case `node.${HostRulesFields.ruleName}`: + return HostRulesFields.ruleName; + case `node.${HostRulesFields.riskScore}`: + return HostRulesFields.riskScore; + default: + return HostRulesFields.riskScore; + } +}; + +const getNodeField = (field: HostRulesFields): string => `node.${field}`; + +export const HostRulesTable = React.memo(HostRulesTableComponent); + +HostRulesTable.displayName = 'HostRulesTable'; diff --git a/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/translations.ts b/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/translations.ts new file mode 100644 index 0000000000000..f029910b9714b --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/translations.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.securitySolution.uebaTableHostRules.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {rule} other {rules}}`, + }); + +export const NAME = i18n.translate('xpack.securitySolution.uebaTableHostRules.ruleName', { + defaultMessage: 'Rule name', +}); + +export const RISK_SCORE = i18n.translate( + 'xpack.securitySolution.uebaTableHostRules.totalRiskScore', + { + defaultMessage: 'Total risk score', + } +); + +export const RULE_TYPE = i18n.translate('xpack.securitySolution.uebaTableHostRules.ruleType', { + defaultMessage: 'Rule type', +}); + +export const HITS = i18n.translate('xpack.securitySolution.uebaTableHostRules.hits', { + defaultMessage: 'Number of hits', +}); diff --git a/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/columns.tsx b/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/columns.tsx new file mode 100644 index 0000000000000..19516ad6fcafa --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/columns.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + DragEffects, + DraggableWrapper, +} from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; +import { HostTacticsColumns } from './'; + +import * as i18n from './translations'; +import { HostTacticsFields } from '../../../../common'; + +export const getHostTacticsColumns = (): HostTacticsColumns => [ + { + field: `node.${HostTacticsFields.tactic}`, + name: i18n.TACTIC, + truncateText: false, + hideForMobile: false, + render: (tactic) => { + if (tactic != null && tactic.length > 0) { + const id = escapeDataProviderId(`ueba-table-tactic-${tactic}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + tactic + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: `node.${HostTacticsFields.technique}`, + name: i18n.TECHNIQUE, + truncateText: false, + hideForMobile: false, + render: (technique) => { + if (technique != null && technique.length > 0) { + const id = escapeDataProviderId(`ueba-table-technique-${technique}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + technique + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: `node.${HostTacticsFields.riskScore}`, + name: i18n.RISK_SCORE, + truncateText: false, + hideForMobile: false, + render: (riskScore) => { + if (riskScore != null) { + const id = escapeDataProviderId(`ueba-table-riskScore-${riskScore}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + riskScore + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: `node.${HostTacticsFields.hits}`, + name: i18n.HITS, + truncateText: false, + hideForMobile: false, + sortable: false, + render: (hits) => { + if (hits != null) { + return hits; + } + return getEmptyTagValue(); + }, + }, +]; diff --git a/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/index.tsx b/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/index.tsx new file mode 100644 index 0000000000000..28bd3d6ad43a0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/index.tsx @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { useMemo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; + +import { + Columns, + Criteria, + PaginatedTable, + SortingBasicTable, +} from '../../../common/components/paginated_table'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaActions, uebaModel, uebaSelectors } from '../../store'; +import { getHostTacticsColumns } from './columns'; +import * as i18n from './translations'; +import { + HostTacticsEdges, + HostTacticsItem, + HostTacticsSortField, + HostTacticsFields, +} from '../../../../common'; +import { Direction } from '../../../../common/search_strategy'; +import { HOST_TACTICS } from '../../pages/translations'; +import { rowItems } from '../utils'; + +const tableType = uebaModel.UebaTableType.hostTactics; + +interface HostTacticsTableProps { + data: HostTacticsEdges[]; + fakeTotalCount: number; + id: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + techniqueCount: number; + totalCount: number; + type: uebaModel.UebaType; +} + +export type HostTacticsColumns = [ + Columns, + Columns, + Columns, + Columns +]; + +const getSorting = (sortField: HostTacticsFields, direction: Direction): SortingBasicTable => ({ + field: getNodeField(sortField), + direction, +}); + +const HostTacticsTableComponent: React.FC = ({ + data, + fakeTotalCount, + id, + isInspect, + loading, + loadPage, + showMorePagesIndicator, + techniqueCount, + totalCount, + type, +}) => { + const dispatch = useDispatch(); + const { activePage, limit, sort } = useDeepEqualSelector(uebaSelectors.hostTacticsSelector()); + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + uebaActions.updateTableLimit({ + uebaType: type, + limit: newLimit, + tableType, + }) + ), + [type, dispatch] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + uebaActions.updateTableActivePage({ + activePage: newPage, + uebaType: type, + tableType, // this will need to become unique for each user table in the group + }) + ), + [type, dispatch] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const newSort: HostTacticsSortField = { + field: getSortField(criteria.sort.field), + direction: criteria.sort.direction as Direction, + }; + if (newSort.direction !== sort.direction || newSort.field !== sort.field) { + // dispatch( + // uebaActions.updateHostTacticsSort({ + // sort, + // uebaType: type, + // }) + // ); TODO: Steph/ueba implement sorting + } + } + }, + [sort] + ); + + const columns = useMemo(() => getHostTacticsColumns(), []); + + const sorting = useMemo(() => getSorting(sort.field, sort.direction), [sort]); + return ( + + ); +}; + +HostTacticsTableComponent.displayName = 'HostTacticsTableComponent'; + +const getSortField = (field: string): HostTacticsFields => { + switch (field) { + case `node.${HostTacticsFields.tactic}`: + return HostTacticsFields.tactic; + case `node.${HostTacticsFields.riskScore}`: + return HostTacticsFields.riskScore; + default: + return HostTacticsFields.riskScore; + } +}; + +const getNodeField = (field: HostTacticsFields): string => `node.${field}`; + +export const HostTacticsTable = React.memo(HostTacticsTableComponent); + +HostTacticsTable.displayName = 'HostTacticsTable'; diff --git a/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/translations.ts b/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/translations.ts new file mode 100644 index 0000000000000..98cd53a59e5f3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/translations.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const COUNT = (totalCount: number, techniqueCount: number) => + i18n.translate('xpack.securitySolution.uebaTableHostTactics.tacticTechnique', { + values: { techniqueCount, totalCount }, + defaultMessage: `{totalCount} {totalCount, plural, =1 {tactic} other {tactics}} with {techniqueCount} {techniqueCount, plural, =1 {technique} other {techniques}}`, + }); + +export const TACTIC = i18n.translate('xpack.securitySolution.uebaTableHostTactics.tactic', { + defaultMessage: 'Tactic', +}); + +export const RISK_SCORE = i18n.translate( + 'xpack.securitySolution.uebaTableHostTactics.totalRiskScore', + { + defaultMessage: 'Total risk score', + } +); + +export const TECHNIQUE = i18n.translate('xpack.securitySolution.uebaTableHostTactics.technique', { + defaultMessage: 'Technique', +}); + +export const HITS = i18n.translate('xpack.securitySolution.uebaTableHostTactics.hits', { + defaultMessage: 'Number of hits', +}); diff --git a/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/columns.tsx new file mode 100644 index 0000000000000..b751521001fe5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/columns.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + DragEffects, + DraggableWrapper, +} from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { UebaDetailsLink } from '../../../common/components/links'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; +import { + AddFilterToGlobalSearchBar, + createFilter, +} from '../../../common/components/add_filter_to_global_search_bar'; +import { RiskScoreColumns } from './'; + +import * as i18n from './translations'; +export const getRiskScoreColumns = (): RiskScoreColumns => [ + { + field: 'node.host_name', + name: i18n.NAME, + truncateText: false, + hideForMobile: false, + sortable: true, + render: (hostName) => { + if (hostName != null && hostName.length > 0) { + const id = escapeDataProviderId(`ueba-table-hostName-${hostName}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: 'node.risk_keyword', + name: i18n.CURRENT_RISK, + truncateText: false, + hideForMobile: false, + sortable: false, + render: (riskKeyword) => { + if (riskKeyword != null) { + return ( + + <>{riskKeyword} + + ); + } + return getEmptyTagValue(); + }, + }, +]; diff --git a/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/index.tsx b/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/index.tsx new file mode 100644 index 0000000000000..9e9c6f81a43bb --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/index.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { useMemo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; + +import { + Columns, + Criteria, + PaginatedTable, + SortingBasicTable, +} from '../../../common/components/paginated_table'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaActions, uebaModel, uebaSelectors } from '../../store'; +import { getRiskScoreColumns } from './columns'; +import * as i18n from './translations'; +import { + RiskScoreEdges, + RiskScoreItem, + RiskScoreSortField, + RiskScoreFields, +} from '../../../../common'; +import { Direction } from '../../../../common/search_strategy'; +import { rowItems } from '../utils'; + +const tableType = uebaModel.UebaTableType.riskScore; + +interface RiskScoreTableProps { + data: RiskScoreEdges[]; + fakeTotalCount: number; + id: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: uebaModel.UebaType; +} + +export type RiskScoreColumns = [ + Columns, + Columns +]; + +const getSorting = (sortField: RiskScoreFields, direction: Direction): SortingBasicTable => ({ + field: getNodeField(sortField), + direction, +}); + +const RiskScoreTableComponent: React.FC = ({ + data, + fakeTotalCount, + id, + isInspect, + loading, + loadPage, + showMorePagesIndicator, + totalCount, + type, +}) => { + const dispatch = useDispatch(); + const { activePage, limit, sort } = useDeepEqualSelector(uebaSelectors.riskScoreSelector()); + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + uebaActions.updateTableLimit({ + uebaType: type, + limit: newLimit, + tableType, + }) + ), + [type, dispatch] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + uebaActions.updateTableActivePage({ + activePage: newPage, + uebaType: type, + tableType, + }) + ), + [type, dispatch] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const newSort: RiskScoreSortField = { + field: getSortField(criteria.sort.field), + direction: criteria.sort.direction as Direction, + }; + if (newSort.direction !== sort.direction || newSort.field !== sort.field) { + // dispatch( + // uebaActions.updateRiskScoreSort({ + // sort, + // uebaType: type, + // }) + // ); TODO: Steph/ueba implement sorting + } + } + }, + [sort] + ); + + const columns = useMemo(() => getRiskScoreColumns(), []); + + const sorting = useMemo(() => getSorting(sort.field, sort.direction), [sort]); + + return ( + + ); +}; + +RiskScoreTableComponent.displayName = 'RiskScoreTableComponent'; + +const getSortField = (field: string): RiskScoreFields => { + switch (field) { + case `node.${RiskScoreFields.hostName}`: + return RiskScoreFields.hostName; + case `node.${RiskScoreFields.riskScore}`: + return RiskScoreFields.riskScore; + default: + return RiskScoreFields.riskScore; + } +}; + +const getNodeField = (field: RiskScoreFields): string => `node.${field}`; + +export const RiskScoreTable = React.memo(RiskScoreTableComponent); + +RiskScoreTable.displayName = 'RiskScoreTable'; diff --git a/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/translations.ts b/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/translations.ts new file mode 100644 index 0000000000000..a4e7a3271d152 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.securitySolution.uebaTableRiskScore.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {user} other {users}}`, + }); + +export const NAME = i18n.translate('xpack.securitySolution.uebaTableRiskScore.nameTitle', { + defaultMessage: 'Host name', +}); + +export const RISK_SCORE = i18n.translate('xpack.securitySolution.uebaTableRiskScore.riskScore', { + defaultMessage: 'Risk score', +}); + +export const CURRENT_RISK = i18n.translate( + 'xpack.securitySolution.uebaTableRiskScore.currentRisk', + { + defaultMessage: 'Current risk', + } +); diff --git a/x-pack/plugins/security_solution/public/ueba/components/translations.ts b/x-pack/plugins/security_solution/public/ueba/components/translations.ts new file mode 100644 index 0000000000000..5775871a3fe4a --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/translations.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ROWS_5 = i18n.translate('xpack.securitySolution.uebaTable.rows', { + values: { numRows: 5 }, + defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', +}); + +export const ROWS_10 = i18n.translate('xpack.securitySolution.uebaTable.rows', { + values: { numRows: 10 }, + defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', +}); diff --git a/x-pack/plugins/security_solution/public/ueba/components/utils.ts b/x-pack/plugins/security_solution/public/ueba/components/utils.ts new file mode 100644 index 0000000000000..d12e66a5f6d7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/utils.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ItemsPerRow } from '../../common/components/paginated_table'; +import * as i18n from './translations'; + +export const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; diff --git a/x-pack/plugins/security_solution/public/ueba/containers/host_rules/index.tsx b/x-pack/plugins/security_solution/public/ueba/containers/host_rules/index.tsx new file mode 100644 index 0000000000000..7db1a77244bbe --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/host_rules/index.tsx @@ -0,0 +1,220 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Subscription } from 'rxjs'; + +import { inputsModel, State } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; +import { useKibana } from '../../../common/lib/kibana'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaModel, uebaSelectors } from '../../store'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { + HostRulesEdges, + PageInfoPaginated, + DocValueFields, + UebaQueries, + HostRulesRequestOptions, + HostRulesStrategyResponse, +} from '../../../../common'; +import { ESTermQuery } from '../../../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; +import { useTransforms } from '../../../transforms/containers/use_transforms'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; + +export const ID = 'hostRulesQuery'; + +type LoadPage = (newActivePage: number) => void; +export interface HostRulesState { + data: HostRulesEdges[]; + endDate: string; + id: string; + inspect: InspectResponse; + isInspected: boolean; + loadPage: LoadPage; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + startDate: string; + totalCount: number; +} + +interface UseHostRules { + docValueFields?: DocValueFields[]; + endDate: string; + filterQuery?: ESTermQuery | string; + hostName: string; + indexNames: string[]; + skip?: boolean; + startDate: string; + type: uebaModel.UebaType; +} + +export const useHostRules = ({ + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip = false, + startDate, +}: UseHostRules): [boolean, HostRulesState] => { + const getHostRulesSelector = useMemo(() => uebaSelectors.hostRulesSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state: State) => + getHostRulesSelector(state) + ); + const { data } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const searchSubscription = useRef(new Subscription()); + const [loading, setLoading] = useState(false); + const [hostRulesRequest, setHostRulesRequest] = useState(null); + const { getTransformChangesIfTheyExist } = useTransforms(); + const { addError, addWarning } = useAppToasts(); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setHostRulesRequest((prevRequest) => { + if (!prevRequest) { + return prevRequest; + } + + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); + + const [hostRulesResponse, setHostRulesResponse] = useState({ + data: [], + endDate, + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + startDate, + totalCount: -1, + }); + + const hostRulesSearch = useCallback( + (request: HostRulesRequestOptions | null) => { + if (request == null || skip) { + return; + } + + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + searchSubscription.current = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + setHostRulesResponse((prevResponse) => ({ + ...prevResponse, + data: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + searchSubscription.current.unsubscribe(); + } else if (isErrorResponse(response)) { + setLoading(false); + addWarning(i18n.ERROR_HOST_RULES); + searchSubscription.current.unsubscribe(); + } + }, + error: (msg) => { + setLoading(false); + addError(msg, { title: i18n.FAIL_HOST_RULES }); + searchSubscription.current.unsubscribe(); + }, + }); + setLoading(false); + }; + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [data.search, addError, addWarning, skip] + ); + + useEffect(() => { + setHostRulesRequest((prevRequest) => { + const { indices, factoryQueryType, timerange } = getTransformChangesIfTheyExist({ + factoryQueryType: UebaQueries.hostRules, + indices: indexNames, + filterQuery, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + const myRequest = { + ...(prevRequest ?? {}), + hostName, + defaultIndex: indices, + docValueFields: docValueFields ?? [], + factoryQueryType, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange, + sort, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [ + activePage, + docValueFields, + endDate, + filterQuery, + indexNames, + limit, + startDate, + sort, + getTransformChangesIfTheyExist, + hostName, + ]); + + useEffect(() => { + hostRulesSearch(hostRulesRequest); + return () => { + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + }; + }, [hostRulesRequest, hostRulesSearch]); + + return [loading, hostRulesResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/containers/host_rules/translations.ts b/x-pack/plugins/security_solution/public/ueba/containers/host_rules/translations.ts new file mode 100644 index 0000000000000..6cf5521f4eaaa --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/host_rules/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_HOST_RULES = i18n.translate( + 'xpack.securitySolution.hostRules.errorSearchDescription', + { + defaultMessage: `An error has occurred on risk score search`, + } +); + +export const FAIL_HOST_RULES = i18n.translate( + 'xpack.securitySolution.hostRules.failSearchDescription', + { + defaultMessage: `Failed to run search on risk score`, + } +); diff --git a/x-pack/plugins/security_solution/public/ueba/containers/host_tactics/index.tsx b/x-pack/plugins/security_solution/public/ueba/containers/host_tactics/index.tsx new file mode 100644 index 0000000000000..35dd2a0b08d4e --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/host_tactics/index.tsx @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Subscription } from 'rxjs'; + +import { inputsModel, State } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; +import { useKibana } from '../../../common/lib/kibana'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaModel, uebaSelectors } from '../../store'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { + HostTacticsEdges, + PageInfoPaginated, + DocValueFields, + UebaQueries, + HostTacticsRequestOptions, + HostTacticsStrategyResponse, +} from '../../../../common'; +import { ESTermQuery } from '../../../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; +import { useTransforms } from '../../../transforms/containers/use_transforms'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; + +export const ID = 'hostTacticsQuery'; + +type LoadPage = (newActivePage: number) => void; +export interface HostTacticsState { + data: HostTacticsEdges[]; + endDate: string; + id: string; + inspect: InspectResponse; + isInspected: boolean; + loadPage: LoadPage; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + startDate: string; + techniqueCount: number; + totalCount: number; +} + +interface UseHostTactics { + docValueFields?: DocValueFields[]; + endDate: string; + filterQuery?: ESTermQuery | string; + hostName: string; + indexNames: string[]; + skip?: boolean; + startDate: string; + type: uebaModel.UebaType; +} + +export const useHostTactics = ({ + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip = false, + startDate, +}: UseHostTactics): [boolean, HostTacticsState] => { + const getHostTacticsSelector = useMemo(() => uebaSelectors.hostTacticsSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state: State) => + getHostTacticsSelector(state) + ); + const { data } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const searchSubscription = useRef(new Subscription()); + const [loading, setLoading] = useState(false); + const [hostTacticsRequest, setHostTacticsRequest] = useState( + null + ); + const { getTransformChangesIfTheyExist } = useTransforms(); + const { addError, addWarning } = useAppToasts(); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setHostTacticsRequest((prevRequest) => { + if (!prevRequest) { + return prevRequest; + } + + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); + + const [hostTacticsResponse, setHostTacticsResponse] = useState({ + data: [], + endDate, + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + startDate, + techniqueCount: -1, + totalCount: -1, + }); + + const hostTacticsSearch = useCallback( + (request: HostTacticsRequestOptions | null) => { + if (request == null || skip) { + return; + } + + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + searchSubscription.current = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + setHostTacticsResponse((prevResponse) => ({ + ...prevResponse, + data: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + techniqueCount: response.techniqueCount, + })); + searchSubscription.current.unsubscribe(); + } else if (isErrorResponse(response)) { + setLoading(false); + addWarning(i18n.ERROR_HOST_RULES); + searchSubscription.current.unsubscribe(); + } + }, + error: (msg) => { + setLoading(false); + addError(msg, { title: i18n.FAIL_HOST_RULES }); + searchSubscription.current.unsubscribe(); + }, + }); + setLoading(false); + }; + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [data.search, addError, addWarning, skip] + ); + + useEffect(() => { + setHostTacticsRequest((prevRequest) => { + const { indices, factoryQueryType, timerange } = getTransformChangesIfTheyExist({ + factoryQueryType: UebaQueries.hostTactics, + indices: indexNames, + filterQuery, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + const myRequest = { + ...(prevRequest ?? {}), + hostName, + defaultIndex: indices, + docValueFields: docValueFields ?? [], + factoryQueryType, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange, + sort, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [ + activePage, + docValueFields, + endDate, + filterQuery, + indexNames, + limit, + startDate, + sort, + getTransformChangesIfTheyExist, + hostName, + ]); + + useEffect(() => { + hostTacticsSearch(hostTacticsRequest); + return () => { + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + }; + }, [hostTacticsRequest, hostTacticsSearch]); + + return [loading, hostTacticsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/containers/host_tactics/translations.ts b/x-pack/plugins/security_solution/public/ueba/containers/host_tactics/translations.ts new file mode 100644 index 0000000000000..6cf5521f4eaaa --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/host_tactics/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_HOST_RULES = i18n.translate( + 'xpack.securitySolution.hostRules.errorSearchDescription', + { + defaultMessage: `An error has occurred on risk score search`, + } +); + +export const FAIL_HOST_RULES = i18n.translate( + 'xpack.securitySolution.hostRules.failSearchDescription', + { + defaultMessage: `Failed to run search on risk score`, + } +); diff --git a/x-pack/plugins/security_solution/public/ueba/containers/risk_score/index.tsx b/x-pack/plugins/security_solution/public/ueba/containers/risk_score/index.tsx new file mode 100644 index 0000000000000..f2f353ffc0cff --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/risk_score/index.tsx @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Subscription } from 'rxjs'; + +import { inputsModel, State } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; +import { useKibana } from '../../../common/lib/kibana'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaModel, uebaSelectors } from '../../store'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { + RiskScoreEdges, + PageInfoPaginated, + DocValueFields, + UebaQueries, + RiskScoreRequestOptions, + RiskScoreStrategyResponse, +} from '../../../../common'; +import { ESTermQuery } from '../../../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; +import { useTransforms } from '../../../transforms/containers/use_transforms'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; + +export const ID = 'riskScoreQuery'; + +type LoadPage = (newActivePage: number) => void; +export interface RiskScoreState { + data: RiskScoreEdges[]; + endDate: string; + id: string; + inspect: InspectResponse; + isInspected: boolean; + loadPage: LoadPage; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + startDate: string; + totalCount: number; +} + +interface UseRiskScore { + docValueFields?: DocValueFields[]; + endDate: string; + filterQuery?: ESTermQuery | string; + indexNames: string[]; + skip?: boolean; + startDate: string; + type: uebaModel.UebaType; +} + +export const useRiskScore = ({ + docValueFields, + endDate, + filterQuery, + indexNames, + skip = false, + startDate, +}: UseRiskScore): [boolean, RiskScoreState] => { + const getRiskScoreSelector = useMemo(() => uebaSelectors.riskScoreSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state: State) => + getRiskScoreSelector(state) + ); + const { data } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const searchSubscription = useRef(new Subscription()); + const [loading, setLoading] = useState(false); + const [riskScoreRequest, setRiskScoreRequest] = useState(null); + const { getTransformChangesIfTheyExist } = useTransforms(); + const { addError, addWarning } = useAppToasts(); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setRiskScoreRequest((prevRequest) => { + if (!prevRequest) { + return prevRequest; + } + + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); + + const [riskScoreResponse, setRiskScoreResponse] = useState({ + data: [], + endDate, + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + startDate, + totalCount: -1, + }); + + const riskScoreSearch = useCallback( + (request: RiskScoreRequestOptions | null) => { + if (request == null || skip) { + return; + } + + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + searchSubscription.current = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + setRiskScoreResponse((prevResponse) => ({ + ...prevResponse, + data: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + searchSubscription.current.unsubscribe(); + } else if (isErrorResponse(response)) { + setLoading(false); + addWarning(i18n.ERROR_RISK_SCORE); + searchSubscription.current.unsubscribe(); + } + }, + error: (msg) => { + setLoading(false); + addError(msg, { title: i18n.FAIL_RISK_SCORE }); + searchSubscription.current.unsubscribe(); + }, + }); + setLoading(false); + }; + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [data.search, addError, addWarning, skip] + ); + + useEffect(() => { + setRiskScoreRequest((prevRequest) => { + const { indices, factoryQueryType, timerange } = getTransformChangesIfTheyExist({ + factoryQueryType: UebaQueries.riskScore, + indices: indexNames, + filterQuery, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + const myRequest = { + ...(prevRequest ?? {}), + defaultIndex: indices, + docValueFields: docValueFields ?? [], + factoryQueryType, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange, + sort, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [ + activePage, + docValueFields, + endDate, + filterQuery, + indexNames, + limit, + startDate, + sort, + getTransformChangesIfTheyExist, + ]); + + useEffect(() => { + riskScoreSearch(riskScoreRequest); + return () => { + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + }; + }, [riskScoreRequest, riskScoreSearch]); + + return [loading, riskScoreResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/containers/risk_score/translations.ts b/x-pack/plugins/security_solution/public/ueba/containers/risk_score/translations.ts new file mode 100644 index 0000000000000..8cc275674d4e9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/risk_score/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_RISK_SCORE = i18n.translate( + 'xpack.securitySolution.riskScore.errorSearchDescription', + { + defaultMessage: `An error has occurred on risk score search`, + } +); + +export const FAIL_RISK_SCORE = i18n.translate( + 'xpack.securitySolution.riskScore.failSearchDescription', + { + defaultMessage: `Failed to run search on risk score`, + } +); diff --git a/x-pack/plugins/security_solution/public/ueba/containers/user_rules/index.tsx b/x-pack/plugins/security_solution/public/ueba/containers/user_rules/index.tsx new file mode 100644 index 0000000000000..3c4e45bd3a1e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/user_rules/index.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Subscription } from 'rxjs'; + +import { inputsModel, State } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; +import { useKibana } from '../../../common/lib/kibana'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaModel, uebaSelectors } from '../../store'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { + DocValueFields, + UebaQueries, + UserRulesRequestOptions, + UserRulesStrategyResponse, + UserRulesStrategyUserResponse, +} from '../../../../common'; +import { ESTermQuery } from '../../../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; +import { useTransforms } from '../../../transforms/containers/use_transforms'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; + +export const ID = 'userRulesQuery'; + +type LoadPage = (newActivePage: number) => void; +export interface UserRulesState { + data: UserRulesStrategyUserResponse[]; + endDate: string; + id: string; + inspect: InspectResponse; + isInspected: boolean; + loadPage: LoadPage; + refetch: inputsModel.Refetch; + startDate: string; +} + +interface UseUserRules { + docValueFields?: DocValueFields[]; + endDate: string; + filterQuery?: ESTermQuery | string; + hostName: string; + indexNames: string[]; + skip?: boolean; + startDate: string; + type: uebaModel.UebaType; +} + +export const useUserRules = ({ + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip = false, + startDate, +}: UseUserRules): [boolean, UserRulesState] => { + const getUserRulesSelector = useMemo(() => uebaSelectors.userRulesSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state: State) => + getUserRulesSelector(state) + ); + const { data } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const searchSubscription = useRef(new Subscription()); + const [loading, setLoading] = useState(false); + const [userRulesRequest, setUserRulesRequest] = useState(null); + const { getTransformChangesIfTheyExist } = useTransforms(); + const { addError, addWarning } = useAppToasts(); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setUserRulesRequest((prevRequest) => { + if (!prevRequest) { + return prevRequest; + } + + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); + + const [userRulesResponse, setUserRulesResponse] = useState({ + data: [], + endDate, + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + refetch: refetch.current, + startDate, + }); + + const userRulesSearch = useCallback( + (request: UserRulesRequestOptions | null) => { + if (request == null || skip) { + return; + } + + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + searchSubscription.current = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + setUserRulesResponse((prevResponse) => ({ + ...prevResponse, + data: response.data, + inspect: getInspectResponse(response, prevResponse.inspect), + refetch: refetch.current, + })); + searchSubscription.current.unsubscribe(); + } else if (isErrorResponse(response)) { + setLoading(false); + addWarning(i18n.ERROR_HOST_RULES); + searchSubscription.current.unsubscribe(); + } + }, + error: (msg) => { + setLoading(false); + addError(msg, { title: i18n.FAIL_HOST_RULES }); + searchSubscription.current.unsubscribe(); + }, + }); + setLoading(false); + }; + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [data.search, addError, addWarning, skip] + ); + + useEffect(() => { + setUserRulesRequest((prevRequest) => { + const { indices, factoryQueryType, timerange } = getTransformChangesIfTheyExist({ + factoryQueryType: UebaQueries.userRules, + indices: indexNames, + filterQuery, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + const myRequest = { + ...(prevRequest ?? {}), + hostName, + defaultIndex: indices, + docValueFields: docValueFields ?? [], + factoryQueryType, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange, + sort, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [ + activePage, + docValueFields, + endDate, + filterQuery, + indexNames, + limit, + startDate, + sort, + getTransformChangesIfTheyExist, + hostName, + ]); + + useEffect(() => { + userRulesSearch(userRulesRequest); + return () => { + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + }; + }, [userRulesRequest, userRulesSearch]); + + return [loading, userRulesResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/containers/user_rules/translations.ts b/x-pack/plugins/security_solution/public/ueba/containers/user_rules/translations.ts new file mode 100644 index 0000000000000..6cf5521f4eaaa --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/user_rules/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_HOST_RULES = i18n.translate( + 'xpack.securitySolution.hostRules.errorSearchDescription', + { + defaultMessage: `An error has occurred on risk score search`, + } +); + +export const FAIL_HOST_RULES = i18n.translate( + 'xpack.securitySolution.hostRules.failSearchDescription', + { + defaultMessage: `Failed to run search on risk score`, + } +); diff --git a/x-pack/plugins/security_solution/public/ueba/index.ts b/x-pack/plugins/security_solution/public/ueba/index.ts new file mode 100644 index 0000000000000..030844735b0f1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { SecuritySubPluginWithStore } from '../app/types'; +import { routes } from './routes'; +import { initialUebaState, uebaReducer, uebaModel } from './store'; +import { TimelineId } from '../../common/types/timeline'; +import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; + +export class Ueba { + public setup() {} + + public start(storage: Storage): SecuritySubPluginWithStore<'ueba', uebaModel.UebaModel> { + return { + routes, + storageTimelines: { + timelineById: getTimelinesInStorageByIds(storage, [TimelineId.uebaPageExternalAlerts]), + }, + store: { + initialState: { ueba: initialUebaState }, + reducer: { ueba: uebaReducer }, + }, + }; + } +} diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/ueba/pages/details/details_tabs.tsx new file mode 100644 index 0000000000000..dad3277d0a7a4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/details_tabs.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { UpdateDateRange } from '../../../common/components/charts/common'; +import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; +import { Anomaly } from '../../../common/components/ml/types'; +import { UebaTableType } from '../../store/model'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; + +import { UebaDetailsTabsProps } from './types'; +import { type } from './utils'; + +import { + HostRulesQueryTabBody, + HostTacticsQueryTabBody, + UserRulesQueryTabBody, +} from '../navigation'; + +export const UebaDetailsTabs = React.memo( + ({ + detailName, + docValueFields, + filterQuery, + indexNames, + indexPattern, + pageFilters, + setAbsoluteRangeDatePicker, + uebaDetailsPagePath, + }) => { + const { from, to, isInitializing, deleteQuery, setQuery } = useGlobalTime(); + const narrowDateRange = useCallback( + (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ); + + const updateDateRange = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); + }, + [setAbsoluteRangeDatePicker] + ); + + const tabProps = { + deleteQuery, + endDate: to, + filterQuery, + skip: isInitializing || filterQuery === undefined, + setQuery, + startDate: from, + type, + indexPattern, + indexNames, + hostName: detailName, + narrowDateRange, + updateDateRange, + }; + return ( + + + + + + + + + + + + ); + } +); + +UebaDetailsTabs.displayName = 'UebaDetailsTabs'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/helpers.ts b/x-pack/plugins/security_solution/public/ueba/pages/details/helpers.ts new file mode 100644 index 0000000000000..70f8027b1f55b --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/helpers.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { escapeQueryValue } from '../../../common/lib/keury'; +import { Filter } from '../../../../../../../src/plugins/data/public'; + +/** Returns the kqlQueryExpression for the `Events` widget on the `Host Details` page */ +export const getUebaDetailsEventsKqlQueryExpression = ({ + filterQueryExpression, + hostName, +}: { + filterQueryExpression: string; + hostName: string; +}): string => { + if (filterQueryExpression.length) { + return `${filterQueryExpression}${ + hostName.length ? ` and host.name: ${escapeQueryValue(hostName)}` : '' + }`; + } else { + return hostName.length ? `host.name: ${escapeQueryValue(hostName)}` : ''; + } +}; + +export const getUebaDetailsPageFilters = (hostName: string): Filter[] => [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.name', + value: hostName, + params: { + query: hostName, + }, + }, + query: { + match: { + 'host.name': { + query: hostName, + type: 'phrase', + }, + }, + }, + }, +]; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx b/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx new file mode 100644 index 0000000000000..5a297099f3834 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React, { useEffect, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; + +import { LastEventIndexKey } from '../../../../common/search_strategy'; +import { SecurityPageName } from '../../../app/types'; +import { FiltersGlobal } from '../../../common/components/filters_global'; +import { HeaderPage } from '../../../common/components/header_page'; +import { LastEventTime } from '../../../common/components/last_event_time'; +import { SecuritySolutionTabNavigation } from '../../../common/components/navigation'; +import { SiemSearchBar } from '../../../common/components/search_bar'; +import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; +import { useKibana } from '../../../common/lib/kibana'; +import { convertToBuildEsQuery } from '../../../common/lib/keury'; +import { inputsSelectors } from '../../../common/store'; +import { setUebaDetailsTablesActivePageToZero } from '../../store/actions'; +import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; +import { SpyRoute } from '../../../common/utils/route/spy_routes'; +import { esQuery, Filter } from '../../../../../../../src/plugins/data/public'; + +import { OverviewEmpty } from '../../../overview/components/overview_empty'; +import { UebaDetailsTabs } from './details_tabs'; +import { navTabsUebaDetails } from './nav_tabs'; +import { UebaDetailsProps } from './types'; +import { type } from './utils'; +import { getUebaDetailsPageFilters } from './helpers'; +import { showGlobalFilters } from '../../../timelines/components/timeline/helpers'; +import { useGlobalFullScreen } from '../../../common/containers/use_full_screen'; +import { Display } from '../display'; +import { timelineSelectors } from '../../../timelines/store/timeline'; +import { TimelineId } from '../../../../common/types/timeline'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; +import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +const ID = 'UebaDetailsQueryId'; + +const UebaDetailsComponent: React.FC = ({ detailName, uebaDetailsPagePath }) => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const graphEventId = useShallowEqualSelector( + (state) => (getTimeline(state, TimelineId.hostsPageEvents) ?? timelineDefaults).graphEventId + ); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + + const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); + const { globalFullScreen } = useGlobalFullScreen(); + + const kibana = useKibana(); + const uebaDetailsPageFilters: Filter[] = useMemo(() => getUebaDetailsPageFilters(detailName), [ + detailName, + ]); + const getFilters = () => [...uebaDetailsPageFilters, ...filters]; + + const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope( + SourcererScopeName.detections + ); + + const [filterQuery, kqlError] = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: getFilters(), + }); + + useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to }); + + useEffect(() => { + dispatch(setUebaDetailsTablesActivePageToZero()); + }, [dispatch, detailName]); + + return ( + <> + {indicesExist ? ( + <> + + + + + + + + + } + title={detailName} + /> + + + + + + + + + ) : ( + + + + + + )} + + + + ); +}; + +UebaDetailsComponent.displayName = 'UebaDetailsComponent'; + +export const UebaDetails = React.memo(UebaDetailsComponent); diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/nav_tabs.tsx b/x-pack/plugins/security_solution/public/ueba/pages/details/nav_tabs.tsx new file mode 100644 index 0000000000000..ba97a03bf6daf --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/nav_tabs.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as i18n from '../translations'; +import { UebaDetailsNavTab } from './types'; +import { UebaTableType } from '../../store/model'; +import { UEBA_PATH } from '../../../../common/constants'; + +const getTabsOnUebaDetailsUrl = (hostName: string, tabName: UebaTableType) => + `${UEBA_PATH}/${hostName}/${tabName}`; + +export const navTabsUebaDetails = (hostName: string): UebaDetailsNavTab => { + return { + [UebaTableType.hostRules]: { + id: UebaTableType.hostRules, + name: i18n.HOST_RULES, + href: getTabsOnUebaDetailsUrl(hostName, UebaTableType.hostRules), + disabled: false, + }, + [UebaTableType.hostTactics]: { + id: UebaTableType.hostTactics, + name: i18n.HOST_TACTICS, + href: getTabsOnUebaDetailsUrl(hostName, UebaTableType.hostTactics), + disabled: false, + }, + [UebaTableType.userRules]: { + id: UebaTableType.userRules, + name: i18n.USER_RULES, + href: getTabsOnUebaDetailsUrl(hostName, UebaTableType.userRules), + disabled: false, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/types.ts b/x-pack/plugins/security_solution/public/ueba/pages/details/types.ts new file mode 100644 index 0000000000000..976b033db5f5a --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/types.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionCreator } from 'typescript-fsa'; +import { Query, IIndexPattern, Filter } from 'src/plugins/data/public'; +import { InputsModelId } from '../../../common/store/inputs/constants'; +import { UebaTableType } from '../../store/model'; +import { UebaQueryProps } from '../types'; +import { NavTab } from '../../../common/components/navigation/types'; +import { uebaModel } from '../../store'; +import { DocValueFields } from '../../../common/containers/source'; + +interface UebaDetailsComponentReduxProps { + query: Query; + filters: Filter[]; +} + +interface HostBodyComponentDispatchProps { + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: string; + to: string; + }>; + detailName: string; + uebaDetailsPagePath: string; +} + +interface UebaDetailsComponentDispatchProps extends HostBodyComponentDispatchProps { + setUebaDetailsTablesActivePageToZero: ActionCreator; +} + +export interface UebaDetailsProps { + detailName: string; + uebaDetailsPagePath: string; +} + +export type UebaDetailsComponentProps = UebaDetailsComponentReduxProps & + UebaDetailsComponentDispatchProps & + UebaQueryProps; + +type KeyUebaDetailsNavTab = UebaTableType.hostRules & + UebaTableType.hostTactics & + UebaTableType.userRules; + +export type UebaDetailsNavTab = Record; + +export type UebaDetailsTabsProps = HostBodyComponentDispatchProps & + UebaQueryProps & { + docValueFields?: DocValueFields[]; + indexNames: string[]; + pageFilters?: Filter[]; + filterQuery?: string; + indexPattern: IIndexPattern; + type: uebaModel.UebaType; + }; + +export type SetAbsoluteRangeDatePicker = ActionCreator<{ + id: InputsModelId; + from: string; + to: string; +}>; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/utils.ts b/x-pack/plugins/security_solution/public/ueba/pages/details/utils.ts new file mode 100644 index 0000000000000..d5f346d3ece64 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/utils.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get, isEmpty } from 'lodash/fp'; + +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { uebaModel } from '../../store'; +import { UebaTableType } from '../../store/model'; +import { getUebaDetailsUrl } from '../../../common/components/link_to/redirect_to_ueba'; + +import * as i18n from '../translations'; +import { UebaRouteSpyState } from '../../../common/utils/route/types'; +import { GetUrlForApp } from '../../../common/components/navigation/types'; +import { APP_ID } from '../../../../common/constants'; +import { SecurityPageName } from '../../../app/types'; + +export const type = uebaModel.UebaType.details; + +const TabNameMappedToI18nKey: Record = { + [UebaTableType.hostRules]: i18n.HOST_RULES, + [UebaTableType.hostTactics]: i18n.HOST_TACTICS, + [UebaTableType.riskScore]: i18n.RISK_SCORE_TITLE, + [UebaTableType.userRules]: i18n.USER_RULES, +}; + +export const getBreadcrumbs = ( + params: UebaRouteSpyState, + search: string[], + getUrlForApp: GetUrlForApp +): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: getUrlForApp(APP_ID, { + path: !isEmpty(search[0]) ? search[0] : '', + deepLinkId: SecurityPageName.ueba, + }), + }, + ]; + + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.detailName, + href: getUrlForApp(APP_ID, { + path: getUebaDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), + deepLinkId: SecurityPageName.ueba, + }), + }, + ]; + } + + if (params.tabName != null) { + const tabName = get('tabName', params); + if (!tabName) return breadcrumb; + + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[tabName], + href: '', + }, + ]; + } + return breadcrumb; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/display.tsx b/x-pack/plugins/security_solution/public/ueba/pages/display.tsx new file mode 100644 index 0000000000000..a907f1fdb5997 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/display.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import styled from 'styled-components'; + +export const Display = styled.div<{ show: boolean }>` + ${({ show }) => (show ? '' : 'display: none;')}; +`; + +Display.displayName = 'Display'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/index.tsx b/x-pack/plugins/security_solution/public/ueba/pages/index.tsx new file mode 100644 index 0000000000000..c4a6794b75999 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/index.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Route, Switch, Redirect } from 'react-router-dom'; +import { UEBA_PATH } from '../../../common/constants'; +import { UebaTableType } from '../store/model'; +import { Ueba } from './ueba'; +import { uebaDetailsPagePath } from './types'; +import { UebaDetails } from './details'; + +const uebaTabPath = `${UEBA_PATH}/:tabName(${UebaTableType.riskScore})`; + +const uebaDetailsTabPath = + `${uebaDetailsPagePath}/:tabName(` + + `${UebaTableType.hostRules}|` + + `${UebaTableType.hostTactics}|` + + `${UebaTableType.userRules})`; + +export const UebaContainer = React.memo(() => ( + + ( + + )} + /> + + + + + } + /> + ( + + )} + /> + +)); + +UebaContainer.displayName = 'UebaContainer'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/nav_tabs.tsx b/x-pack/plugins/security_solution/public/ueba/pages/nav_tabs.tsx new file mode 100644 index 0000000000000..5e06e5c9bf068 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/nav_tabs.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as i18n from './translations'; +import { UebaTableType } from '../store/model'; +import { UebaNavTab } from './navigation/types'; +import { UEBA_PATH } from '../../../common/constants'; + +const getTabsOnUebaUrl = (tabName: UebaTableType) => `${UEBA_PATH}/${tabName}`; + +export const navTabsUeba: UebaNavTab = { + [UebaTableType.riskScore]: { + id: UebaTableType.riskScore, + name: i18n.RISK_SCORE_TITLE, + href: getTabsOnUebaUrl(UebaTableType.riskScore), + disabled: false, + }, +}; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/navigation/host_rules_query_tab_body.tsx b/x-pack/plugins/security_solution/public/ueba/pages/navigation/host_rules_query_tab_body.tsx new file mode 100644 index 0000000000000..bce19a9da7ab9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/navigation/host_rules_query_tab_body.tsx @@ -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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { useHostRules } from '../../containers/host_rules'; +import { HostQueryProps } from './types'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { HostRulesTable } from '../../components/host_rules_table'; +import { uebaModel } from '../../store'; + +const HostRulesTableManage = manageQuery(HostRulesTable); + +export const HostRulesQueryTabBody = ({ + deleteQuery, + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip, + setQuery, + startDate, + type, +}: HostQueryProps) => { + const [ + loading, + { data, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useHostRules({ + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip, + startDate, + type, + }); + + return ( + + ); +}; + +HostRulesQueryTabBody.displayName = 'HostRulesQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/navigation/host_tactics_query_tab_body.tsx b/x-pack/plugins/security_solution/public/ueba/pages/navigation/host_tactics_query_tab_body.tsx new file mode 100644 index 0000000000000..c441eff3219d2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/navigation/host_tactics_query_tab_body.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { useHostTactics } from '../../containers/host_tactics'; +import { HostQueryProps } from './types'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { HostTacticsTable } from '../../components/host_tactics_table'; + +const HostTacticsTableManage = manageQuery(HostTacticsTable); + +export const HostTacticsQueryTabBody = ({ + deleteQuery, + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip, + setQuery, + startDate, + type, +}: HostQueryProps) => { + const [ + loading, + { data, techniqueCount, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useHostTactics({ + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip, + startDate, + type, + }); + + return ( + + ); +}; + +HostTacticsQueryTabBody.displayName = 'HostTacticsQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/navigation/index.ts b/x-pack/plugins/security_solution/public/ueba/pages/navigation/index.ts new file mode 100644 index 0000000000000..dd549659a3eab --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/navigation/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './host_rules_query_tab_body'; +export * from './host_tactics_query_tab_body'; +export * from './risk_score_query_tab_body'; +export * from './user_rules_query_tab_body'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/navigation/risk_score_query_tab_body.tsx b/x-pack/plugins/security_solution/public/ueba/pages/navigation/risk_score_query_tab_body.tsx new file mode 100644 index 0000000000000..cde972d8a66ca --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/navigation/risk_score_query_tab_body.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { useRiskScore } from '../../containers/risk_score'; +import { RiskScoreQueryProps } from './types'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { RiskScoreTable } from '../../components/risk_score_table'; + +const RiskScoreTableManage = manageQuery(RiskScoreTable); + +export const RiskScoreQueryTabBody = ({ + deleteQuery, + docValueFields, + endDate, + filterQuery, + indexNames, + skip, + setQuery, + startDate, + type, +}: RiskScoreQueryProps) => { + const [ + loading, + { data, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useRiskScore({ docValueFields, endDate, filterQuery, indexNames, skip, startDate, type }); + + return ( + + ); +}; + +RiskScoreQueryTabBody.displayName = 'RiskScoreQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/ueba/pages/navigation/types.ts new file mode 100644 index 0000000000000..e24b3271cf534 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/navigation/types.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 { UebaTableType, UebaType } from '../../store/model'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; +import { ESTermQuery } from '../../../../common/typed_json'; +import { DocValueFields } from '../../../../../timelines/common'; +import { Filter } from '../../../../../../../src/plugins/data/common'; +import { UpdateDateRange } from '../../../common/components/charts/common'; +import { NarrowDateRange } from '../../../common/components/ml/types'; +import { NavTab } from '../../../common/components/navigation/types'; + +type KeyUebaNavTab = UebaTableType.riskScore; + +export type UebaNavTab = Record; +export interface QueryTabBodyProps { + type: UebaType; + startDate: GlobalTimeArgs['from']; + endDate: GlobalTimeArgs['to']; + filterQuery?: string | ESTermQuery; +} + +export type RiskScoreQueryProps = QueryTabBodyProps & { + deleteQuery?: GlobalTimeArgs['deleteQuery']; + docValueFields?: DocValueFields[]; + indexNames: string[]; + pageFilters?: Filter[]; + skip: boolean; + setQuery: GlobalTimeArgs['setQuery']; + updateDateRange?: UpdateDateRange; + narrowDateRange?: NarrowDateRange; +}; +export type HostQueryProps = RiskScoreQueryProps & { + hostName: string; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/navigation/user_rules_query_tab_body.tsx b/x-pack/plugins/security_solution/public/ueba/pages/navigation/user_rules_query_tab_body.tsx new file mode 100644 index 0000000000000..f7542b7b4b8a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/navigation/user_rules_query_tab_body.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useUserRules } from '../../containers/user_rules'; +import { HostQueryProps } from './types'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { HostRulesTable } from '../../components/host_rules_table'; +import { uebaModel } from '../../store'; +import { UserRulesFields } from '../../../../common'; + +const UserRulesTableManage = manageQuery(HostRulesTable); + +export const UserRulesQueryTabBody = ({ + deleteQuery, + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip, + setQuery, + startDate, + type, +}: HostQueryProps) => { + const [loading, { data, loadPage, id, inspect, isInspected, refetch }] = useUserRules({ + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip, + startDate, + type, + }); + return ( + + {data.map((user, i) => ( + + {`Total user risk score: ${user[UserRulesFields.riskScore]}`}

} + headerTitle={`user.name: ${user[UserRulesFields.userName]}`} + fakeTotalCount={getOr(50, 'fakeTotalCount', user.pageInfo)} + id={`${id}${i}`} + inspect={inspect} + isInspect={isInspected} + loading={loading} + loadPage={loadPage} + refetch={refetch} + setQuery={setQuery} + showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', user.pageInfo)} + tableType={uebaModel.UebaTableType.userRules} // pagination will not work until this is unique + totalCount={user.totalCount} + type={type} + /> +
+ ))} +
+ ); +}; + +UserRulesQueryTabBody.displayName = 'UserRulesQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/translations.ts b/x-pack/plugins/security_solution/public/ueba/pages/translations.ts new file mode 100644 index 0000000000000..0e6519d9d45ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/translations.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate('xpack.securitySolution.ueba.pageTitle', { + defaultMessage: 'Users & Entities', +}); +export const RISK_SCORE_TITLE = i18n.translate('xpack.securitySolution.ueba.riskScore', { + defaultMessage: 'Risk score', +}); + +export const HOST_RULES = i18n.translate('xpack.securitySolution.ueba.hostRules', { + defaultMessage: 'Host risk score by rule', +}); + +export const HOST_TACTICS = i18n.translate('xpack.securitySolution.ueba.hostTactics', { + defaultMessage: 'Host risk score by tactic', +}); + +export const USER_RULES = i18n.translate('xpack.securitySolution.ueba.userRules', { + defaultMessage: 'User risk score by rule', +}); diff --git a/x-pack/plugins/security_solution/public/ueba/pages/types.ts b/x-pack/plugins/security_solution/public/ueba/pages/types.ts new file mode 100644 index 0000000000000..07c4d5fccd066 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ActionCreator } from 'typescript-fsa'; + +import { GlobalTimeArgs } from '../../common/containers/use_global_time'; +import { UEBA_PATH } from '../../../common/constants'; +import { uebaModel } from '../../ueba/store'; +import { DocValueFields } from '../../../../timelines/common'; +import { InputsModelId } from '../../common/store/inputs/constants'; + +export const uebaDetailsPagePath = `${UEBA_PATH}/:detailName`; + +export type UebaTabsProps = GlobalTimeArgs & { + docValueFields: DocValueFields[]; + filterQuery: string; + indexNames: string[]; + type: uebaModel.UebaType; + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: string; + to: string; + }>; +}; + +export type UebaQueryProps = GlobalTimeArgs; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/ueba.tsx b/x-pack/plugins/security_solution/public/ueba/pages/ueba.tsx new file mode 100644 index 0000000000000..4e0041a98454c --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/ueba.tsx @@ -0,0 +1,184 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; +import styled from 'styled-components'; +import { noop } from 'lodash/fp'; +import React, { useCallback, useMemo, useRef } from 'react'; +import { isTab } from '../../../../timelines/public'; + +import { SecurityPageName } from '../../app/types'; +import { FiltersGlobal } from '../../common/components/filters_global'; +import { HeaderPage } from '../../common/components/header_page'; +import { LastEventTime } from '../../common/components/last_event_time'; +import { SecuritySolutionTabNavigation } from '../../common/components/navigation'; + +import { SiemSearchBar } from '../../common/components/search_bar'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; +import { useGlobalFullScreen } from '../../common/containers/use_full_screen'; +import { useGlobalTime } from '../../common/containers/use_global_time'; +import { TimelineId } from '../../../common'; +import { LastEventIndexKey } from '../../../common/search_strategy'; +import { useKibana } from '../../common/lib/kibana'; +import { convertToBuildEsQuery } from '../../common/lib/keury'; +import { inputsSelectors } from '../../common/store'; +import { setAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; + +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { esQuery } from '../../../../../../src/plugins/data/public'; +import { OverviewEmpty } from '../../overview/components/overview_empty'; +import { Display } from './display'; +import { UebaTabs } from './ueba_tabs'; +import { navTabsUeba } from './nav_tabs'; +import * as i18n from './translations'; +import { uebaModel } from '../store'; +import { + onTimelineTabKeyPressed, + resetKeyboardFocus, + showGlobalFilters, +} from '../../timelines/components/timeline/helpers'; +import { timelineSelectors } from '../../timelines/store/timeline'; +import { timelineDefaults } from '../../timelines/store/timeline/defaults'; +import { useSourcererScope } from '../../common/containers/sourcerer'; +import { useDeepEqualSelector, useShallowEqualSelector } from '../../common/hooks/use_selector'; +import { useInvalidFilterQuery } from '../../common/hooks/use_invalid_filter_query'; + +const ID = 'UebaQueryId'; + +/** + * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. + */ +const StyledFullHeightContainer = styled.div` + display: flex; + flex-direction: column; + flex: 1 1 auto; +`; + +const UebaComponent = () => { + const containerElement = useRef(null); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const graphEventId = useShallowEqualSelector( + (state) => + (getTimeline(state, TimelineId.uebaPageExternalAlerts) ?? timelineDefaults).graphEventId + ); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + + const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); + const { globalFullScreen } = useGlobalFullScreen(); + const { uiSettings } = useKibana().services; + const tabsFilters = filters; + + const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); + const [filterQuery, kqlError] = useMemo( + () => + convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(uiSettings), + indexPattern, + queries: [query], + filters, + }), + [filters, indexPattern, uiSettings, query] + ); + const [tabsFilterQuery] = useMemo( + () => + convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(uiSettings), + indexPattern, + queries: [query], + filters: tabsFilters, + }), + [indexPattern, query, tabsFilters, uiSettings] + ); + + useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to }); + + const onSkipFocusBeforeEventsTable = useCallback(() => { + containerElement.current + ?.querySelector('.inspectButtonComponent:last-of-type') + ?.focus(); + }, [containerElement]); + + const onSkipFocusAfterEventsTable = useCallback(() => { + resetKeyboardFocus(); + }, []); + + const onKeyDown = useCallback( + (keyboardEvent: React.KeyboardEvent) => { + if (isTab(keyboardEvent)) { + onTimelineTabKeyPressed({ + containerElement: containerElement.current, + keyboardEvent, + onSkipFocusBeforeEventsTable, + onSkipFocusAfterEventsTable, + }); + } + }, + [containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable] + ); + + return ( + <> + {indicesExist ? ( + + + + + + + + + + } + title={i18n.PAGE_TITLE} + /> + + + + + + + + + + ) : ( + + + + + + )} + + + + ); +}; +UebaComponent.displayName = 'UebaComponent'; + +export const Ueba = React.memo(UebaComponent); diff --git a/x-pack/plugins/security_solution/public/ueba/pages/ueba_tabs.tsx b/x-pack/plugins/security_solution/public/ueba/pages/ueba_tabs.tsx new file mode 100644 index 0000000000000..b6ae4419b609a --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/ueba_tabs.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback } from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { UebaTabsProps } from './types'; +import { scoreIntervalToDateTime } from '../../common/components/ml/score/score_interval_to_datetime'; +import { Anomaly } from '../../common/components/ml/types'; +import { UebaTableType } from '../store/model'; +import { UpdateDateRange } from '../../common/components/charts/common'; +import { UEBA_PATH } from '../../../common/constants'; +import { RiskScoreQueryTabBody } from './navigation'; + +export const UebaTabs = memo( + ({ + deleteQuery, + docValueFields, + filterQuery, + from, + indexNames, + isInitializing, + setAbsoluteRangeDatePicker, + setQuery, + to, + type, + }) => { + const narrowDateRange = useCallback( + (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ); + + const updateDateRange = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); + }, + [setAbsoluteRangeDatePicker] + ); + + const tabProps = { + deleteQuery, + endDate: to, + filterQuery, + indexNames, + skip: isInitializing || filterQuery === undefined, + setQuery, + startDate: from, + type, + narrowDateRange, + updateDateRange, + }; + + return ( + + + + + + ); + } +); + +UebaTabs.displayName = 'UebaTabs'; diff --git a/x-pack/plugins/security_solution/public/ueba/routes.tsx b/x-pack/plugins/security_solution/public/ueba/routes.tsx new file mode 100644 index 0000000000000..4d761856155e3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/routes.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 React from 'react'; +import { UebaContainer } from './pages'; + +import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; +import { SecurityPageName, SecuritySubPluginRoutes } from '../app/types'; +import { UEBA_PATH } from '../../common/constants'; + +export const UebaRoutes = () => ( + + + +); + +export const routes: SecuritySubPluginRoutes = [ + { + path: UEBA_PATH, + render: UebaRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/ueba/store/actions.ts b/x-pack/plugins/security_solution/public/ueba/store/actions.ts new file mode 100644 index 0000000000000..72ec2ff425d20 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/store/actions.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import actionCreatorFactory from 'typescript-fsa'; +import { uebaModel } from '.'; + +const actionCreator = actionCreatorFactory('x-pack/security_solution/local/ueba'); + +export const updateUebaTable = actionCreator<{ + uebaType: uebaModel.UebaType; + tableType: uebaModel.UebaTableType | uebaModel.UebaTableType; + updates: uebaModel.TableUpdates; +}>('UPDATE_NETWORK_TABLE'); + +export const setUebaDetailsTablesActivePageToZero = actionCreator( + 'SET_UEBA_DETAILS_TABLES_ACTIVE_PAGE_TO_ZERO' +); + +export const setUebaTablesActivePageToZero = actionCreator('SET_UEBA_TABLES_ACTIVE_PAGE_TO_ZERO'); + +export const updateTableLimit = actionCreator<{ + uebaType: uebaModel.UebaType; + limit: number; + tableType: uebaModel.UebaTableType; +}>('UPDATE_UEBA_TABLE_LIMIT'); + +export const updateTableActivePage = actionCreator<{ + uebaType: uebaModel.UebaType; + activePage: number; + tableType: uebaModel.UebaTableType; +}>('UPDATE_UEBA_ACTIVE_PAGE'); diff --git a/x-pack/plugins/security_solution/public/ueba/store/helpers.ts b/x-pack/plugins/security_solution/public/ueba/store/helpers.ts new file mode 100644 index 0000000000000..653cf30fac484 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/store/helpers.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UebaModel, UebaType, UebaTableType, UebaQueries, UebaDetailsQueries } from './model'; +import { DEFAULT_TABLE_ACTIVE_PAGE } from '../../common/store/constants'; + +export const setUebaPageQueriesActivePageToZero = (state: UebaModel): UebaQueries => ({ + ...state.page.queries, + [UebaTableType.riskScore]: { + ...state.page.queries[UebaTableType.riskScore], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, +}); + +export const setUebaDetailsQueriesActivePageToZero = (state: UebaModel): UebaDetailsQueries => ({ + ...state.details.queries, + [UebaTableType.hostRules]: { + ...state.details.queries[UebaTableType.hostRules], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [UebaTableType.hostTactics]: { + ...state.details.queries[UebaTableType.hostTactics], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [UebaTableType.userRules]: { + ...state.details.queries[UebaTableType.userRules], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, +}); + +export const setUebaQueriesActivePageToZero = ( + state: UebaModel, + type: UebaType +): UebaQueries | UebaDetailsQueries => { + if (type === UebaType.page) { + return setUebaPageQueriesActivePageToZero(state); + } else if (type === UebaType.details) { + return setUebaDetailsQueriesActivePageToZero(state); + } + throw new Error(`UebaType ${type} is unknown`); +}; diff --git a/x-pack/plugins/security_solution/public/ueba/store/index.ts b/x-pack/plugins/security_solution/public/ueba/store/index.ts new file mode 100644 index 0000000000000..8538509e58d4b --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/store/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Reducer, AnyAction } from 'redux'; +import * as uebaActions from './actions'; +import * as uebaModel from './model'; +import * as uebaSelectors from './selectors'; + +export { uebaActions, uebaModel, uebaSelectors }; +export * from './reducer'; + +export interface UebaPluginState { + ueba: uebaModel.UebaModel; +} + +export interface UebaPluginReducer { + ueba: Reducer; +} diff --git a/x-pack/plugins/security_solution/public/ueba/store/model.ts b/x-pack/plugins/security_solution/public/ueba/store/model.ts new file mode 100644 index 0000000000000..9e9f39977c8ef --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/store/model.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + HostRulesSortField, + HostTacticsSortField, + RiskScoreFields, + RiskScoreSortField, + SortField, + UserRulesSortField, +} from '../../../common/search_strategy'; + +export enum UebaType { + page = 'page', + details = 'details', +} + +export enum UebaTableType { + riskScore = 'riskScore', + hostRules = 'hostRules', + hostTactics = 'hostTactics', + userRules = 'userRules', +} + +export type AllUebaTables = UebaTableType; + +export interface BasicQueryPaginated { + activePage: number; + limit: number; +} + +// Ueba Page Models +export interface RiskScoreQuery extends BasicQueryPaginated { + sort: RiskScoreSortField; +} +export interface HostRulesQuery extends BasicQueryPaginated { + sort: HostRulesSortField; +} +export interface UserRulesQuery extends BasicQueryPaginated { + sort: UserRulesSortField; +} +export interface HostTacticsQuery extends BasicQueryPaginated { + sort: HostTacticsSortField; +} + +export interface TableUpdates { + activePage?: number; + limit?: number; + isPtrIncluded?: boolean; + sort?: SortField; +} + +export interface UebaQueries { + [UebaTableType.riskScore]: RiskScoreQuery; +} + +export interface UebaPageModel { + queries: UebaQueries; +} + +export interface UebaDetailsQueries { + [UebaTableType.hostRules]: HostRulesQuery; + [UebaTableType.hostTactics]: HostTacticsQuery; + [UebaTableType.userRules]: UserRulesQuery; +} + +export interface UebaDetailsModel { + queries: UebaDetailsQueries; +} + +export interface UebaModel { + [UebaType.page]: UebaPageModel; + [UebaType.details]: UebaDetailsModel; +} diff --git a/x-pack/plugins/security_solution/public/ueba/store/reducer.ts b/x-pack/plugins/security_solution/public/ueba/store/reducer.ts new file mode 100644 index 0000000000000..f981868c21eb1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/store/reducer.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { reducerWithInitialState } from 'typescript-fsa-reducers'; +import { get } from 'lodash/fp'; +import { + Direction, + HostRulesFields, + HostTacticsFields, + RiskScoreFields, +} from '../../../common/search_strategy'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../common/store/constants'; + +import { + setUebaDetailsTablesActivePageToZero, + setUebaTablesActivePageToZero, + updateUebaTable, + updateTableActivePage, + updateTableLimit, +} from './actions'; +import { + setUebaDetailsQueriesActivePageToZero, + setUebaPageQueriesActivePageToZero, +} from './helpers'; +import { UebaTableType, UebaModel } from './model'; + +export const initialUebaState: UebaModel = { + page: { + queries: { + [UebaTableType.riskScore]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: RiskScoreFields.riskScore, + direction: Direction.desc, + }, + }, + }, + }, + details: { + queries: { + [UebaTableType.hostRules]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: HostRulesFields.riskScore, + direction: Direction.desc, + }, + }, + [UebaTableType.hostTactics]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: HostTacticsFields.riskScore, + direction: Direction.desc, + }, + }, + [UebaTableType.userRules]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: HostRulesFields.riskScore, // this looks wrong but its right, the user "table" is an array of host tables + direction: Direction.desc, + }, + }, + }, + }, +}; + +export const uebaReducer = reducerWithInitialState(initialUebaState) + .case(updateUebaTable, (state, { uebaType, tableType, updates }) => ({ + ...state, + [uebaType]: { + ...state[uebaType], + queries: { + ...state[uebaType].queries, + [tableType]: { + ...get([uebaType, 'queries', tableType], state), + ...updates, + }, + }, + }, + })) + .case(setUebaTablesActivePageToZero, (state) => ({ + ...state, + page: { + ...state.page, + queries: setUebaPageQueriesActivePageToZero(state), + }, + details: { + ...state.details, + queries: setUebaDetailsQueriesActivePageToZero(state), + }, + })) + .case(setUebaDetailsTablesActivePageToZero, (state) => ({ + ...state, + details: { + ...state.details, + queries: setUebaDetailsQueriesActivePageToZero(state), + }, + })) + .case(updateTableActivePage, (state, { activePage, uebaType, tableType }) => ({ + ...state, + [uebaType]: { + ...state[uebaType], + queries: { + ...state[uebaType].queries, + [tableType]: { + // TODO: Steph/ueba fix active page/limit on ueba tables. is broken because multiple UebaTableType.userRules tables + // @ts-ignore + ...state[uebaType].queries[tableType], + activePage, + }, + }, + }, + })) + .case(updateTableLimit, (state, { limit, uebaType, tableType }) => ({ + ...state, + [uebaType]: { + ...state[uebaType], + queries: { + ...state[uebaType].queries, + [tableType]: { + // TODO: Steph/ueba fix active page/limit on ueba tables. is broken because multiple UebaTableType.userRules tables + // @ts-ignore + ...state[uebaType].queries[tableType], + limit, + }, + }, + }, + })) + .build(); diff --git a/x-pack/plugins/security_solution/public/ueba/store/selectors.ts b/x-pack/plugins/security_solution/public/ueba/store/selectors.ts new file mode 100644 index 0000000000000..a3d7a5f8a8867 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/store/selectors.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createSelector } from 'reselect'; + +import { State } from '../../common/store/types'; + +import { UebaDetailsModel, UebaPageModel, UebaTableType } from './model'; + +const selectUebaPage = (state: State): UebaPageModel => state.ueba.page; +const selectUebaDetailsPage = (state: State): UebaDetailsModel => state.ueba.details; + +export const riskScoreSelector = () => + createSelector(selectUebaPage, (ueba) => ueba.queries[UebaTableType.riskScore]); + +export const hostRulesSelector = () => + createSelector(selectUebaDetailsPage, (ueba) => ueba.queries[UebaTableType.hostRules]); + +export const hostTacticsSelector = () => + createSelector(selectUebaDetailsPage, (ueba) => ueba.queries[UebaTableType.hostTactics]); + +export const userRulesSelector = () => + createSelector(selectUebaDetailsPage, (ueba) => ueba.queries[UebaTableType.userRules]); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts index 66ac744b3a50c..9071351ab541a 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts @@ -23,9 +23,8 @@ import { import { BulkInstallPackageInfo, BulkInstallPackagesResponse, - CreateFleetSetupResponse, IBulkInstallPackageHTTPError, - PostIngestSetupResponse, + PostFleetSetupResponse, } from '../../../fleet/common/types/rest_spec'; import { KbnClientWithApiKeySupport } from './kbn_client_with_api_key_support'; @@ -63,7 +62,7 @@ async function doIngestSetup(kbnClient: KbnClient) { const setupResponse = (await kbnClient.request({ path: SETUP_API_ROUTE, method: 'POST', - })) as AxiosResponse; + })) as AxiosResponse; if (!setupResponse.data.isInitialized) { console.error(setupResponse.data); @@ -79,7 +78,7 @@ async function doIngestSetup(kbnClient: KbnClient) { const setupResponse = (await kbnClient.request({ path: AGENTS_SETUP_API_ROUTES.CREATE_PATTERN, method: 'POST', - })) as AxiosResponse; + })) as AxiosResponse; if (!setupResponse.data.isInitialized) { console.error(setupResponse.data); diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index 8018a2f050fc3..a1c6601520a54 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -64,6 +64,12 @@ export const configSchema = schema.object({ * Artifacts Configuration */ packagerTaskInterval: schema.string({ defaultValue: '60s' }), + + /** + * Detection prebuilt rules + */ + prebuiltRulesFromFileSystem: schema.boolean({ defaultValue: true }), + prebuiltRulesFromSavedObjects: schema.boolean({ defaultValue: true }), }); export const createConfig = (context: PluginInitializerContext) => diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 3ab0e6179f842..a4d900c514190 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -94,6 +94,8 @@ export class EndpointAppContextService { this.manifestManager, dependencies.appClientFactory, dependencies.config.maxTimelineImportExportSize, + dependencies.config.prebuiltRulesFromFileSystem, + dependencies.config.prebuiltRulesFromSavedObjects, dependencies.security, dependencies.alerting, dependencies.licenseService, diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 1edcef6dec722..56c462de54c52 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -42,6 +42,8 @@ describe('ingest_integration tests ', () => { let ctx: SecuritySolutionRequestHandlerContext; const exceptionListClient: ExceptionListClient = getExceptionListClientMock(); const maxTimelineImportExportSize = createMockConfig().maxTimelineImportExportSize; + const prebuiltRulesFromFileSystem = createMockConfig().prebuiltRulesFromFileSystem; + const prebuiltRulesFromSavedObjects = createMockConfig().prebuiltRulesFromSavedObjects; let licenseEmitter: Subject; let licenseService: LicenseService; const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); @@ -80,6 +82,8 @@ describe('ingest_integration tests ', () => { manifestManager, endpointAppContextMock.appClientFactory, maxTimelineImportExportSize, + prebuiltRulesFromFileSystem, + prebuiltRulesFromSavedObjects, endpointAppContextMock.security, endpointAppContextMock.alerting, licenseService, diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index 9e1bb2f9b32b0..3e12fcac52a94 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -34,6 +34,8 @@ export const getPackagePolicyCreateCallback = ( manifestManager: ManifestManager, appClientFactory: AppClientFactory, maxTimelineImportExportSize: number, + prebuiltRulesFromFileSystem: boolean, + prebuiltRulesFromSavedObjects: boolean, securityStart: SecurityPluginStart, alerts: AlertsStartContract, licenseService: LicenseService, @@ -61,6 +63,8 @@ export const getPackagePolicyCreateCallback = ( securityStart, alerts, maxTimelineImportExportSize, + prebuiltRulesFromFileSystem, + prebuiltRulesFromSavedObjects, exceptionsClient, }), diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts index a387b7e3fdca5..d8adf4ea6a1ca 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts @@ -22,6 +22,8 @@ export interface InstallPrepackagedRulesProps { securityStart: SecurityPluginStart; alerts: AlertsStartContract; maxTimelineImportExportSize: number; + prebuiltRulesFromFileSystem: boolean; + prebuiltRulesFromSavedObjects: boolean; exceptionsClient: ExceptionListClient; } @@ -37,6 +39,8 @@ export const installPrepackagedRules = async ({ securityStart, alerts, maxTimelineImportExportSize, + prebuiltRulesFromFileSystem, + prebuiltRulesFromSavedObjects, exceptionsClient, }: InstallPrepackagedRulesProps): Promise => { // prep for detection rules creation @@ -67,9 +71,11 @@ export const installPrepackagedRules = async ({ // @ts-expect-error context, appClient, - alerts.getAlertsClientWithRequest(request), + alerts.getRulesClientWithRequest(request), frameworkRequest, maxTimelineImportExportSize, + prebuiltRulesFromFileSystem, + prebuiltRulesFromSavedObjects, exceptionsClient ); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.test.ts index f9244add61651..33721c055cb89 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.test.ts @@ -5,21 +5,21 @@ * 2.0. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { createNotifications } from './create_notifications'; describe('createNotifications', () => { - let alertsClient: ReturnType; + let rulesClient: ReturnType; beforeEach(() => { - alertsClient = alertsClientMock.create(); + rulesClient = rulesClientMock.create(); }); - it('calls the alertsClient with proper params', async () => { + it('calls the rulesClient with proper params', async () => { const ruleAlertId = 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd'; await createNotifications({ - alertsClient, + rulesClient, actions: [], ruleAlertId, enabled: true, @@ -27,7 +27,7 @@ describe('createNotifications', () => { name: '', }); - expect(alertsClient.create).toHaveBeenCalledWith( + expect(rulesClient.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ @@ -38,7 +38,7 @@ describe('createNotifications', () => { ); }); - it('calls the alertsClient with transformed actions', async () => { + it('calls the rulesClient with transformed actions', async () => { const action = { group: 'default', id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', @@ -46,7 +46,7 @@ describe('createNotifications', () => { action_type_id: '.slack', }; await createNotifications({ - alertsClient, + rulesClient, actions: [action], ruleAlertId: 'new-rule-id', enabled: true, @@ -54,7 +54,7 @@ describe('createNotifications', () => { name: '', }); - expect(alertsClient.create).toHaveBeenCalledWith( + expect(rulesClient.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ actions: expect.arrayContaining([ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts index c445c33566289..907976062b519 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts @@ -12,14 +12,14 @@ import { addTags } from './add_tags'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; export const createNotifications = async ({ - alertsClient, + rulesClient, actions, enabled, ruleAlertId, interval, name, }: CreateNotificationParams): Promise> => - alertsClient.create({ + rulesClient.create({ data: { name, tags: addTags([], ruleAlertId), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/delete_notifications.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/delete_notifications.test.ts index d277f04fb4826..9cd01df6bcdf3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/delete_notifications.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/delete_notifications.test.ts @@ -5,25 +5,25 @@ * 2.0. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { deleteNotifications } from './delete_notifications'; import { readNotifications } from './read_notifications'; jest.mock('./read_notifications'); describe('deleteNotifications', () => { - let alertsClient: ReturnType; + let rulesClient: ReturnType; const notificationId = 'notification-52128c15-0d1b-4716-a4c5-46997ac7f3bd'; const ruleAlertId = 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd'; beforeEach(() => { - alertsClient = alertsClientMock.create(); + rulesClient = rulesClientMock.create(); }); it('should return null if notification was not found', async () => { (readNotifications as jest.Mock).mockResolvedValue(null); const result = await deleteNotifications({ - alertsClient, + rulesClient, id: notificationId, ruleAlertId, }); @@ -31,18 +31,18 @@ describe('deleteNotifications', () => { expect(result).toBe(null); }); - it('should call alertsClient.delete if notification was found', async () => { + it('should call rulesClient.delete if notification was found', async () => { (readNotifications as jest.Mock).mockResolvedValue({ id: notificationId, }); const result = await deleteNotifications({ - alertsClient, + rulesClient, id: notificationId, ruleAlertId, }); - expect(alertsClient.delete).toHaveBeenCalledWith( + expect(rulesClient.delete).toHaveBeenCalledWith( expect.objectContaining({ id: notificationId, }) @@ -50,18 +50,18 @@ describe('deleteNotifications', () => { expect(result).toEqual({ id: notificationId }); }); - it('should call alertsClient.delete if notification.id was null', async () => { + it('should call rulesClient.delete if notification.id was null', async () => { (readNotifications as jest.Mock).mockResolvedValue({ id: null, }); const result = await deleteNotifications({ - alertsClient, + rulesClient, id: notificationId, ruleAlertId, }); - expect(alertsClient.delete).toHaveBeenCalledWith( + expect(rulesClient.delete).toHaveBeenCalledWith( expect.objectContaining({ id: notificationId, }) @@ -69,24 +69,24 @@ describe('deleteNotifications', () => { expect(result).toEqual({ id: null }); }); - it('should return null if alertsClient.delete rejects with 404 if notification.id was null', async () => { + it('should return null if rulesClient.delete rejects with 404 if notification.id was null', async () => { (readNotifications as jest.Mock).mockResolvedValue({ id: null, }); - alertsClient.delete.mockRejectedValue({ + rulesClient.delete.mockRejectedValue({ output: { statusCode: 404, }, }); const result = await deleteNotifications({ - alertsClient, + rulesClient, id: notificationId, ruleAlertId, }); - expect(alertsClient.delete).toHaveBeenCalledWith( + expect(rulesClient.delete).toHaveBeenCalledWith( expect.objectContaining({ id: notificationId, }) @@ -94,7 +94,7 @@ describe('deleteNotifications', () => { expect(result).toEqual(null); }); - it('should return error object if alertsClient.delete rejects with status different than 404 and if notification.id was null', async () => { + it('should return error object if rulesClient.delete rejects with status different than 404 and if notification.id was null', async () => { (readNotifications as jest.Mock).mockResolvedValue({ id: null, }); @@ -105,12 +105,12 @@ describe('deleteNotifications', () => { }, }; - alertsClient.delete.mockRejectedValue(errorObject); + rulesClient.delete.mockRejectedValue(errorObject); let errorResult; try { await deleteNotifications({ - alertsClient, + rulesClient, id: notificationId, ruleAlertId, }); @@ -118,7 +118,7 @@ describe('deleteNotifications', () => { errorResult = error; } - expect(alertsClient.delete).toHaveBeenCalledWith( + expect(rulesClient.delete).toHaveBeenCalledWith( expect.objectContaining({ id: notificationId, }) @@ -132,7 +132,7 @@ describe('deleteNotifications', () => { }); const result = await deleteNotifications({ - alertsClient, + rulesClient, id: undefined, ruleAlertId, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/delete_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/delete_notifications.ts index 09dc0044db02f..cf6812b7cacdc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/delete_notifications.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/delete_notifications.ts @@ -9,21 +9,21 @@ import { readNotifications } from './read_notifications'; import { DeleteNotificationParams } from './types'; export const deleteNotifications = async ({ - alertsClient, + rulesClient, id, ruleAlertId, }: DeleteNotificationParams) => { - const notification = await readNotifications({ alertsClient, id, ruleAlertId }); + const notification = await readNotifications({ rulesClient, id, ruleAlertId }); if (notification == null) { return null; } if (notification.id != null) { - await alertsClient.delete({ id: notification.id }); + await rulesClient.delete({ id: notification.id }); return notification; } else if (id != null) { try { - await alertsClient.delete({ id }); + await rulesClient.delete({ id }); return notification; } catch (err) { if (err.output.statusCode === 404) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/find_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/find_notifications.ts index 6f8b1de1d9325..1f3d4247a0ad9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/find_notifications.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/find_notifications.ts @@ -18,7 +18,7 @@ export const getFilter = (filter: string | null | undefined) => { }; export const findNotifications = async ({ - alertsClient, + rulesClient, perPage, page, fields, @@ -26,7 +26,7 @@ export const findNotifications = async ({ sortField, sortOrder, }: FindNotificationParams): Promise> => - alertsClient.find({ + rulesClient.find({ options: { fields, page, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/read_notifications.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/read_notifications.test.ts index 0ee09dd4074c8..0e87dc76bd1cf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/read_notifications.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/read_notifications.test.ts @@ -6,7 +6,7 @@ */ import { readNotifications } from './read_notifications'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getNotificationResult, getFindNotificationsResultWithSingleHit, @@ -23,18 +23,18 @@ class TestError extends Error { } describe('read_notifications', () => { - let alertsClient: ReturnType; + let rulesClient: ReturnType; beforeEach(() => { - alertsClient = alertsClientMock.create(); + rulesClient = rulesClientMock.create(); }); describe('readNotifications', () => { - test('should return the output from alertsClient if id is set but ruleAlertId is undefined', async () => { - alertsClient.get.mockResolvedValue(getNotificationResult()); + test('should return the output from rulesClient if id is set but ruleAlertId is undefined', async () => { + rulesClient.get.mockResolvedValue(getNotificationResult()); const rule = await readNotifications({ - alertsClient, + rulesClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleAlertId: undefined, }); @@ -44,10 +44,10 @@ describe('read_notifications', () => { const result = getNotificationResult(); // @ts-expect-error delete result.alertTypeId; - alertsClient.get.mockResolvedValue(result); + rulesClient.get.mockResolvedValue(result); const rule = await readNotifications({ - alertsClient, + rulesClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleAlertId: undefined, }); @@ -55,12 +55,12 @@ describe('read_notifications', () => { }); test('should return error if alerts client throws 404 error on get', async () => { - alertsClient.get.mockImplementation(() => { + rulesClient.get.mockImplementation(() => { throw new TestError(); }); const rule = await readNotifications({ - alertsClient, + rulesClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleAlertId: undefined, }); @@ -68,12 +68,12 @@ describe('read_notifications', () => { }); test('should return error if alerts client throws error on get', async () => { - alertsClient.get.mockImplementation(() => { + rulesClient.get.mockImplementation(() => { throw new Error('Test error'); }); try { await readNotifications({ - alertsClient, + rulesClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleAlertId: undefined, }); @@ -82,47 +82,47 @@ describe('read_notifications', () => { } }); - test('should return the output from alertsClient if id is set but ruleAlertId is null', async () => { - alertsClient.get.mockResolvedValue(getNotificationResult()); + test('should return the output from rulesClient if id is set but ruleAlertId is null', async () => { + rulesClient.get.mockResolvedValue(getNotificationResult()); const rule = await readNotifications({ - alertsClient, + rulesClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleAlertId: null, }); expect(rule).toEqual(getNotificationResult()); }); - test('should return the output from alertsClient if id is undefined but ruleAlertId is set', async () => { - alertsClient.get.mockResolvedValue(getNotificationResult()); - alertsClient.find.mockResolvedValue(getFindNotificationsResultWithSingleHit()); + test('should return the output from rulesClient if id is undefined but ruleAlertId is set', async () => { + rulesClient.get.mockResolvedValue(getNotificationResult()); + rulesClient.find.mockResolvedValue(getFindNotificationsResultWithSingleHit()); const rule = await readNotifications({ - alertsClient, + rulesClient, id: undefined, ruleAlertId: 'rule-1', }); expect(rule).toEqual(getNotificationResult()); }); - test('should return null if the output from alertsClient with ruleAlertId set is empty', async () => { - alertsClient.get.mockResolvedValue(getNotificationResult()); - alertsClient.find.mockResolvedValue({ data: [], page: 0, perPage: 1, total: 0 }); + test('should return null if the output from rulesClient with ruleAlertId set is empty', async () => { + rulesClient.get.mockResolvedValue(getNotificationResult()); + rulesClient.find.mockResolvedValue({ data: [], page: 0, perPage: 1, total: 0 }); const rule = await readNotifications({ - alertsClient, + rulesClient, id: undefined, ruleAlertId: 'rule-1', }); expect(rule).toEqual(null); }); - test('should return the output from alertsClient if id is null but ruleAlertId is set', async () => { - alertsClient.get.mockResolvedValue(getNotificationResult()); - alertsClient.find.mockResolvedValue(getFindNotificationsResultWithSingleHit()); + test('should return the output from rulesClient if id is null but ruleAlertId is set', async () => { + rulesClient.get.mockResolvedValue(getNotificationResult()); + rulesClient.find.mockResolvedValue(getFindNotificationsResultWithSingleHit()); const rule = await readNotifications({ - alertsClient, + rulesClient, id: null, ruleAlertId: 'rule-1', }); @@ -130,11 +130,11 @@ describe('read_notifications', () => { }); test('should return null if id and ruleAlertId are null', async () => { - alertsClient.get.mockResolvedValue(getNotificationResult()); - alertsClient.find.mockResolvedValue(getFindNotificationsResultWithSingleHit()); + rulesClient.get.mockResolvedValue(getNotificationResult()); + rulesClient.find.mockResolvedValue(getFindNotificationsResultWithSingleHit()); const rule = await readNotifications({ - alertsClient, + rulesClient, id: null, ruleAlertId: null, }); @@ -142,11 +142,11 @@ describe('read_notifications', () => { }); test('should return null if id and ruleAlertId are undefined', async () => { - alertsClient.get.mockResolvedValue(getNotificationResult()); - alertsClient.find.mockResolvedValue(getFindNotificationsResultWithSingleHit()); + rulesClient.get.mockResolvedValue(getNotificationResult()); + rulesClient.find.mockResolvedValue(getFindNotificationsResultWithSingleHit()); const rule = await readNotifications({ - alertsClient, + rulesClient, id: undefined, ruleAlertId: undefined, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/read_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/read_notifications.ts index 2f14f36fc5e0a..a31281821d2d7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/read_notifications.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/read_notifications.ts @@ -11,13 +11,13 @@ import { findNotifications } from './find_notifications'; import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants'; export const readNotifications = async ({ - alertsClient, + rulesClient, id, ruleAlertId, }: ReadNotificationParams): Promise | null> => { if (id != null) { try { - const notification = await alertsClient.get({ id }); + const notification = await rulesClient.get({ id }); if (isAlertType(notification)) { return notification; } else { @@ -33,7 +33,7 @@ export const readNotifications = async ({ } } else if (ruleAlertId != null) { const notificationFromFind = await findNotifications({ - alertsClient, + rulesClient, filter: `alert.attributes.tags: "${INTERNAL_RULE_ALERT_ID_KEY}:${ruleAlertId}"`, page: 1, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/types.ts index 50ad98865544e..fb3eb715368e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/types.ts @@ -6,7 +6,7 @@ */ import { - AlertsClient, + RulesClient, PartialAlert, AlertType, AlertTypeParams, @@ -25,7 +25,7 @@ export interface RuleNotificationAlertTypeParams extends AlertTypeParams { export type RuleNotificationAlertType = Alert; export interface FindNotificationParams { - alertsClient: AlertsClient; + rulesClient: RulesClient; perPage?: number; page?: number; sortField?: string; @@ -45,7 +45,7 @@ export interface FindNotificationsRequestParams { } export interface Clients { - alertsClient: AlertsClient; + rulesClient: RulesClient; } export type UpdateNotificationParams = Omit< @@ -73,7 +73,7 @@ export interface NotificationAlertParams { export type CreateNotificationParams = NotificationAlertParams & Clients; export interface ReadNotificationParams { - alertsClient: AlertsClient; + rulesClient: RulesClient; id?: string | null; ruleAlertId?: string | null; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/update_notifications.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/update_notifications.test.ts index 0d4af8cd029a0..a2a858b552c0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/update_notifications.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/update_notifications.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { updateNotifications } from './update_notifications'; import { readNotifications } from './read_notifications'; import { createNotifications } from './create_notifications'; @@ -16,17 +16,17 @@ jest.mock('./create_notifications'); describe('updateNotifications', () => { const notification = getNotificationResult(); - let alertsClient: ReturnType; + let rulesClient: ReturnType; beforeEach(() => { - alertsClient = alertsClientMock.create(); + rulesClient = rulesClientMock.create(); }); it('should update the existing notification if interval provided', async () => { (readNotifications as jest.Mock).mockResolvedValue(notification); await updateNotifications({ - alertsClient, + rulesClient, actions: [], ruleAlertId: 'new-rule-id', enabled: true, @@ -34,7 +34,7 @@ describe('updateNotifications', () => { name: '', }); - expect(alertsClient.update).toHaveBeenCalledWith( + expect(rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ id: notification.id, data: expect.objectContaining({ @@ -50,7 +50,7 @@ describe('updateNotifications', () => { (readNotifications as jest.Mock).mockResolvedValue(null); const params: UpdateNotificationParams = { - alertsClient, + rulesClient, actions: [], ruleAlertId: 'new-rule-id', enabled: true, @@ -67,7 +67,7 @@ describe('updateNotifications', () => { (readNotifications as jest.Mock).mockResolvedValue(notification); await updateNotifications({ - alertsClient, + rulesClient, actions: [], ruleAlertId: 'new-rule-id', enabled: true, @@ -75,14 +75,14 @@ describe('updateNotifications', () => { name: '', }); - expect(alertsClient.delete).toHaveBeenCalledWith( + expect(rulesClient.delete).toHaveBeenCalledWith( expect.objectContaining({ id: notification.id, }) ); }); - it('should call the alertsClient with transformed actions', async () => { + it('should call the rulesClient with transformed actions', async () => { (readNotifications as jest.Mock).mockResolvedValue(notification); const action = { group: 'default', @@ -91,7 +91,7 @@ describe('updateNotifications', () => { action_type_id: '.slack', }; await updateNotifications({ - alertsClient, + rulesClient, actions: [action], ruleAlertId: 'new-rule-id', enabled: true, @@ -99,7 +99,7 @@ describe('updateNotifications', () => { name: '', }); - expect(alertsClient.update).toHaveBeenCalledWith( + expect(rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ actions: expect.arrayContaining([ @@ -120,7 +120,7 @@ describe('updateNotifications', () => { const ruleAlertId = 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd'; const result = await updateNotifications({ - alertsClient, + rulesClient, actions: [], enabled: true, ruleAlertId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/update_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/update_notifications.ts index 40c2845abb607..a568bfbc608e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/update_notifications.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/update_notifications.ts @@ -13,17 +13,17 @@ import { createNotifications } from './create_notifications'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; export const updateNotifications = async ({ - alertsClient, + rulesClient, actions, enabled, ruleAlertId, name, interval, }: UpdateNotificationParams): Promise | null> => { - const notification = await readNotifications({ alertsClient, id: undefined, ruleAlertId }); + const notification = await readNotifications({ rulesClient, id: undefined, ruleAlertId }); if (interval && notification) { - return alertsClient.update({ + return rulesClient.update({ id: notification.id, data: { tags: addTags([], ruleAlertId), @@ -41,7 +41,7 @@ export const updateNotifications = async ({ }); } else if (interval && !notification) { return createNotifications({ - alertsClient, + rulesClient, enabled, name, interval, @@ -49,7 +49,7 @@ export const updateNotifications = async ({ ruleAlertId, }); } else if (!interval && notification) { - await alertsClient.delete({ id: notification.id }); + await rulesClient.delete({ id: notification.id }); return null; } else { return null; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts index 4ca9448f5e3c7..d4ce280e73268 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts @@ -10,8 +10,9 @@ import { schema } from '@kbn/config-schema'; import { Logger } from '@kbn/logging'; import { ESSearchRequest } from 'src/core/types/elasticsearch'; -import { buildEsQuery, IIndexPattern } from '../../../../../../../src/plugins/data/common'; +import { buildEsQuery } from '@kbn/es-query'; +import type { IIndexPattern } from 'src/plugins/data/public'; import { RuleDataClient, createPersistenceRuleTypeFactory, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts index f376b353531c3..a768273c9d147 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts @@ -26,6 +26,8 @@ export const createMockConfig = (): ConfigType => ({ endpointResultListDefaultPageSize: 10, packagerTaskInterval: '60s', alertMergeStrategy: 'missingFields', + prebuiltRulesFromFileSystem: true, + prebuiltRulesFromSavedObjects: false, }); export const mockGetCurrentUser = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index 2e33200ee7390..53c7f9d1fbb11 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -11,12 +11,12 @@ import { elasticsearchServiceMock, savedObjectsClientMock, } from '../../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../../alerting/server/mocks'; import { licensingMock } from '../../../../../../licensing/server/mocks'; import { siemMock } from '../../../../mocks'; const createMockClients = () => ({ - alertsClient: alertsClientMock.create(), + rulesClient: rulesClientMock.create(), licensing: { license: licensingMock.createLicenseMock() }, clusterClient: elasticsearchServiceMock.createScopedClusterClient(), savedObjectsClient: savedObjectsClientMock.create(), @@ -47,7 +47,7 @@ const createRequestContextMock = ( ): SecuritySolutionRequestHandlerContextMock => { const coreContext = coreMock.createRequestHandlerContext(); return ({ - alerting: { getAlertsClient: jest.fn(() => clients.alertsClient) }, + alerting: { getRulesClient: jest.fn(() => clients.rulesClient) }, core: { ...coreContext, elasticsearch: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 8987bc9b6f0c0..102d799984d15 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -89,7 +89,7 @@ describe('add_prepackaged_rules_route', () => { mockExceptionsClient = listMock.getExceptionListClient(); - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); (installPrepackagedTimelines as jest.Mock).mockReset(); (installPrepackagedTimelines as jest.Mock).mockResolvedValue({ @@ -107,15 +107,15 @@ describe('add_prepackaged_rules_route', () => { }); describe('status codes', () => { - test('returns 200 when creating with a valid actionClient and alertClient', async () => { + test('returns 200 when creating with a valid actionClient and rulesClient', async () => { const request = addPrepackagedRulesRequest(); const response = await server.inject(request, context); expect(response.status).toEqual(200); }); - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + test('returns 404 if rulesClient is not available on the route', async () => { + context.alerting!.getRulesClient = jest.fn(); const request = addPrepackagedRulesRequest(); const response = await server.inject(request, context); @@ -156,7 +156,7 @@ describe('add_prepackaged_rules_route', () => { describe('responses', () => { test('1 rule is installed and 0 are updated when find results are empty', async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); const request = addPrepackagedRulesRequest(); const response = await server.inject(request, context); @@ -292,13 +292,16 @@ describe('add_prepackaged_rules_route', () => { getExceptionListClient: jest.fn(), getListClient: jest.fn(), }; + const config = createMockConfig(); await createPrepackagedRules( context, siemMockClient, - clients.alertsClient, + clients.rulesClient, {} as FrameworkRequest, 1200, + config.prebuiltRulesFromFileSystem, + config.prebuiltRulesFromSavedObjects, mockExceptionsClient ); @@ -308,13 +311,16 @@ describe('add_prepackaged_rules_route', () => { test('uses passed in exceptions list client when lists client not available in context', async () => { const { lists, ...myContext } = context; + const config = createMockConfig(); await createPrepackagedRules( myContext, siemMockClient, - clients.alertsClient, + clients.rulesClient, {} as FrameworkRequest, 1200, + config.prebuiltRulesFromFileSystem, + config.prebuiltRulesFromSavedObjects, mockExceptionsClient ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 03d357ab10bb9..48a847474eeed 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -34,7 +34,7 @@ import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackage import { ruleAssetSavedObjectsClientFactory } from '../../rules/rule_asset_saved_objects_client'; import { buildSiemResponse } from '../utils'; -import { AlertsClient } from '../../../../../../alerting/server'; +import { RulesClient } from '../../../../../../alerting/server'; import { FrameworkRequest } from '../../../framework'; import { ExceptionListClient } from '../../../../../../lists/server'; @@ -65,19 +65,21 @@ export const addPrepackedRulesRoute = ( const frameworkRequest = await buildFrameworkRequest(context, security, _); try { - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const siemClient = context.securitySolution?.getAppClient(); - if (!siemClient || !alertsClient) { + if (!siemClient || !rulesClient) { return siemResponse.error({ statusCode: 404 }); } const validated = await createPrepackagedRules( context, siemClient, - alertsClient, + rulesClient, frameworkRequest, - config.maxTimelineImportExportSize + config.maxTimelineImportExportSize, + config.prebuiltRulesFromFileSystem, + config.prebuiltRulesFromSavedObjects ); return response.ok({ body: validated ?? {} }); } catch (err) { @@ -102,9 +104,11 @@ class PrepackagedRulesError extends Error { export const createPrepackagedRules = async ( context: SecuritySolutionRequestHandlerContext, siemClient: AppClient, - alertsClient: AlertsClient, + rulesClient: RulesClient, frameworkRequest: FrameworkRequest, - maxTimelineImportExportSize: number, + maxTimelineImportExportSize: ConfigType['maxTimelineImportExportSize'], + prebuiltRulesFromFileSystem: ConfigType['prebuiltRulesFromFileSystem'], + prebuiltRulesFromSavedObjects: ConfigType['prebuiltRulesFromSavedObjects'], exceptionsClient?: ExceptionListClient ): Promise => { const esClient = context.core.elasticsearch.client; @@ -112,7 +116,7 @@ export const createPrepackagedRules = async ( const exceptionsListClient = context.lists != null ? context.lists.getExceptionListClient() : exceptionsClient; const ruleAssetsClient = ruleAssetSavedObjectsClientFactory(savedObjectsClient); - if (!siemClient || !alertsClient) { + if (!siemClient || !rulesClient) { throw new PrepackagedRulesError('', 404); } @@ -121,8 +125,12 @@ export const createPrepackagedRules = async ( await exceptionsListClient.createEndpointList(); } - const latestPrepackagedRules = await getLatestPrepackagedRules(ruleAssetsClient); - const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); + const latestPrepackagedRules = await getLatestPrepackagedRules( + ruleAssetsClient, + prebuiltRulesFromFileSystem, + prebuiltRulesFromSavedObjects + ); + const prepackagedRules = await getExistingPrepackagedRules({ rulesClient }); const rulesToInstall = getRulesToInstall(latestPrepackagedRules, prepackagedRules); const rulesToUpdate = getRulesToUpdate(latestPrepackagedRules, prepackagedRules); const signalsIndex = siemClient.getSignalsIndex(); @@ -136,7 +144,7 @@ export const createPrepackagedRules = async ( } } - await Promise.all(installPrepackagedRules(alertsClient, rulesToInstall, signalsIndex)); + await Promise.all(installPrepackagedRules(rulesClient, rulesToInstall, signalsIndex)); const timeline = await installPrepackagedTimelines( maxTimelineImportExportSize, frameworkRequest, @@ -146,7 +154,7 @@ export const createPrepackagedRules = async ( timeline, importTimelineResultSchema ); - await updatePrepackagedRules(alertsClient, savedObjectsClient, rulesToUpdate, signalsIndex); + await updatePrepackagedRules(rulesClient, savedObjectsClient, rulesToUpdate, signalsIndex); const prepackagedRulesOutput: PrePackagedRulesAndTimelinesSchema = { rules_installed: rulesToInstall.length, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index bbb753f1f62de..3de2770972c82 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -34,8 +34,8 @@ describe('create_rules_bulk', () => { ({ clients, context } = requestContextMock.createTools()); ml = mlServicesMock.createSetupContract(); - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no existing rules - clients.alertsClient.create.mockResolvedValue(getAlertMock(getQueryRuleParams())); // successful creation + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); // no existing rules + clients.rulesClient.create.mockResolvedValue(getAlertMock(getQueryRuleParams())); // successful creation context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) @@ -50,7 +50,7 @@ describe('create_rules_bulk', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getReadBulkRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); @@ -105,7 +105,7 @@ describe('create_rules_bulk', () => { }); test('returns a duplicate error if rule_id already exists', async () => { - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); const response = await server.inject(getReadBulkRequest(), context); expect(response.status).toEqual(200); @@ -120,7 +120,7 @@ describe('create_rules_bulk', () => { }); test('catches error if creation throws', async () => { - clients.alertsClient.create.mockImplementation(async () => { + clients.rulesClient.create.mockImplementation(async () => { throw new Error('Test error'); }); const response = await server.inject(getReadBulkRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 500c74e47ea7d..447da0f20a657 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -40,12 +40,12 @@ export const createRulesBulkRoute = ( }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const esClient = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution?.getAppClient(); - if (!siemClient || !alertsClient) { + if (!siemClient || !rulesClient) { return siemResponse.error({ statusCode: 404 }); } @@ -65,7 +65,7 @@ export const createRulesBulkRoute = ( .map(async (payloadRule) => { if (payloadRule.rule_id != null) { const rule = await readRules({ - alertsClient, + rulesClient, ruleId: payloadRule.rule_id, id: undefined, }); @@ -99,13 +99,13 @@ export const createRulesBulkRoute = ( }); } - const createdRule = await alertsClient.create({ + const createdRule = await rulesClient.create({ data: internalRule, }); const ruleActions = await updateRulesNotifications({ ruleAlertId: createdRule.id, - alertsClient, + rulesClient, savedObjectsClient, enabled: createdRule.enabled, actions: payloadRule.actions, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 6b0b01a9a9de9..25bb7f2bf0d0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -36,8 +36,8 @@ describe('create_rules', () => { ({ clients, context } = requestContextMock.createTools()); ml = mlServicesMock.createSetupContract(); - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no current rules - clients.alertsClient.create.mockResolvedValue(getAlertMock(getQueryRuleParams())); // creation succeeds + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); // no current rules + clients.rulesClient.create.mockResolvedValue(getAlertMock(getQueryRuleParams())); // creation succeeds clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // needed to transform context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( @@ -59,7 +59,7 @@ describe('create_rules', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getCreateRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); @@ -118,7 +118,7 @@ describe('create_rules', () => { }); test('returns a duplicate error if rule_id already exists', async () => { - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); const response = await server.inject(getCreateRequest(), context); expect(response.status).toEqual(409); @@ -129,7 +129,7 @@ describe('create_rules', () => { }); test('catches error if creation throws', async () => { - clients.alertsClient.create.mockImplementation(async () => { + clients.rulesClient.create.mockImplementation(async () => { throw new Error('Test error'); }); const response = await server.inject(getCreateRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 4b78586ba739b..6f4cf633e5fdd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -45,18 +45,18 @@ export const createRulesRoute = ( return siemResponse.error({ statusCode: 400, body: validationErrors }); } try { - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const esClient = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution?.getAppClient(); - if (!siemClient || !alertsClient) { + if (!siemClient || !rulesClient) { return siemResponse.error({ statusCode: 404 }); } if (request.body.rule_id != null) { const rule = await readRules({ - alertsClient, + rulesClient, ruleId: request.body.rule_id, id: undefined, }); @@ -92,13 +92,13 @@ export const createRulesRoute = ( // This will create the endpoint list if it does not exist yet await context.lists?.getExceptionListClient().createEndpointList(); - const createdRule = await alertsClient.create({ + const createdRule = await rulesClient.create({ data: internalRule, }); const ruleActions = await updateRulesNotifications({ ruleAlertId: createdRule.id, - alertsClient, + rulesClient, savedObjectsClient, enabled: createdRule.enabled, actions: request.body.actions, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index 9084557962ba0..d37b0f5a685af 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -27,8 +27,8 @@ describe('delete_rules', () => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists - clients.alertsClient.delete.mockResolvedValue({}); // successful deletion + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists + clients.rulesClient.delete.mockResolvedValue({}); // successful deletion clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatusEmpty()); // rule status request deleteRulesBulkRoute(server.router); @@ -62,13 +62,13 @@ describe('delete_rules', () => { }); test('returns 200 because the error is in the payload when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); const response = await server.inject(getDeleteBulkRequest(), context); expect(response.status).toEqual(200); }); test('returns 404 in the payload when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); const response = await server.inject(getDeleteBulkRequest(), context); expect(response.status).toEqual(200); @@ -83,7 +83,7 @@ describe('delete_rules', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getDeleteBulkRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 3068521682f8f..403565debea08 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -50,10 +50,10 @@ export const deleteRulesBulkRoute = (router: SecuritySolutionPluginRouter) => { const handler: Handler = async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; - if (!alertsClient) { + if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); } @@ -73,7 +73,7 @@ export const deleteRulesBulkRoute = (router: SecuritySolutionPluginRouter) => { } try { - const rule = await readRules({ alertsClient, id, ruleId }); + const rule = await readRules({ rulesClient, id, ruleId }); if (!rule) { return getIdBulkError({ id, ruleId }); } @@ -84,7 +84,7 @@ export const deleteRulesBulkRoute = (router: SecuritySolutionPluginRouter) => { searchFields: ['alertId'], }); await deleteRules({ - alertsClient, + rulesClient, savedObjectsClient, ruleStatusClient, ruleStatuses, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index e820487dc0c5d..b64b14dc8cd0c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -26,7 +26,7 @@ describe('delete_rules', () => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); deleteRulesRoute(server.router); @@ -40,14 +40,14 @@ describe('delete_rules', () => { }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { - clients.alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); + clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); const response = await server.inject(getDeleteRequestById(), context); expect(response.status).toEqual(200); }); test('returns 404 when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); const response = await server.inject(getDeleteRequest(), context); expect(response.status).toEqual(404); @@ -58,7 +58,7 @@ describe('delete_rules', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getDeleteRequest(), context); expect(response.status).toEqual(404); @@ -66,7 +66,7 @@ describe('delete_rules', () => { }); test('catches error if deletion throws error', async () => { - clients.alertsClient.delete.mockImplementation(async () => { + clients.rulesClient.delete.mockImplementation(async () => { throw new Error('Test error'); }); const response = await server.inject(getDeleteRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 4a6b41230f799..02c22750439f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -48,15 +48,15 @@ export const deleteRulesRoute = ( try { const { id, rule_id: ruleId } = request.query; - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; - if (!alertsClient) { + if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); } const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); - const rule = await readRules({ alertsClient, id, ruleId }); + const rule = await readRules({ rulesClient, id, ruleId }); if (!rule) { const error = getIdError({ id, ruleId }); return siemResponse.error({ @@ -71,7 +71,7 @@ export const deleteRulesRoute = ( searchFields: ['alertId'], }); await deleteRules({ - alertsClient, + rulesClient, savedObjectsClient, ruleStatusClient, ruleStatuses, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts index cb1c1feba5295..022118859aa0b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -39,9 +39,9 @@ export const exportRulesRoute = (router: SecuritySolutionPluginRouter, config: C }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); - if (!alertsClient) { + if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); } @@ -53,7 +53,7 @@ export const exportRulesRoute = (router: SecuritySolutionPluginRouter, config: C body: `Can't export more than ${exportSizeLimit} rules`, }); } else { - const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient }); + const nonPackagedRulesCount = await getNonPackagedRulesCount({ rulesClient }); if (nonPackagedRulesCount > exportSizeLimit) { return siemResponse.error({ statusCode: 400, @@ -64,8 +64,8 @@ export const exportRulesRoute = (router: SecuritySolutionPluginRouter, config: C const exported = request.body?.objects != null - ? await getExportByObjectIds(alertsClient, request.body.objects) - : await getExportAll(alertsClient); + ? await getExportByObjectIds(rulesClient, request.body.objects) + : await getExportAll(rulesClient); const responseBody = request.query.exclude_export_details ? exported.rulesNdjson diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index 06f3ca83c4722..ed92c045ade29 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -25,8 +25,8 @@ describe('find_rules', () => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - clients.alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); clients.savedObjectsClient.find.mockResolvedValue(getFindBulkResultStatus()); findRulesRoute(server.router); @@ -39,14 +39,14 @@ describe('find_rules', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getFindRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); test('catches error if search throws error', async () => { - clients.alertsClient.find.mockImplementation(async () => { + clients.rulesClient.find.mockImplementation(async () => { throw new Error('Test error'); }); const response = await server.inject(getFindRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts index 428978fe1d820..32f18d816bacf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -46,16 +46,16 @@ export const findRulesRoute = ( try { const { query } = request; - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; - if (!alertsClient) { + if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); } const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await findRules({ - alertsClient, + rulesClient, perPage: query.per_page, page: query.page, sortField: query.sort_field, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts index 73f076649b72f..43e60b0eb5035 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts @@ -27,18 +27,18 @@ describe('find_statuses', () => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); clients.savedObjectsClient.find.mockResolvedValue(getFindBulkResultStatus()); // successful status search - clients.alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); + clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); findRulesStatusesRoute(server.router); }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when finding a single rule status with a valid alertsClient', async () => { + test('returns 200 when finding a single rule status with a valid rulesClient', async () => { const response = await server.inject(ruleStatusRequest(), context); expect(response.status).toEqual(200); }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(ruleStatusRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); @@ -70,7 +70,7 @@ describe('find_statuses', () => { }; // 1. getFailingRules api found a rule where the executionStatus was 'error' - clients.alertsClient.get.mockResolvedValue({ + clients.rulesClient.get.mockResolvedValue({ ...failingExecutionRule, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index aed8b80e4f133..1760d6bf7c18b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -40,10 +40,10 @@ export const findRulesStatusesRoute = (router: SecuritySolutionPluginRouter) => async (context, request, response) => { const { body } = request; const siemResponse = buildSiemResponse(response); - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; - if (!alertsClient) { + if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); } @@ -52,7 +52,7 @@ export const findRulesStatusesRoute = (router: SecuritySolutionPluginRouter) => const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const [statusesById, failingRules] = await Promise.all([ ruleStatusClient.findBulk(ids, 6), - getFailingRules(ids, alertsClient), + getFailingRules(ids, rulesClient), ]); const statuses = ids.reduce((acc, id) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts index f88da36db4491..61c618dc4d5e6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts @@ -73,7 +73,7 @@ describe('get_prepackaged_rule_status_route', () => { authz: {}, } as unknown) as SecurityPluginSetup; - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); getPrepackagedRulesStatusRoute(server.router, createMockConfig(), securitySetup); }); @@ -85,14 +85,14 @@ describe('get_prepackaged_rule_status_route', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getPrepackagedRulesStatusRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); test('catch error when finding rules throws error', async () => { - clients.alertsClient.find.mockImplementation(async () => { + clients.rulesClient.find.mockImplementation(async () => { throw new Error('Test error'); }); const response = await server.inject(getPrepackagedRulesStatusRequest(), context); @@ -106,7 +106,7 @@ describe('get_prepackaged_rule_status_route', () => { describe('responses', () => { test('0 rules installed, 0 custom rules, 1 rules not installed, and 1 rule not updated', async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); const request = getPrepackagedRulesStatusRequest(); const response = await server.inject(request, context); @@ -123,7 +123,7 @@ describe('get_prepackaged_rule_status_route', () => { }); test('1 rule installed, 1 custom rules, 0 rules not installed, and 1 rule to not updated', async () => { - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); const request = getPrepackagedRulesStatusRequest(); const response = await server.inject(request, context); @@ -140,7 +140,7 @@ describe('get_prepackaged_rule_status_route', () => { }); test('0 timelines installed, 3 timelines not installed, 0 timelines not updated', async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); (checkTimelinesStatus as jest.Mock).mockResolvedValue( mockCheckTimelinesStatusBeforeInstallResult ); @@ -160,7 +160,7 @@ describe('get_prepackaged_rule_status_route', () => { }); test('3 timelines installed, 0 timelines not installed, 0 timelines not updated', async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); (checkTimelinesStatus as jest.Mock).mockResolvedValue( mockCheckTimelinesStatusAfterInstallResult ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index cd02cc72ba40c..38c315462bf55 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -45,17 +45,21 @@ export const getPrepackagedRulesStatusRoute = ( async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; const siemResponse = buildSiemResponse(response); - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const ruleAssetsClient = ruleAssetSavedObjectsClientFactory(savedObjectsClient); - if (!alertsClient) { + if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); } try { - const latestPrepackagedRules = await getLatestPrepackagedRules(ruleAssetsClient); + const latestPrepackagedRules = await getLatestPrepackagedRules( + ruleAssetsClient, + config.prebuiltRulesFromFileSystem, + config.prebuiltRulesFromSavedObjects + ); const customRules = await findRules({ - alertsClient, + rulesClient, perPage: 1, page: 1, sortField: 'enabled', @@ -64,7 +68,7 @@ export const getPrepackagedRulesStatusRoute = ( fields: undefined, }); const frameworkRequest = await buildFrameworkRequest(context, security, request); - const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); + const prepackagedRules = await getExistingPrepackagedRules({ rulesClient }); const rulesToInstall = getRulesToInstall(latestPrepackagedRules, prepackagedRules); const rulesToUpdate = getRulesToUpdate(latestPrepackagedRules, prepackagedRules); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index ab9e6983590c9..210a065012d03 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -44,7 +44,7 @@ describe('import_rules_route', () => { request = getImportRulesRequest(hapiStream); ml = mlServicesMock.createSetupContract(); - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no extant rules + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); // no extant rules context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) @@ -72,7 +72,7 @@ describe('import_rules_route', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(request, context); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); @@ -166,7 +166,7 @@ describe('import_rules_route', () => { describe('single rule import', () => { test('returns 200 if rule imported successfully', async () => { - clients.alertsClient.create.mockResolvedValue(getAlertMock(getQueryRuleParams())); + clients.rulesClient.create.mockResolvedValue(getAlertMock(getQueryRuleParams())); const response = await server.inject(request, context); expect(response.status).toEqual(200); expect(response.body).toEqual({ @@ -199,7 +199,7 @@ describe('import_rules_route', () => { describe('rule with existing rule_id', () => { test('returns with reported conflict if `overwrite` is set to `false`', async () => { - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // extant rule + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // extant rule const response = await server.inject(request, context); expect(response.status).toEqual(200); @@ -219,7 +219,7 @@ describe('import_rules_route', () => { }); test('returns with NO reported conflict if `overwrite` is set to `true`', async () => { - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // extant rule + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // extant rule const overwriteRequest = getImportRulesRequestOverwriteTrue( buildHapiStream(ruleIdsToNdJsonString(['rule-1'])) ); @@ -339,7 +339,7 @@ describe('import_rules_route', () => { describe('rules with existing rule_id', () => { beforeEach(() => { - clients.alertsClient.find.mockResolvedValueOnce(getFindResultWithSingleHit()); // extant rule + clients.rulesClient.find.mockResolvedValueOnce(getFindResultWithSingleHit()); // extant rule }); test('returns with reported conflict if `overwrite` is set to `false`', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 8e322405280d3..b4a7fc4ac554f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -76,12 +76,12 @@ export const importRulesRoute = ( const siemResponse = buildSiemResponse(response); try { - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const esClient = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution?.getAppClient(); - if (!siemClient || !alertsClient) { + if (!siemClient || !rulesClient) { return siemResponse.error({ statusCode: 404 }); } @@ -200,10 +200,10 @@ export const importRulesRoute = ( throwHttpError(await mlAuthz.validateRuleType(type)); - const rule = await readRules({ alertsClient, ruleId, id: undefined }); + const rule = await readRules({ rulesClient, ruleId, id: undefined }); if (rule == null) { await createRules({ - alertsClient, + rulesClient, anomalyThreshold, author, buildingBlockType, @@ -256,7 +256,7 @@ export const importRulesRoute = ( resolve({ rule_id: ruleId, status_code: 200 }); } else if (rule != null && request.query.overwrite) { await patchRules({ - alertsClient, + rulesClient, author, buildingBlockType, savedObjectsClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index b6dd8a3fe0431..31f805c563f76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -32,8 +32,8 @@ describe('patch_rules_bulk', () => { ({ clients, context } = requestContextMock.createTools()); ml = mlServicesMock.createSetupContract(); - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists - clients.alertsClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); // update succeeds + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists + clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); // update succeeds patchRulesBulkRoute(server.router, ml); }); @@ -45,7 +45,7 @@ describe('patch_rules_bulk', () => { }); test('returns an error in the response when updating a single rule that does not exist', async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); const response = await server.inject(getPatchBulkRequest(), context); expect(response.status).toEqual(200); expect(response.body).toEqual([ @@ -71,7 +71,7 @@ describe('patch_rules_bulk', () => { }); await server.inject(request, context); - expect(clients.alertsClient.update).toHaveBeenCalledWith( + expect(clients.rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ @@ -84,7 +84,7 @@ describe('patch_rules_bulk', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getPatchBulkRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 7eb01e8b0d402..6def864885dcd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -46,10 +46,10 @@ export const patchRulesBulkRoute = ( async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; - if (!alertsClient) { + if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); } @@ -123,7 +123,7 @@ export const patchRulesBulkRoute = ( throwHttpError(await mlAuthz.validateRuleType(type)); } - const existingRule = await readRules({ alertsClient, ruleId, id }); + const existingRule = await readRules({ rulesClient, ruleId, id }); if (existingRule?.params.type) { // reject an unauthorized modification of an ML rule throwHttpError(await mlAuthz.validateRuleType(existingRule?.params.type)); @@ -131,7 +131,7 @@ export const patchRulesBulkRoute = ( const rule = await patchRules({ rule: existingRule, - alertsClient, + rulesClient, author, buildingBlockType, description, @@ -182,7 +182,7 @@ export const patchRulesBulkRoute = ( if (rule != null && rule.enabled != null && rule.name != null) { const ruleActions = await updateRulesNotifications({ ruleAlertId: rule.id, - alertsClient, + rulesClient, savedObjectsClient, enabled: rule.enabled, actions, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index 9920ec5229a02..840763661f0bc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -34,9 +34,9 @@ describe('patch_rules', () => { ({ clients, context } = requestContextMock.createTools()); ml = mlServicesMock.createSetupContract(); - clients.alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); // existing rule - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule - clients.alertsClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); // successful update + clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); // existing rule + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule + clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); // successful update clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // successful transform patchRulesRoute(server.router, ml); @@ -49,7 +49,7 @@ describe('patch_rules', () => { }); test('returns 404 when updating a single rule that does not exist', async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); const response = await server.inject(getPatchRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ @@ -59,14 +59,14 @@ describe('patch_rules', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getPatchRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); test('returns error if requesting a non-rule', async () => { - clients.alertsClient.find.mockResolvedValue(nonRuleFindResult()); + clients.rulesClient.find.mockResolvedValue(nonRuleFindResult()); const response = await server.inject(getPatchRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ @@ -76,7 +76,7 @@ describe('patch_rules', () => { }); test('catches error if update throws error', async () => { - clients.alertsClient.update.mockImplementation(async () => { + clients.rulesClient.update.mockImplementation(async () => { throw new Error('Test error'); }); const response = await server.inject(getPatchRequest(), context); @@ -100,7 +100,7 @@ describe('patch_rules', () => { }); await server.inject(request, context); - expect(clients.alertsClient.update).toHaveBeenCalledWith( + expect(clients.rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts index eaaa44fcf1916..c6123a5ac53c8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -107,10 +107,10 @@ export const patchRulesRoute = ( const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; - if (!alertsClient) { + if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); } @@ -125,7 +125,7 @@ export const patchRulesRoute = ( throwHttpError(await mlAuthz.validateRuleType(type)); } - const existingRule = await readRules({ alertsClient, ruleId, id }); + const existingRule = await readRules({ rulesClient, ruleId, id }); if (existingRule?.params.type) { // reject an unauthorized modification of an ML rule throwHttpError(await mlAuthz.validateRuleType(existingRule?.params.type)); @@ -133,7 +133,7 @@ export const patchRulesRoute = ( const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await patchRules({ - alertsClient, + rulesClient, author, buildingBlockType, description, @@ -185,7 +185,7 @@ export const patchRulesRoute = ( if (rule != null && rule.enabled != null && rule.name != null) { const ruleActions = await updateRulesNotifications({ ruleAlertId: rule.id, - alertsClient, + rulesClient, savedObjectsClient, enabled: rule.enabled, actions, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts index 60677fd8eda90..1facd291eede4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts @@ -31,7 +31,7 @@ describe('perform_bulk_action', () => { ({ clients, context } = requestContextMock.createTools()); ml = mlServicesMock.createSetupContract(); - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); performBulkActionRoute(server.router, ml); @@ -45,14 +45,14 @@ describe('perform_bulk_action', () => { }); it("returns 200 when provided filter query doesn't match any rules", async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); const response = await server.inject(getBulkActionRequest(), context); expect(response.status).toEqual(200); expect(response.body).toEqual({ success: true, rules_count: 0 }); }); it('returns 400 when provided filter query matches too many rules', async () => { - clients.alertsClient.find.mockResolvedValue( + clients.rulesClient.find.mockResolvedValue( getFindResultWithMultiHits({ data: [], total: Infinity }) ); const response = await server.inject(getBulkActionRequest(), context); @@ -64,14 +64,14 @@ describe('perform_bulk_action', () => { }); it('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getBulkActionRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); it('catches error if disable throws error', async () => { - clients.alertsClient.disable.mockImplementation(async () => { + clients.rulesClient.disable.mockImplementation(async () => { throw new Error('Test error'); }); const response = await server.inject(getBulkActionRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts index 9d569acf3782a..ab6fc24c5fa76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts @@ -45,7 +45,7 @@ export const performBulkActionRoute = ( const siemResponse = buildSiemResponse(response); try { - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); @@ -56,12 +56,12 @@ export const performBulkActionRoute = ( savedObjectsClient, }); - if (!alertsClient) { + if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); } const rules = await findRules({ - alertsClient, + rulesClient, perPage: BULK_ACTION_RULES_LIMIT, filter: body.query !== '' ? body.query : undefined, page: undefined, @@ -83,7 +83,7 @@ export const performBulkActionRoute = ( rules.data.map(async (rule) => { if (!rule.enabled) { throwHttpError(await mlAuthz.validateRuleType(rule.params.type)); - await enableRule({ rule, alertsClient, savedObjectsClient }); + await enableRule({ rule, rulesClient, savedObjectsClient }); } }) ); @@ -93,7 +93,7 @@ export const performBulkActionRoute = ( rules.data.map(async (rule) => { if (rule.enabled) { throwHttpError(await mlAuthz.validateRuleType(rule.params.type)); - await alertsClient.disable({ id: rule.id }); + await rulesClient.disable({ id: rule.id }); } }) ); @@ -107,7 +107,7 @@ export const performBulkActionRoute = ( searchFields: ['alertId'], }); await deleteRules({ - alertsClient, + rulesClient, savedObjectsClient, ruleStatusClient, ruleStatuses, @@ -121,7 +121,7 @@ export const performBulkActionRoute = ( rules.data.map(async (rule) => { throwHttpError(await mlAuthz.validateRuleType(rule.params.type)); - const createdRule = await alertsClient.create({ + const createdRule = await rulesClient.create({ data: duplicateRule(rule), }); @@ -132,7 +132,7 @@ export const performBulkActionRoute = ( await updateRulesNotifications({ ruleAlertId: createdRule.id, - alertsClient, + rulesClient, savedObjectsClient, enabled: createdRule.enabled, actions: ruleActions?.actions || [], @@ -144,7 +144,7 @@ export const performBulkActionRoute = ( break; case BulkAction.export: const exported = await getExportByObjectIds( - alertsClient, + rulesClient, rules.data.map(({ params }) => ({ rule_id: params.ruleId })) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index bc3c282c9cdde..11043273eaef0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -24,7 +24,7 @@ describe('read_signals', () => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatusEmpty()); // successful transform readRulesRoute(server.router); @@ -37,14 +37,14 @@ describe('read_signals', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getReadRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); test('returns error if requesting a non-rule', async () => { - clients.alertsClient.find.mockResolvedValue(nonRuleFindResult()); + clients.rulesClient.find.mockResolvedValue(nonRuleFindResult()); const response = await server.inject(getReadRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ @@ -54,7 +54,7 @@ describe('read_signals', () => { }); test('catches error if search throws error', async () => { - clients.alertsClient.find.mockImplementation(async () => { + clients.rulesClient.find.mockImplementation(async () => { throw new Error('Test error'); }); const response = await server.inject(getReadRequest(), context); @@ -68,7 +68,7 @@ describe('read_signals', () => { describe('data validation', () => { test('returns 404 if given a non-existent id', async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); const request = requestMock.create({ method: 'get', path: DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts index 917da6c9708d5..ab618dc2a30e8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -47,17 +47,17 @@ export const readRulesRoute = ( const { id, rule_id: ruleId } = request.query; - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; try { - if (!alertsClient) { + if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); } const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await readRules({ - alertsClient, + rulesClient, id, ruleId, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index a57bed7a895f9..7851920adc1d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -34,8 +34,8 @@ describe('update_rules_bulk', () => { ({ clients, context } = requestContextMock.createTools()); ml = mlServicesMock.createSetupContract(); - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - clients.alertsClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); updateRulesBulkRoute(server.router, ml); @@ -48,7 +48,7 @@ describe('update_rules_bulk', () => { }); test('returns 200 as a response when updating a single rule that does not exist', async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); const expected: BulkError[] = [ { error: { message: 'rule_id: "rule-1" not found', status_code: 404 }, @@ -62,7 +62,7 @@ describe('update_rules_bulk', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getUpdateBulkRequest(), context); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); @@ -77,7 +77,7 @@ describe('update_rules_bulk', () => { }); test('returns an error if update throws', async () => { - clients.alertsClient.update.mockImplementation(() => { + clients.rulesClient.update.mockImplementation(() => { throw new Error('Test error'); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 4c59ae2ba442e..f7604ebcdc22f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -39,11 +39,11 @@ export const updateRulesBulkRoute = ( async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution?.getAppClient(); - if (!siemClient || !alertsClient) { + if (!siemClient || !rulesClient) { return siemResponse.error({ statusCode: 404 }); } @@ -71,7 +71,7 @@ export const updateRulesBulkRoute = ( throwHttpError(await mlAuthz.validateRuleType(payloadRule.type)); const rule = await updateRules({ - alertsClient, + rulesClient, savedObjectsClient, defaultOutputIndex: siemClient.getSignalsIndex(), ruleUpdate: payloadRule, @@ -79,7 +79,7 @@ export const updateRulesBulkRoute = ( if (rule != null) { const ruleActions = await updateRulesNotifications({ ruleAlertId: rule.id, - alertsClient, + rulesClient, savedObjectsClient, enabled: payloadRule.enabled ?? true, actions: payloadRule.actions, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index cf121d1610d39..60a91521bc766 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -36,9 +36,9 @@ describe('update_rules', () => { ({ clients, context } = requestContextMock.createTools()); ml = mlServicesMock.createSetupContract(); - clients.alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); // existing rule - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists - clients.alertsClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); // successful update + clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); // existing rule + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists + clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); // successful update clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatusEmpty()); // successful transform updateRulesRoute(server.router, ml); @@ -57,7 +57,7 @@ describe('update_rules', () => { }); test('returns 404 when updating a single rule that does not exist', async () => { - clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); const response = await server.inject(getUpdateRequest(), context); expect(response.status).toEqual(404); @@ -68,7 +68,7 @@ describe('update_rules', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - context.alerting!.getAlertsClient = jest.fn(); + context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getUpdateRequest(), context); expect(response.status).toEqual(404); @@ -84,7 +84,7 @@ describe('update_rules', () => { }); test('returns error when updating non-rule', async () => { - clients.alertsClient.find.mockResolvedValue(nonRuleFindResult()); + clients.rulesClient.find.mockResolvedValue(nonRuleFindResult()); const response = await server.inject(getUpdateRequest(), context); expect(response.status).toEqual(404); @@ -95,7 +95,7 @@ describe('update_rules', () => { }); test('catches error if search throws error', async () => { - clients.alertsClient.find.mockImplementation(async () => { + clients.rulesClient.find.mockImplementation(async () => { throw new Error('Test error'); }); const response = await server.inject(getUpdateRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index 0ff6cb3cd2d0f..a6f07c2f84d16 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -45,12 +45,12 @@ export const updateRulesRoute = ( return siemResponse.error({ statusCode: 400, body: validationErrors }); } try { - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution?.getAppClient(); const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); - if (!siemClient || !alertsClient) { + if (!siemClient || !rulesClient) { return siemResponse.error({ statusCode: 404 }); } @@ -63,7 +63,7 @@ export const updateRulesRoute = ( throwHttpError(await mlAuthz.validateRuleType(request.body.type)); const rule = await updateRules({ - alertsClient, + rulesClient, savedObjectsClient, defaultOutputIndex: siemClient.getSignalsIndex(), ruleUpdate: request.body, @@ -72,7 +72,7 @@ export const updateRulesRoute = ( if (rule != null) { const ruleActions = await updateRulesNotifications({ ruleAlertId: rule.id, - alertsClient, + rulesClient, savedObjectsClient, enabled: request.body.enabled ?? true, actions: request.body.actions ?? [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/tags/read_tags_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/tags/read_tags_route.ts index 817e4b95aabce..e22497071b2b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/tags/read_tags_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/tags/read_tags_route.ts @@ -23,15 +23,15 @@ export const readTagsRoute = (router: SecuritySolutionPluginRouter) => { }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const alertsClient = context.alerting?.getAlertsClient(); + const rulesClient = context.alerting?.getRulesClient(); - if (!alertsClient) { + if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); } try { const tags = await readTags({ - alertsClient, + rulesClient, }); return response.ok({ body: tags }); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts index ce7d4b3173370..83091c7e1f82e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts @@ -9,7 +9,7 @@ import Boom from '@hapi/boom'; import { SavedObjectsFindResponse } from 'kibana/server'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusSOAttributes } from '../rules/types'; import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { @@ -30,7 +30,7 @@ import { getAlertMock } from './__mocks__/request_responses'; import { AlertExecutionStatusErrorReasons } from '../../../../../alerting/common'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; -let alertsClient: ReturnType; +let rulesClient: ReturnType; describe('utils', () => { describe('transformBulkError', () => { @@ -386,11 +386,11 @@ describe('utils', () => { describe('getFailingRules', () => { beforeEach(() => { - alertsClient = alertsClientMock.create(); + rulesClient = rulesClientMock.create(); }); it('getFailingRules finds no failing rules', async () => { - alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); - const res = await getFailingRules(['my-fake-id'], alertsClient); + rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); + const res = await getFailingRules(['my-fake-id'], rulesClient); expect(res).toEqual({}); }); it('getFailingRules finds a failing rule', async () => { @@ -403,22 +403,22 @@ describe('utils', () => { message: 'oops', }, }; - alertsClient.get.mockResolvedValue(foundRule); - const res = await getFailingRules([foundRule.id], alertsClient); + rulesClient.get.mockResolvedValue(foundRule); + const res = await getFailingRules([foundRule.id], rulesClient); expect(res).toEqual({ [foundRule.id]: foundRule }); }); it('getFailingRules throws an error', async () => { - alertsClient.get.mockImplementation(() => { + rulesClient.get.mockImplementation(() => { throw new Error('my test error'); }); let error; try { - await getFailingRules(['my-fake-id'], alertsClient); + await getFailingRules(['my-fake-id'], rulesClient); } catch (exc) { error = exc; } expect(error.message).toEqual( - 'Failed to get executionStatus with AlertsClient: my test error' + 'Failed to get executionStatus with RulesClient: my test error' ); }); }); 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 9ff75726322a1..8f078b01daf73 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 @@ -15,7 +15,7 @@ import { KibanaResponseFactory, CustomHttpResponseOptions, } from '../../../../../../../src/core/server'; -import { AlertsClient } from '../../../../../alerting/server'; +import { RulesClient } from '../../../../../alerting/server'; import { RuleStatusResponse, IRuleStatusSOAttributes } from '../rules/types'; import { RuleParams } from '../schemas/rule_schemas'; @@ -304,12 +304,12 @@ export type GetFailingRulesResult = Record>; export const getFailingRules = async ( ids: string[], - alertsClient: AlertsClient + rulesClient: RulesClient ): Promise => { try { const errorRules = await Promise.all( ids.map(async (id) => - alertsClient.get({ + rulesClient.get({ id, }) ) @@ -328,6 +328,6 @@ export const getFailingRules = async ( if (Boom.isBoom(exc)) { throw exc; } - throw new Error(`Failed to get executionStatus with AlertsClient: ${exc.message}`); + throw new Error(`Failed to get executionStatus with RulesClient: ${exc.message}`); } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts index 86feed1f39cef..f7aae1564bb17 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts @@ -6,12 +6,12 @@ */ import { CreateRulesOptions } from './types'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({ author: ['Elastic'], buildingBlockType: undefined, - alertsClient: alertsClientMock.create(), + rulesClient: rulesClientMock.create(), anomalyThreshold: undefined, description: 'some description', enabled: true, @@ -63,7 +63,7 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({ author: ['Elastic'], buildingBlockType: undefined, - alertsClient: alertsClientMock.create(), + rulesClient: rulesClientMock.create(), anomalyThreshold: 55, description: 'some description', enabled: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.test.ts index c719412d27e4d..3dd29977a8f2c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.test.ts @@ -9,10 +9,10 @@ import { createRules } from './create_rules'; import { getCreateMlRulesOptionsMock } from './create_rules.mock'; describe('createRules', () => { - it('calls the alertsClient with legacy ML params', async () => { + it('calls the rulesClient with legacy ML params', async () => { const ruleOptions = getCreateMlRulesOptionsMock(); await createRules(ruleOptions); - expect(ruleOptions.alertsClient.create).toHaveBeenCalledWith( + expect(ruleOptions.rulesClient.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ @@ -24,13 +24,13 @@ describe('createRules', () => { ); }); - it('calls the alertsClient with ML params', async () => { + it('calls the rulesClient with ML params', async () => { const ruleOptions = { ...getCreateMlRulesOptionsMock(), machineLearningJobId: ['new_job_1', 'new_job_2'], }; await createRules(ruleOptions); - expect(ruleOptions.alertsClient.create).toHaveBeenCalledWith( + expect(ruleOptions.rulesClient.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts index db039bbc31390..c94cb39572ddc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts @@ -17,7 +17,7 @@ import { addTags } from './add_tags'; import { PartialFilter, RuleTypeParams } from '../types'; export const createRules = async ({ - alertsClient, + rulesClient, anomalyThreshold, author, buildingBlockType, @@ -67,7 +67,7 @@ export const createRules = async ({ exceptionsList, actions, }: CreateRulesOptions): Promise> => { - return alertsClient.create({ + return rulesClient.create({ data: { name, tags: addTags(tags, ruleId, immutable), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts index f581be9e1f62b..cd7bbfd9fced7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts @@ -6,7 +6,7 @@ */ import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { ruleStatusSavedObjectsClientMock } from '../signals/__mocks__/rule_status_saved_objects_client.mock'; import { deleteRules } from './delete_rules'; import { deleteNotifications } from '../notifications/delete_notifications'; @@ -18,12 +18,12 @@ jest.mock('../notifications/delete_notifications'); jest.mock('../rule_actions/delete_rule_actions_saved_object'); describe('deleteRules', () => { - let alertsClient: ReturnType; + let rulesClient: ReturnType; let ruleStatusClient: ReturnType; let savedObjectsClient: ReturnType; beforeEach(() => { - alertsClient = alertsClientMock.create(); + rulesClient = rulesClientMock.create(); savedObjectsClient = savedObjectsClientMock.create(); ruleStatusClient = ruleStatusSavedObjectsClientMock.create(); }); @@ -50,7 +50,7 @@ describe('deleteRules', () => { }; const rule = { - alertsClient, + rulesClient, savedObjectsClient, ruleStatusClient, id: 'ruleId', @@ -64,10 +64,10 @@ describe('deleteRules', () => { await deleteRules(rule); - expect(alertsClient.delete).toHaveBeenCalledWith({ id: rule.id }); + expect(rulesClient.delete).toHaveBeenCalledWith({ id: rule.id }); expect(deleteNotifications).toHaveBeenCalledWith({ ruleAlertId: rule.id, - alertsClient: expect.any(Object), + rulesClient: expect.any(Object), }); expect(deleteRuleActionsSavedObject).toHaveBeenCalledWith({ ruleAlertId: rule.id, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts index ed5477599253b..e1385eb05a6b4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts @@ -10,14 +10,14 @@ import { deleteRuleActionsSavedObject } from '../rule_actions/delete_rule_action import { DeleteRuleOptions } from './types'; export const deleteRules = async ({ - alertsClient, + rulesClient, savedObjectsClient, ruleStatusClient, ruleStatuses, id, }: DeleteRuleOptions) => { - await alertsClient.delete({ id }); - await deleteNotifications({ alertsClient, ruleAlertId: id }); + await rulesClient.delete({ id }); + await deleteNotifications({ rulesClient, ruleAlertId: id }); await deleteRuleActionsSavedObject({ ruleAlertId: id, savedObjectsClient }); ruleStatuses.saved_objects.forEach(async (obj) => ruleStatusClient.delete(obj.id)); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts index dc4cca2059b3e..1a81cf0cb5ebe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts @@ -7,13 +7,13 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { SanitizedAlert } from '../../../../../alerting/common'; -import { AlertsClient } from '../../../../../alerting/server'; +import { RulesClient } from '../../../../../alerting/server'; import { RuleParams } from '../schemas/rule_schemas'; import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; interface EnableRuleArgs { rule: SanitizedAlert; - alertsClient: AlertsClient; + rulesClient: RulesClient; savedObjectsClient: SavedObjectsClientContract; } @@ -21,11 +21,11 @@ interface EnableRuleArgs { * Enables the rule and updates its status to 'going to run' * * @param rule - rule to enable - * @param alertsClient - Alerts client + * @param rulesClient - Alerts client * @param savedObjectsClient - Saved Objects client */ -export const enableRule = async ({ rule, alertsClient, savedObjectsClient }: EnableRuleArgs) => { - await alertsClient.enable({ id: rule.id }); +export const enableRule = async ({ rule, rulesClient, savedObjectsClient }: EnableRuleArgs) => { + await rulesClient.enable({ id: rule.id }); const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const ruleCurrentStatus = await ruleStatusClient.find({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts index eae5ccd2f6ffd..e41dac066e18a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts @@ -19,7 +19,7 @@ export const getFilter = (filter: string | null | undefined) => { }; export const findRules = ({ - alertsClient, + rulesClient, perPage, page, fields, @@ -27,7 +27,7 @@ export const findRules = ({ sortField, sortOrder, }: FindRuleOptions): Promise> => { - return alertsClient.find({ + return rulesClient.find({ options: { fields, page, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts index da67bea0ca970..19a6a4e43d877 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getAlertMock, getFindResultWithSingleHit, @@ -27,14 +27,14 @@ describe('get_existing_prepackaged_rules', () => { describe('getExistingPrepackagedRules', () => { test('should return a single item in a single page', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const rules = await getExistingPrepackagedRules({ alertsClient }); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const rules = await getExistingPrepackagedRules({ rulesClient }); expect(rules).toEqual([getAlertMock(getQueryRuleParams())]); }); test('should return 3 items over 1 page with all on one page', async () => { - const alertsClient = alertsClientMock.create(); + const rulesClient = rulesClientMock.create(); const result1 = getAlertMock(getQueryRuleParams()); result1.params.immutable = true; @@ -49,7 +49,7 @@ describe('get_existing_prepackaged_rules', () => { result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; // first result mock which is for returning the total - alertsClient.find.mockResolvedValueOnce( + rulesClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1], perPage: 1, @@ -59,7 +59,7 @@ describe('get_existing_prepackaged_rules', () => { ); // second mock which will return all the data on a single page - alertsClient.find.mockResolvedValueOnce( + rulesClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2, result3], perPage: 3, @@ -68,21 +68,21 @@ describe('get_existing_prepackaged_rules', () => { }) ); - const rules = await getExistingPrepackagedRules({ alertsClient }); + const rules = await getExistingPrepackagedRules({ rulesClient }); expect(rules).toEqual([result1, result2, result3]); }); }); describe('getNonPackagedRules', () => { test('should return a single item in a single page', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const rules = await getNonPackagedRules({ alertsClient }); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const rules = await getNonPackagedRules({ rulesClient }); expect(rules).toEqual([getAlertMock(getQueryRuleParams())]); }); test('should return 2 items over 1 page', async () => { - const alertsClient = alertsClientMock.create(); + const rulesClient = rulesClientMock.create(); const result1 = getAlertMock(getQueryRuleParams()); result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; @@ -91,7 +91,7 @@ describe('get_existing_prepackaged_rules', () => { result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; // first result mock which is for returning the total - alertsClient.find.mockResolvedValueOnce( + rulesClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1], perPage: 1, @@ -101,16 +101,16 @@ describe('get_existing_prepackaged_rules', () => { ); // second mock which will return all the data on a single page - alertsClient.find.mockResolvedValueOnce( + rulesClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) ); - const rules = await getNonPackagedRules({ alertsClient }); + const rules = await getNonPackagedRules({ rulesClient }); expect(rules).toEqual([result1, result2]); }); test('should return 3 items over 1 page with all on one page', async () => { - const alertsClient = alertsClientMock.create(); + const rulesClient = rulesClientMock.create(); const result1 = getAlertMock(getQueryRuleParams()); result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; @@ -122,7 +122,7 @@ describe('get_existing_prepackaged_rules', () => { result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; // first result mock which is for returning the total - alertsClient.find.mockResolvedValueOnce( + rulesClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1], perPage: 3, @@ -132,7 +132,7 @@ describe('get_existing_prepackaged_rules', () => { ); // second mock which will return all the data on a single page - alertsClient.find.mockResolvedValueOnce( + rulesClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2, result3], perPage: 3, @@ -141,21 +141,21 @@ describe('get_existing_prepackaged_rules', () => { }) ); - const rules = await getNonPackagedRules({ alertsClient }); + const rules = await getNonPackagedRules({ rulesClient }); expect(rules).toEqual([result1, result2, result3]); }); }); describe('getRules', () => { test('should return a single item in a single page', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const rules = await getRules({ alertsClient, filter: '' }); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const rules = await getRules({ rulesClient, filter: '' }); expect(rules).toEqual([getAlertMock(getQueryRuleParams())]); }); test('should return 2 items over two pages, one per page', async () => { - const alertsClient = alertsClientMock.create(); + const rulesClient = rulesClientMock.create(); const result1 = getAlertMock(getQueryRuleParams()); result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; @@ -164,7 +164,7 @@ describe('get_existing_prepackaged_rules', () => { result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; // first result mock which is for returning the total - alertsClient.find.mockResolvedValueOnce( + rulesClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1], perPage: 1, @@ -174,29 +174,29 @@ describe('get_existing_prepackaged_rules', () => { ); // second mock which will return all the data on a single page - alertsClient.find.mockResolvedValueOnce( + rulesClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) ); - const rules = await getRules({ alertsClient, filter: '' }); + const rules = await getRules({ rulesClient, filter: '' }); expect(rules).toEqual([result1, result2]); }); }); describe('getRulesCount', () => { test('it returns a count', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const rules = await getRulesCount({ alertsClient, filter: '' }); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const rules = await getRulesCount({ rulesClient, filter: '' }); expect(rules).toEqual(1); }); }); describe('getNonPackagedRulesCount', () => { test('it returns a count', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const rules = await getNonPackagedRulesCount({ alertsClient }); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const rules = await getNonPackagedRulesCount({ rulesClient }); expect(rules).toEqual(1); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts index a8c1d7aeb8eb1..be8bf1303846d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts @@ -6,7 +6,7 @@ */ import { INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; -import { AlertsClient } from '../../../../../alerting/server'; +import { RulesClient } from '../../../../../alerting/server'; import { RuleAlertType, isAlertTypes } from './types'; import { findRules } from './find_rules'; @@ -14,22 +14,22 @@ export const FILTER_NON_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IM export const FILTER_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`; export const getNonPackagedRulesCount = async ({ - alertsClient, + rulesClient, }: { - alertsClient: AlertsClient; + rulesClient: RulesClient; }): Promise => { - return getRulesCount({ alertsClient, filter: FILTER_NON_PREPACKED_RULES }); + return getRulesCount({ rulesClient, filter: FILTER_NON_PREPACKED_RULES }); }; export const getRulesCount = async ({ - alertsClient, + rulesClient, filter, }: { - alertsClient: AlertsClient; + rulesClient: RulesClient; filter: string; }): Promise => { const firstRule = await findRules({ - alertsClient, + rulesClient, filter, perPage: 1, page: 1, @@ -41,15 +41,15 @@ export const getRulesCount = async ({ }; export const getRules = async ({ - alertsClient, + rulesClient, filter, }: { - alertsClient: AlertsClient; + rulesClient: RulesClient; filter: string; }): Promise => { - const count = await getRulesCount({ alertsClient, filter }); + const count = await getRulesCount({ rulesClient, filter }); const rules = await findRules({ - alertsClient, + rulesClient, filter, perPage: count, page: 1, @@ -68,23 +68,23 @@ export const getRules = async ({ }; export const getNonPackagedRules = async ({ - alertsClient, + rulesClient, }: { - alertsClient: AlertsClient; + rulesClient: RulesClient; }): Promise => { return getRules({ - alertsClient, + rulesClient, filter: FILTER_NON_PREPACKED_RULES, }); }; export const getExistingPrepackagedRules = async ({ - alertsClient, + rulesClient, }: { - alertsClient: AlertsClient; + rulesClient: RulesClient; }): Promise => { return getRules({ - alertsClient, + rulesClient, filter: FILTER_PREPACKED_RULES, }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts index 4c937b2e4ca8a..2870bee99e51a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts @@ -10,7 +10,7 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getExportAll } from './get_export_all'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock'; @@ -18,7 +18,7 @@ import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; describe('getExportAll', () => { test('it exports everything from the alerts client', async () => { - const alertsClient = alertsClientMock.create(); + const rulesClient = rulesClientMock.create(); const result = getFindResultWithSingleHit(); const alert = getAlertMock(getQueryRuleParams()); alert.params = { @@ -30,9 +30,9 @@ describe('getExportAll', () => { timelineTitle: 'some-timeline-title', }; result.data = [alert]; - alertsClient.find.mockResolvedValue(result); + rulesClient.find.mockResolvedValue(result); - const exports = await getExportAll(alertsClient); + const exports = await getExportAll(rulesClient); const rulesJson = JSON.parse(exports.rulesNdjson); const detailsJson = JSON.parse(exports.exportDetails); expect(rulesJson).toEqual({ @@ -84,7 +84,7 @@ describe('getExportAll', () => { }); test('it will export empty rules', async () => { - const alertsClient = alertsClientMock.create(); + const rulesClient = rulesClientMock.create(); const findResult: FindHit = { page: 1, perPage: 1, @@ -92,9 +92,9 @@ describe('getExportAll', () => { data: [], }; - alertsClient.find.mockResolvedValue(findResult); + rulesClient.find.mockResolvedValue(findResult); - const exports = await getExportAll(alertsClient); + const exports = await getExportAll(rulesClient); expect(exports).toEqual({ rulesNdjson: '', exportDetails: '{"exported_count":0,"missing_rules":[],"missing_rules_count":0}\n', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts index c0e893b8aea96..9ec51cf18c7c7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { AlertsClient } from '../../../../../alerting/server'; +import { RulesClient } from '../../../../../alerting/server'; import { getNonPackagedRules } from './get_existing_prepackaged_rules'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { transformAlertsToRules } from '../routes/rules/utils'; import { transformDataToNdjson } from '../../../utils/read_stream/create_stream_from_ndjson'; export const getExportAll = async ( - alertsClient: AlertsClient + rulesClient: RulesClient ): Promise<{ rulesNdjson: string; exportDetails: string; }> => { - const ruleAlertTypes = await getNonPackagedRules({ alertsClient }); + const ruleAlertTypes = await getNonPackagedRules({ rulesClient }); const rules = transformAlertsToRules(ruleAlertTypes); const rulesNdjson = transformDataToNdjson(rules); const exportDetails = getExportDetailsNdjson(rules); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 7410f97241966..f4325086e4212 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -11,7 +11,7 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; @@ -24,11 +24,11 @@ describe('get_export_by_object_ids', () => { }); describe('getExportByObjectIds', () => { test('it exports object ids into an expected string with new line characters', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds(alertsClient, objects); + const exports = await getExportByObjectIds(rulesClient, objects); const exportsObj = { rulesNdjson: JSON.parse(exports.rulesNdjson), exportDetails: JSON.parse(exports.exportDetails), @@ -84,7 +84,7 @@ describe('get_export_by_object_ids', () => { }); test('it does not export immutable rules', async () => { - const alertsClient = alertsClientMock.create(); + const rulesClient = rulesClientMock.create(); const result = getAlertMock(getQueryRuleParams()); result.params.immutable = true; @@ -95,11 +95,11 @@ describe('get_export_by_object_ids', () => { data: [result], }; - alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); - alertsClient.find.mockResolvedValue(findResult); + rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); + rulesClient.find.mockResolvedValue(findResult); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds(alertsClient, objects); + const exports = await getExportByObjectIds(rulesClient, objects); expect(exports).toEqual({ rulesNdjson: '', exportDetails: @@ -110,11 +110,11 @@ describe('get_export_by_object_ids', () => { describe('getRulesFromObjects', () => { test('it returns transformed rules from objects sent in', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(alertsClient, objects); + const exports = await getRulesFromObjects(rulesClient, objects); const expected: RulesErrors = { exportedCount: 1, missingRules: [], @@ -174,7 +174,7 @@ describe('get_export_by_object_ids', () => { }); test('it does not transform the rule if the rule is an immutable rule and designates it as a missing rule', async () => { - const alertsClient = alertsClientMock.create(); + const rulesClient = rulesClientMock.create(); const result = getAlertMock(getQueryRuleParams()); result.params.immutable = true; @@ -185,11 +185,11 @@ describe('get_export_by_object_ids', () => { data: [result], }; - alertsClient.get.mockResolvedValue(result); - alertsClient.find.mockResolvedValue(findResult); + rulesClient.get.mockResolvedValue(result); + rulesClient.find.mockResolvedValue(findResult); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(alertsClient, objects); + const exports = await getRulesFromObjects(rulesClient, objects); const expected: RulesErrors = { exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], @@ -199,7 +199,7 @@ describe('get_export_by_object_ids', () => { }); test('it exports missing rules', async () => { - const alertsClient = alertsClientMock.create(); + const rulesClient = rulesClientMock.create(); const findResult: FindHit = { page: 1, @@ -208,11 +208,11 @@ describe('get_export_by_object_ids', () => { data: [], }; - alertsClient.get.mockRejectedValue({ output: { statusCode: 404 } }); - alertsClient.find.mockResolvedValue(findResult); + rulesClient.get.mockRejectedValue({ output: { statusCode: 404 } }); + rulesClient.find.mockResolvedValue(findResult); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(alertsClient, objects); + const exports = await getRulesFromObjects(rulesClient, objects); const expected: RulesErrors = { exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts index 63b34435e8427..5d33e37c2ecf9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -8,7 +8,7 @@ import { chunk } from 'lodash'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; -import { AlertsClient } from '../../../../../alerting/server'; +import { RulesClient } from '../../../../../alerting/server'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { isAlertType } from '../rules/types'; import { transformAlertToRule } from '../routes/rules/utils'; @@ -33,20 +33,20 @@ export interface RulesErrors { } export const getExportByObjectIds = async ( - alertsClient: AlertsClient, + rulesClient: RulesClient, objects: Array<{ rule_id: string }> ): Promise<{ rulesNdjson: string; exportDetails: string; }> => { - const rulesAndErrors = await getRulesFromObjects(alertsClient, objects); + const rulesAndErrors = await getRulesFromObjects(rulesClient, objects); const rulesNdjson = transformDataToNdjson(rulesAndErrors.rules); const exportDetails = getExportDetailsNdjson(rulesAndErrors.rules, rulesAndErrors.missingRules); return { rulesNdjson, exportDetails }; }; export const getRulesFromObjects = async ( - alertsClient: AlertsClient, + rulesClient: RulesClient, objects: Array<{ rule_id: string }> ): Promise => { // If we put more than 1024 ids in one block like "alert.attributes.tags: (id1 OR id2 OR ... OR id1100)" @@ -65,7 +65,7 @@ export const getRulesFromObjects = async ( }) .join(' OR '); const rules = await findRules({ - alertsClient, + rulesClient, filter, page: 1, fields: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts index f2d28d13fa926..6fe326a8d85a3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts @@ -21,6 +21,7 @@ import { rawRules } from './prepackaged_rules'; import { RuleAssetSavedObjectsClient } from './rule_asset_saved_objects_client'; import { IRuleAssetSOAttributes } from './types'; import { SavedObjectAttributes } from '../../../../../../../src/core/types'; +import { ConfigType } from '../../../config'; /** * Validate the rules from the file system and throw any errors indicating to the developer @@ -103,21 +104,25 @@ export const getPrepackagedRules = ( }; export const getLatestPrepackagedRules = async ( - client: RuleAssetSavedObjectsClient + client: RuleAssetSavedObjectsClient, + prebuiltRulesFromFileSystem: ConfigType['prebuiltRulesFromFileSystem'], + prebuiltRulesFromSavedObjects: ConfigType['prebuiltRulesFromSavedObjects'] ): Promise => { // build a map of the most recent version of each rule - const prepackaged = getPrepackagedRules(); + const prepackaged = prebuiltRulesFromFileSystem ? getPrepackagedRules() : []; const ruleMap = new Map(prepackaged.map((r) => [r.rule_id, r])); // check the rules installed via fleet and create/update if the version is newer - const fleetRules = await getFleetInstalledRules(client); - const fleetUpdates = fleetRules.filter((r) => { - const rule = ruleMap.get(r.rule_id); - return rule == null || rule.version < r.version; - }); + if (prebuiltRulesFromSavedObjects) { + const fleetRules = await getFleetInstalledRules(client); + const fleetUpdates = fleetRules.filter((r) => { + const rule = ruleMap.get(r.rule_id); + return rule == null || rule.version < r.version; + }); - // add the new or updated rules to the map - fleetUpdates.forEach((r) => ruleMap.set(r.rule_id, r)); + // add the new or updated rules to the map + fleetUpdates.forEach((r) => ruleMap.set(r.rule_id, r)); + } return Array.from(ruleMap.values()); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts index 7efd63cc67722..587ce3f002b80 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -7,12 +7,12 @@ import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { SanitizedAlert, AlertTypeParams } from '../../../../../alerting/common'; -import { AlertsClient } from '../../../../../alerting/server'; +import { RulesClient } from '../../../../../alerting/server'; import { createRules } from './create_rules'; import { PartialFilter } from '../types'; export const installPrepackagedRules = ( - alertsClient: AlertsClient, + rulesClient: RulesClient, rules: AddPrepackagedRulesSchemaDecoded[], outputIndex: string ): Array>> => @@ -70,7 +70,7 @@ export const installPrepackagedRules = ( return [ ...acc, createRules({ - alertsClient, + rulesClient, anomalyThreshold, author, buildingBlockType, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts index d42b6c5aeefaa..826b197cfe094 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts @@ -6,7 +6,7 @@ */ import { PatchRulesOptions } from './types'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { getAlertMock } from '../routes/__mocks__/request_responses'; import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.mock'; @@ -14,7 +14,7 @@ import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.moc export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({ author: ['Elastic'], buildingBlockType: undefined, - alertsClient: alertsClientMock.create(), + rulesClient: rulesClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), anomalyThreshold: undefined, description: 'some description', @@ -65,7 +65,7 @@ export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({ export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({ author: ['Elastic'], buildingBlockType: undefined, - alertsClient: alertsClientMock.create(), + rulesClient: rulesClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), anomalyThreshold: 55, description: 'some description', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.test.ts index e275a02b2b0c1..1bd2656e41bae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.test.ts @@ -10,21 +10,21 @@ import { getPatchRulesOptionsMock, getPatchMlRulesOptionsMock } from './patch_ru import { PatchRulesOptions } from './types'; describe('patchRules', () => { - it('should call alertsClient.disable if the rule was enabled and enabled is false', async () => { + it('should call rulesClient.disable if the rule was enabled and enabled is false', async () => { const rulesOptionsMock = getPatchRulesOptionsMock(); const ruleOptions: PatchRulesOptions = { ...rulesOptionsMock, enabled: false, }; await patchRules(ruleOptions); - expect(ruleOptions.alertsClient.disable).toHaveBeenCalledWith( + expect(ruleOptions.rulesClient.disable).toHaveBeenCalledWith( expect.objectContaining({ id: ruleOptions.rule?.id, }) ); }); - it('should call alertsClient.enable if the rule was disabled and enabled is true', async () => { + it('should call rulesClient.enable if the rule was disabled and enabled is true', async () => { const rulesOptionsMock = getPatchRulesOptionsMock(); const ruleOptions: PatchRulesOptions = { ...rulesOptionsMock, @@ -34,14 +34,14 @@ describe('patchRules', () => { ruleOptions.rule.enabled = false; } await patchRules(ruleOptions); - expect(ruleOptions.alertsClient.enable).toHaveBeenCalledWith( + expect(ruleOptions.rulesClient.enable).toHaveBeenCalledWith( expect.objectContaining({ id: ruleOptions.rule?.id, }) ); }); - it('calls the alertsClient with legacy ML params', async () => { + it('calls the rulesClient with legacy ML params', async () => { const rulesOptionsMock = getPatchMlRulesOptionsMock(); const ruleOptions: PatchRulesOptions = { ...rulesOptionsMock, @@ -51,7 +51,7 @@ describe('patchRules', () => { ruleOptions.rule.enabled = false; } await patchRules(ruleOptions); - expect(ruleOptions.alertsClient.update).toHaveBeenCalledWith( + expect(ruleOptions.rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ @@ -63,7 +63,7 @@ describe('patchRules', () => { ); }); - it('calls the alertsClient with new ML params', async () => { + it('calls the rulesClient with new ML params', async () => { const rulesOptionsMock = getPatchMlRulesOptionsMock(); const ruleOptions: PatchRulesOptions = { ...rulesOptionsMock, @@ -74,7 +74,7 @@ describe('patchRules', () => { ruleOptions.rule.enabled = false; } await patchRules(ruleOptions); - expect(ruleOptions.alertsClient.update).toHaveBeenCalledWith( + expect(ruleOptions.rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ @@ -103,7 +103,7 @@ describe('patchRules', () => { ], }; await patchRules(ruleOptions); - expect(ruleOptions.alertsClient.update).toHaveBeenCalledWith( + expect(ruleOptions.rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ actions: [ @@ -138,7 +138,7 @@ describe('patchRules', () => { } await patchRules(ruleOptions); - expect(ruleOptions.alertsClient.update).toHaveBeenCalledWith( + expect(ruleOptions.rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ actions: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index 72af7ebc340cd..60e406255494a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -28,7 +28,7 @@ class PatchError extends Error { } export const patchRules = async ({ - alertsClient, + rulesClient, author, buildingBlockType, savedObjectsClient, @@ -192,15 +192,15 @@ export const patchRules = async ({ throw new PatchError(`Applying patch would create invalid rule: ${errors}`, 400); } - const update = await alertsClient.update({ + const update = await rulesClient.update({ id: rule.id, data: validated, }); if (rule.enabled && enabled === false) { - await alertsClient.disable({ id: rule.id }); + await rulesClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { - await enableRule({ rule, alertsClient, savedObjectsClient }); + await enableRule({ rule, rulesClient, savedObjectsClient }); } else { // enabled is null or undefined and we do not touch the rule } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts index ce82384291303..33bc002942497 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts @@ -6,7 +6,7 @@ */ import { readRules } from './read_rules'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getAlertMock, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; @@ -28,26 +28,26 @@ describe('read_rules', () => { jest.clearAllMocks(); }); describe('readRules', () => { - test('should return the output from alertsClient if id is set but ruleId is undefined', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); + test('should return the output from rulesClient if id is set but ruleId is undefined', async () => { + const rulesClient = rulesClientMock.create(); + rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); const rule = await readRules({ - alertsClient, + rulesClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: undefined, }); expect(rule).toEqual(getAlertMock(getQueryRuleParams())); }); test('should return null if saved object found by alerts client given id is not alert type', async () => { - const alertsClient = alertsClientMock.create(); + const rulesClient = rulesClientMock.create(); const result = getAlertMock(getQueryRuleParams()); // @ts-expect-error delete result.alertTypeId; - alertsClient.get.mockResolvedValue(result); + rulesClient.get.mockResolvedValue(result); const rule = await readRules({ - alertsClient, + rulesClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: undefined, }); @@ -55,13 +55,13 @@ describe('read_rules', () => { }); test('should return error if alerts client throws 404 error on get', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockImplementation(() => { + const rulesClient = rulesClientMock.create(); + rulesClient.get.mockImplementation(() => { throw new TestError(); }); const rule = await readRules({ - alertsClient, + rulesClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: undefined, }); @@ -69,13 +69,13 @@ describe('read_rules', () => { }); test('should return error if alerts client throws error on get', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockImplementation(() => { + const rulesClient = rulesClientMock.create(); + rulesClient.get.mockImplementation(() => { throw new Error('Test error'); }); try { await readRules({ - alertsClient, + rulesClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: undefined, }); @@ -84,39 +84,39 @@ describe('read_rules', () => { } }); - test('should return the output from alertsClient if id is undefined but ruleId is set', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + test('should return the output from rulesClient if id is undefined but ruleId is set', async () => { + const rulesClient = rulesClientMock.create(); + rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); + rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); const rule = await readRules({ - alertsClient, + rulesClient, id: undefined, ruleId: 'rule-1', }); expect(rule).toEqual(getAlertMock(getQueryRuleParams())); }); - test('should return null if the output from alertsClient with ruleId set is empty', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); - alertsClient.find.mockResolvedValue({ data: [], page: 0, perPage: 1, total: 0 }); + test('should return null if the output from rulesClient with ruleId set is empty', async () => { + const rulesClient = rulesClientMock.create(); + rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); + rulesClient.find.mockResolvedValue({ data: [], page: 0, perPage: 1, total: 0 }); const rule = await readRules({ - alertsClient, + rulesClient, id: undefined, ruleId: 'rule-1', }); expect(rule).toEqual(null); }); - test('should return the output from alertsClient if id is null but ruleId is set', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + test('should return the output from rulesClient if id is null but ruleId is set', async () => { + const rulesClient = rulesClientMock.create(); + rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); + rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); const rule = await readRules({ - alertsClient, + rulesClient, id: undefined, ruleId: 'rule-1', }); @@ -124,12 +124,12 @@ describe('read_rules', () => { }); test('should return null if id and ruleId are undefined', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const rulesClient = rulesClientMock.create(); + rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); + rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); const rule = await readRules({ - alertsClient, + rulesClient, id: undefined, ruleId: undefined, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts index 62f8e7642cc64..141977f2474e0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts @@ -14,19 +14,19 @@ import { isAlertType, ReadRuleOptions } from './types'; /** * This reads the rules through a cascade try of what is fastest to what is slowest. * @param id - This is the fastest. This is the auto-generated id through the parameter id. - * and the id will either be found through `alertsClient.get({ id })` or it will not + * and the id will either be found through `rulesClient.get({ id })` or it will not * be returned as a not-found or a thrown error that is not 404. * @param ruleId - This is a close second to being fast as long as it can find the rule_id from * a filter query against the tags using `alert.attributes.tags: "__internal:${ruleId}"]` */ export const readRules = async ({ - alertsClient, + rulesClient, id, ruleId, }: ReadRuleOptions): Promise | null> => { if (id != null) { try { - const rule = await alertsClient.get({ id }); + const rule = await rulesClient.get({ id }); if (isAlertType(rule)) { return rule; } else { @@ -42,7 +42,7 @@ export const readRules = async ({ } } else if (ruleId != null) { const ruleFromFind = await findRules({ - alertsClient, + rulesClient, filter: `alert.attributes.tags: "${INTERNAL_RULE_ID_KEY}:${ruleId}"`, page: 1, fields: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index d029393ce781e..7274614e2c9ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -101,7 +101,7 @@ import { EventCategoryOverrideOrUndefined, } from '../../../../common/detection_engine/schemas/common/schemas'; -import { AlertsClient, PartialAlert } from '../../../../../alerting/server'; +import { RulesClient, PartialAlert } from '../../../../../alerting/server'; import { Alert, SanitizedAlert } from '../../../../../alerting/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { PartialFilter } from '../types'; @@ -186,7 +186,7 @@ export interface HapiReadableStream extends Readable { } export interface Clients { - alertsClient: AlertsClient; + rulesClient: RulesClient; } export const isAlertTypes = ( @@ -214,7 +214,7 @@ export const isRuleStatusFindType = ( }; export interface CreateRulesOptions { - alertsClient: AlertsClient; + rulesClient: RulesClient; anomalyThreshold: AnomalyThresholdOrUndefined; author: Author; buildingBlockType: BuildingBlockTypeOrUndefined; @@ -267,14 +267,14 @@ export interface CreateRulesOptions { export interface UpdateRulesOptions { savedObjectsClient: SavedObjectsClientContract; - alertsClient: AlertsClient; + rulesClient: RulesClient; defaultOutputIndex: string; ruleUpdate: UpdateRulesSchema; } export interface PatchRulesOptions { savedObjectsClient: SavedObjectsClientContract; - alertsClient: AlertsClient; + rulesClient: RulesClient; anomalyThreshold: AnomalyThresholdOrUndefined; author: AuthorOrUndefined; buildingBlockType: BuildingBlockTypeOrUndefined; @@ -324,13 +324,13 @@ export interface PatchRulesOptions { } export interface ReadRuleOptions { - alertsClient: AlertsClient; + rulesClient: RulesClient; id: IdOrUndefined; ruleId: RuleIdOrUndefined; } export interface DeleteRuleOptions { - alertsClient: AlertsClient; + rulesClient: RulesClient; savedObjectsClient: SavedObjectsClientContract; ruleStatusClient: RuleStatusSavedObjectsClient; ruleStatuses: SavedObjectsFindResponse; @@ -338,7 +338,7 @@ export interface DeleteRuleOptions { } export interface FindRuleOptions { - alertsClient: AlertsClient; + rulesClient: RulesClient; perPage: PerPageOrUndefined; page: PageOrUndefined; sortField: SortFieldOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index 1f75aa5a19a98..b88c7f0450cac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -6,7 +6,7 @@ */ import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; import { updatePrepackagedRules } from './update_prepacked_rules'; import { patchRules } from './patch_rules'; @@ -14,11 +14,11 @@ import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/dete jest.mock('./patch_rules'); describe('updatePrepackagedRules', () => { - let alertsClient: ReturnType; + let rulesClient: ReturnType; let savedObjectsClient: ReturnType; beforeEach(() => { - alertsClient = alertsClientMock.create(); + rulesClient = rulesClientMock.create(); savedObjectsClient = savedObjectsClientMock.create(); }); @@ -33,10 +33,10 @@ describe('updatePrepackagedRules', () => { ]; const outputIndex = 'outputIndex'; const prepackagedRule = getAddPrepackagedRulesSchemaDecodedMock(); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); await updatePrepackagedRules( - alertsClient, + rulesClient, savedObjectsClient, [{ ...prepackagedRule, actions }], outputIndex diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts index f3ee7e251c02d..872a3b69d27ed 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { chunk } from 'lodash/fp'; import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; -import { AlertsClient, PartialAlert } from '../../../../../alerting/server'; +import { RulesClient, PartialAlert } from '../../../../../alerting/server'; import { patchRules } from './patch_rules'; import { readRules } from './read_rules'; import { PartialFilter } from '../types'; @@ -43,34 +43,34 @@ export const UPDATE_CHUNK_SIZE = 50; * Updates the prepackaged rules given a set of rules and output index. * This implements a chunked approach to not saturate network connections and * avoid being a "noisy neighbor". - * @param alertsClient Alerting client + * @param rulesClient Alerting client * @param savedObjectsClient Saved object client * @param rules The rules to apply the update for * @param outputIndex The output index to apply the update to. */ export const updatePrepackagedRules = async ( - alertsClient: AlertsClient, + rulesClient: RulesClient, savedObjectsClient: SavedObjectsClientContract, rules: AddPrepackagedRulesSchemaDecoded[], outputIndex: string ): Promise => { const ruleChunks = chunk(UPDATE_CHUNK_SIZE, rules); for (const ruleChunk of ruleChunks) { - const rulePromises = createPromises(alertsClient, savedObjectsClient, ruleChunk, outputIndex); + const rulePromises = createPromises(rulesClient, savedObjectsClient, ruleChunk, outputIndex); await Promise.all(rulePromises); } }; /** * Creates promises of the rules and returns them. - * @param alertsClient Alerting client + * @param rulesClient Alerting client * @param savedObjectsClient Saved object client * @param rules The rules to apply the update for * @param outputIndex The output index to apply the update to. * @returns Promise of what was updated. */ export const createPromises = ( - alertsClient: AlertsClient, + rulesClient: RulesClient, savedObjectsClient: SavedObjectsClientContract, rules: AddPrepackagedRulesSchemaDecoded[], outputIndex: string @@ -122,7 +122,7 @@ export const createPromises = ( exceptions_list: exceptionsList, } = rule; - const existingRule = await readRules({ alertsClient, ruleId, id: undefined }); + const existingRule = await readRules({ rulesClient, ruleId, id: undefined }); // TODO: Fix these either with an is conversion or by better typing them within io-ts const filters: PartialFilter[] | undefined = filtersObject as PartialFilter[]; @@ -130,7 +130,7 @@ export const createPromises = ( // Note: we do not pass down enabled as we do not want to suddenly disable // or enable rules on the user when they were not expecting it if a rule updates return patchRules({ - alertsClient, + rulesClient, author, buildingBlockType, description, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts index fd18ac7f7b6bc..778337a3650c7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts @@ -6,7 +6,7 @@ */ import { UpdateRulesOptions } from './types'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { getUpdateRulesSchemaMock, @@ -14,14 +14,14 @@ import { } from '../../../../common/detection_engine/schemas/request/rule_schemas.mock'; export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ - alertsClient: alertsClientMock.create(), + rulesClient: rulesClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), defaultOutputIndex: '.siem-signals-default', ruleUpdate: getUpdateRulesSchemaMock(), }); export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ - alertsClient: alertsClientMock.create(), + rulesClient: rulesClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), defaultOutputIndex: '.siem-signals-default', ruleUpdate: getUpdateMachineLearningSchemaMock(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts index 48b8905384566..7d04d3412899d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts @@ -8,55 +8,55 @@ import { getAlertMock } from '../routes/__mocks__/request_responses'; import { updateRules } from './update_rules'; import { getUpdateRulesOptionsMock, getUpdateMlRulesOptionsMock } from './update_rules.mock'; -import { AlertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; +import { RulesClientMock } from '../../../../../alerting/server/rules_client.mock'; import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.mock'; describe('updateRules', () => { - it('should call alertsClient.disable if the rule was enabled and enabled is false', async () => { + it('should call rulesClient.disable if the rule was enabled and enabled is false', async () => { const rulesOptionsMock = getUpdateRulesOptionsMock(); rulesOptionsMock.ruleUpdate.enabled = false; - ((rulesOptionsMock.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue( + ((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).get.mockResolvedValue( getAlertMock(getQueryRuleParams()) ); await updateRules(rulesOptionsMock); - expect(rulesOptionsMock.alertsClient.disable).toHaveBeenCalledWith( + expect(rulesOptionsMock.rulesClient.disable).toHaveBeenCalledWith( expect.objectContaining({ id: rulesOptionsMock.ruleUpdate.id, }) ); }); - it('should call alertsClient.enable if the rule was disabled and enabled is true', async () => { + it('should call rulesClient.enable if the rule was disabled and enabled is true', async () => { const rulesOptionsMock = getUpdateRulesOptionsMock(); rulesOptionsMock.ruleUpdate.enabled = true; - ((rulesOptionsMock.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue({ + ((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).get.mockResolvedValue({ ...getAlertMock(getQueryRuleParams()), enabled: false, }); await updateRules(rulesOptionsMock); - expect(rulesOptionsMock.alertsClient.enable).toHaveBeenCalledWith( + expect(rulesOptionsMock.rulesClient.enable).toHaveBeenCalledWith( expect.objectContaining({ id: rulesOptionsMock.ruleUpdate.id, }) ); }); - it('calls the alertsClient with params', async () => { + it('calls the rulesClient with params', async () => { const rulesOptionsMock = getUpdateMlRulesOptionsMock(); rulesOptionsMock.ruleUpdate.enabled = true; - ((rulesOptionsMock.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue( + ((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).get.mockResolvedValue( getAlertMock(getMlRuleParams()) ); await updateRules(rulesOptionsMock); - expect(rulesOptionsMock.alertsClient.update).toHaveBeenCalledWith( + expect(rulesOptionsMock.rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index 0fac804163afa..e0be646dc3f39 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -18,13 +18,13 @@ import { InternalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; import { enableRule } from './enable_rule'; export const updateRules = async ({ - alertsClient, + rulesClient, savedObjectsClient, defaultOutputIndex, ruleUpdate, }: UpdateRulesOptions): Promise | null> => { const existingRule = await readRules({ - alertsClient, + rulesClient, ruleId: ruleUpdate.rule_id, id: ruleUpdate.id, }); @@ -80,15 +80,15 @@ export const updateRules = async ({ notifyWhen: null, }; - const update = await alertsClient.update({ + const update = await rulesClient.update({ id: existingRule.id, data: newInternalRule, }); if (existingRule.enabled && enabled === false) { - await alertsClient.disable({ id: existingRule.id }); + await rulesClient.disable({ id: existingRule.id }); } else if (!existingRule.enabled && enabled === true) { - await enableRule({ rule: existingRule, alertsClient, savedObjectsClient }); + await enableRule({ rule: existingRule, rulesClient, savedObjectsClient }); } return { ...update, enabled }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules_notifications.ts index 60e448e3058c8..5f2729f129948 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules_notifications.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules_notifications.ts @@ -6,13 +6,13 @@ */ import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { AlertsClient, AlertServices } from '../../../../../alerting/server'; +import { RulesClient, AlertServices } from '../../../../../alerting/server'; import { updateOrCreateRuleActionsSavedObject } from '../rule_actions/update_or_create_rule_actions_saved_object'; import { updateNotifications } from '../notifications/update_notifications'; import { RuleActions } from '../rule_actions/types'; interface UpdateRulesNotifications { - alertsClient: AlertsClient; + rulesClient: RulesClient; savedObjectsClient: AlertServices['savedObjectsClient']; ruleAlertId: string; actions: RuleAlertAction[] | undefined; @@ -22,7 +22,7 @@ interface UpdateRulesNotifications { } export const updateRulesNotifications = async ({ - alertsClient, + rulesClient, savedObjectsClient, ruleAlertId, actions, @@ -38,7 +38,7 @@ export const updateRulesNotifications = async ({ }); await updateNotifications({ - alertsClient, + rulesClient, ruleAlertId, enabled, name, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts index 28cea9ea22b0d..e6e1212c46905 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts @@ -477,4 +477,63 @@ describe('create_signals', () => { }, }); }); + + test('if trackTotalHits is provided it should be included', () => { + const query = buildEventsSearchQuery({ + index: ['auditbeat-*'], + from: 'now-5m', + to: 'today', + filter: {}, + size: 100, + searchAfterSortIds: undefined, + timestampOverride: undefined, + trackTotalHits: false, + }); + expect(query.track_total_hits).toEqual(false); + }); + + test('if sortOrder is provided it should be included', () => { + const query = buildEventsSearchQuery({ + index: ['auditbeat-*'], + from: 'now-5m', + to: 'today', + filter: {}, + size: 100, + searchAfterSortIds: undefined, + timestampOverride: undefined, + sortOrder: 'desc', + trackTotalHits: false, + }); + expect(query.body.sort[0]).toEqual({ + '@timestamp': { + order: 'desc', + unmapped_type: 'date', + }, + }); + }); + + test('it respects sort order for timestampOverride', () => { + const query = buildEventsSearchQuery({ + index: ['auditbeat-*'], + from: 'now-5m', + to: 'today', + filter: {}, + size: 100, + searchAfterSortIds: undefined, + timestampOverride: 'event.ingested', + sortOrder: 'desc', + }); + expect(query.body.sort[0]).toEqual({ + 'event.ingested': { + order: 'desc', + unmapped_type: 'date', + }, + }); + expect(query.body.sort[1]).toEqual({ + '@timestamp': { + order: 'desc', + unmapped_type: 'date', + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts index 7b27d22d9b387..8d10eda2a30c2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts @@ -6,10 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; import { isEmpty } from 'lodash'; -import { - SortOrderOrUndefined, - TimestampOverrideOrUndefined, -} from '../../../../common/detection_engine/schemas/common/schemas'; +import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; interface BuildEventsSearchQuery { aggregations?: Record; @@ -18,9 +15,10 @@ interface BuildEventsSearchQuery { to: string; filter: estypes.QueryDslQueryContainer; size: number; - sortOrder?: SortOrderOrUndefined; + sortOrder?: estypes.SearchSortOrder; searchAfterSortIds: estypes.SearchSortResults | undefined; timestampOverride: TimestampOverrideOrUndefined; + trackTotalHits?: boolean; } export const buildEventsSearchQuery = ({ @@ -33,6 +31,7 @@ export const buildEventsSearchQuery = ({ searchAfterSortIds, sortOrder, timestampOverride, + trackTotalHits, }: BuildEventsSearchQuery) => { const defaultTimeFields = ['@timestamp']; const timestamps = @@ -97,11 +96,28 @@ export const buildEventsSearchQuery = ({ { bool: { filter: [{ bool: { should: [...rangeFilter], minimum_should_match: 1 } }] } }, ]; + const sort: estypes.SearchSort = []; + if (timestampOverride) { + sort.push({ + [timestampOverride]: { + order: sortOrder ?? 'asc', + unmapped_type: 'date', + }, + }); + } + sort.push({ + '@timestamp': { + order: sortOrder ?? 'asc', + unmapped_type: 'date', + }, + }); + const searchQuery = { allow_no_indices: true, index, size, ignore_unavailable: true, + track_total_hits: trackTotalHits, body: { query: { bool: { @@ -121,31 +137,7 @@ export const buildEventsSearchQuery = ({ ...docFields, ], ...(aggregations ? { aggregations } : {}), - sort: [ - ...(timestampOverride != null - ? [ - { - [timestampOverride]: { - order: sortOrder ?? 'asc', - unmapped_type: 'date', - }, - }, - { - '@timestamp': { - order: sortOrder ?? 'asc', - unmapped_type: 'date', - }, - }, - ] - : [ - { - '@timestamp': { - order: sortOrder ?? 'asc', - unmapped_type: 'date', - }, - }, - ]), - ], + sort, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts index a1d7d03f313db..e98e9b49b3646 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts @@ -16,6 +16,7 @@ import { getIndexVersion } from '../../routes/index/get_index_version'; import { SIGNALS_TEMPLATE_VERSION } from '../../routes/index/get_signals_template'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { allowedExperimentalValues } from '../../../../../common/experimental_features'; jest.mock('../../routes/index/get_index_version'); @@ -73,6 +74,7 @@ describe('eql_executor', () => { rule: eqlSO, tuple, exceptionItems, + experimentalFeatures: allowedExperimentalValues, services: alertServices, version, logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts index e08f519e9761a..8d19510c63477 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts @@ -34,11 +34,13 @@ import { SimpleHit, } from '../types'; import { createSearchAfterReturnType, makeFloatString } from '../utils'; +import { ExperimentalFeatures } from '../../../../../common/experimental_features'; export const eqlExecutor = async ({ rule, tuple, exceptionItems, + experimentalFeatures, services, version, logger, @@ -50,6 +52,7 @@ export const eqlExecutor = async ({ rule: SavedObject>; tuple: RuleRangeTuple; exceptionItems: ExceptionListItemSchema[]; + experimentalFeatures: ExperimentalFeatures; services: AlertServices; version: string; logger: Logger; @@ -85,7 +88,12 @@ export const eqlExecutor = async ({ throw err; } } - const inputIndex = await getInputIndex(services, version, ruleParams.index); + const inputIndex = await getInputIndex({ + experimentalFeatures, + services, + version, + index: ruleParams.index, + }); const request = buildEqlSearchRequest( ruleParams.query, inputIndex, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts index 385c01c2f1cda..454cb464506a9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts @@ -21,12 +21,14 @@ import { AlertAttributes, RuleRangeTuple, BulkCreate, WrapHits } from '../types' import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { QueryRuleParams, SavedQueryRuleParams } from '../../schemas/rule_schemas'; +import { ExperimentalFeatures } from '../../../../../common/experimental_features'; export const queryExecutor = async ({ rule, tuple, listClient, exceptionItems, + experimentalFeatures, services, version, searchAfterSize, @@ -40,6 +42,7 @@ export const queryExecutor = async ({ tuple: RuleRangeTuple; listClient: ListClient; exceptionItems: ExceptionListItemSchema[]; + experimentalFeatures: ExperimentalFeatures; services: AlertServices; version: string; searchAfterSize: number; @@ -50,7 +53,12 @@ export const queryExecutor = async ({ wrapHits: WrapHits; }) => { const ruleParams = rule.attributes.params; - const inputIndex = await getInputIndex(services, version, ruleParams.index); + const inputIndex = await getInputIndex({ + experimentalFeatures, + services, + version, + index: ruleParams.index, + }); const esFilter = await getFilter({ type: ruleParams.type, filters: ruleParams.filters, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts index d0e22f696b222..37b2c53636cfd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts @@ -20,6 +20,7 @@ import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { createThreatSignals } from '../threat_mapping/create_threat_signals'; import { ThreatRuleParams } from '../../schemas/rule_schemas'; +import { ExperimentalFeatures } from '../../../../../common/experimental_features'; export const threatMatchExecutor = async ({ rule, @@ -31,6 +32,7 @@ export const threatMatchExecutor = async ({ searchAfterSize, logger, eventsTelemetry, + experimentalFeatures, buildRuleMessage, bulkCreate, wrapHits, @@ -44,12 +46,18 @@ export const threatMatchExecutor = async ({ searchAfterSize: number; logger: Logger; eventsTelemetry: TelemetryEventsSender | undefined; + experimentalFeatures: ExperimentalFeatures; buildRuleMessage: BuildRuleMessage; bulkCreate: BulkCreate; wrapHits: WrapHits; }) => { const ruleParams = rule.attributes.params; - const inputIndex = await getInputIndex(services, version, ruleParams.index); + const inputIndex = await getInputIndex({ + experimentalFeatures, + services, + version, + index: ruleParams.index, + }); return createThreatSignals({ tuple, threatMapping: ruleParams.threatMapping, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts index 3906c66922238..afcb3707591fc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts @@ -16,6 +16,7 @@ import { getEntryListMock } from '../../../../../../lists/common/schemas/types/e import { getThresholdRuleParams } from '../../schemas/rule_schemas.mock'; import { buildRuleMessageFactory } from '../rule_messages'; import { sampleEmptyDocSearchResults } from '../__mocks__/es_results'; +import { allowedExperimentalValues } from '../../../../../common/experimental_features'; describe('threshold_executor', () => { const version = '8.0.0'; @@ -70,6 +71,7 @@ describe('threshold_executor', () => { rule: thresholdSO, tuple, exceptionItems, + experimentalFeatures: allowedExperimentalValues, services: alertServices, version, logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts index 378d68fc13d2a..ffd90f3b90b91 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts @@ -36,11 +36,13 @@ import { mergeReturns, } from '../utils'; import { BuildRuleMessage } from '../rule_messages'; +import { ExperimentalFeatures } from '../../../../../common/experimental_features'; export const thresholdExecutor = async ({ rule, tuple, exceptionItems, + experimentalFeatures, services, version, logger, @@ -52,6 +54,7 @@ export const thresholdExecutor = async ({ rule: SavedObject>; tuple: RuleRangeTuple; exceptionItems: ExceptionListItemSchema[]; + experimentalFeatures: ExperimentalFeatures; services: AlertServices; version: string; logger: Logger; @@ -68,7 +71,12 @@ export const thresholdExecutor = async ({ ); result.warning = true; } - const inputIndex = await getInputIndex(services, version, ruleParams.index); + const inputIndex = await getInputIndex({ + experimentalFeatures, + services, + version, + index: ruleParams.index, + }); const { thresholdSignalHistory, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts index 9c4bf37aca789..5058056b169a3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts @@ -7,7 +7,7 @@ import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; -import { getInputIndex } from './get_input_output_index'; +import { getInputIndex, GetInputIndex } from './get_input_output_index'; describe('get_input_output_index', () => { let servicesMock: AlertServicesMock; @@ -19,7 +19,7 @@ describe('get_input_output_index', () => { afterAll(() => { jest.resetAllMocks(); }); - + let defaultProps: GetInputIndex; beforeEach(() => { servicesMock = alertsMock.createAlertServices(); servicesMock.savedObjectsClient.get.mockImplementation(async (type: string, id: string) => ({ @@ -28,6 +28,18 @@ describe('get_input_output_index', () => { references: [], attributes: {}, })); + defaultProps = { + services: servicesMock, + version: '8.0.0', + index: ['test-input-index-1'], + experimentalFeatures: { + trustedAppsByPolicyEnabled: false, + metricsEntitiesEnabled: false, + ruleRegistryEnabled: false, + tGridEnabled: false, + uebaEnabled: false, + }, + }; }); describe('getInputOutputIndex', () => { @@ -38,7 +50,7 @@ describe('get_input_output_index', () => { references: [], attributes: {}, })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', ['test-input-index-1']); + const inputIndex = await getInputIndex(defaultProps); expect(inputIndex).toEqual(['test-input-index-1']); }); @@ -51,7 +63,10 @@ describe('get_input_output_index', () => { [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], }, })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: undefined, + }); expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); }); @@ -64,7 +79,10 @@ describe('get_input_output_index', () => { [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], }, })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: null, + }); expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); }); @@ -77,7 +95,26 @@ describe('get_input_output_index', () => { [DEFAULT_INDEX_KEY]: null, }, })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: null, + }); + expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); + }); + + test('Returns a saved object inputIndex default along with experimental features when uebaEnabled=true', async () => { + servicesMock.savedObjectsClient.get.mockImplementation(async (type: string, id: string) => ({ + id, + type, + references: [], + attributes: { + [DEFAULT_INDEX_KEY]: null, + }, + })); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: null, + }); expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); }); @@ -90,17 +127,26 @@ describe('get_input_output_index', () => { [DEFAULT_INDEX_KEY]: null, }, })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: undefined, + }); expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); }); test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes are missing and the index is undefined', async () => { - const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: undefined, + }); expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); }); test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes are missing and the index is null', async () => { - const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: null, + }); expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts index f0c62bee7aec9..d3b60f1e9a281 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts @@ -5,20 +5,33 @@ * 2.0. */ -import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; +import { + DEFAULT_INDEX_KEY, + DEFAULT_INDEX_PATTERN, + DEFAULT_INDEX_PATTERN_EXPERIMENTAL, +} from '../../../../common/constants'; import { AlertInstanceContext, AlertInstanceState, AlertServices, } from '../../../../../alerting/server'; +import { ExperimentalFeatures } from '../../../../common/experimental_features'; + +export interface GetInputIndex { + experimentalFeatures: ExperimentalFeatures; + index: string[] | null | undefined; + services: AlertServices; + version: string; +} -export const getInputIndex = async ( - services: AlertServices, - version: string, - inputIndex: string[] | null | undefined -): Promise => { - if (inputIndex != null) { - return inputIndex; +export const getInputIndex = async ({ + experimentalFeatures, + index, + services, + version, +}: GetInputIndex): Promise => { + if (index != null) { + return index; } else { const configuration = await services.savedObjectsClient.get<{ 'securitySolution:defaultIndex': string[]; @@ -26,7 +39,9 @@ export const getInputIndex = async ( if (configuration.attributes != null && configuration.attributes[DEFAULT_INDEX_KEY] != null) { return configuration.attributes[DEFAULT_INDEX_KEY]; } else { - return DEFAULT_INDEX_PATTERN; + return experimentalFeatures.uebaEnabled + ? [...DEFAULT_INDEX_PATTERN, ...DEFAULT_INDEX_PATTERN_EXPERIMENTAL] + : DEFAULT_INDEX_PATTERN; } } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index eb4af0c38ce25..75cffb598d186 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -14,7 +14,7 @@ import { createSearchAfterReturnType, createSearchResultReturnType, createSearchAfterReturnTypeFromResponse, - createTotalHitsFromSearchResult, + getTotalHitsValue, mergeReturns, mergeSearchResults, getSafeSortIds, @@ -37,6 +37,8 @@ export const searchAfterAndBulkCreate = async ({ enrichment = identity, bulkCreate, wrapHits, + sortOrder, + trackTotalHits, }: SearchAfterAndBulkCreateParams): Promise => { const ruleParams = ruleSO.attributes.params; let toReturn = createSearchAfterReturnType(); @@ -75,6 +77,8 @@ export const searchAfterAndBulkCreate = async ({ filter, pageSize: Math.ceil(Math.min(tuple.maxSignals, pageSize)), timestampOverride: ruleParams.timestampOverride, + trackTotalHits, + sortOrder, }); mergedSearchResults = mergeSearchResults([mergedSearchResults, searchResult]); toReturn = mergeReturns([ @@ -101,7 +105,7 @@ export const searchAfterAndBulkCreate = async ({ } // determine if there are any candidate signals to be processed - const totalHits = createTotalHitsFromSearchResult({ searchResult: mergedSearchResults }); + const totalHits = getTotalHitsValue(mergedSearchResults.hits.total); logger.debug(buildRuleMessage(`totalHits: ${totalHits}`)); logger.debug( buildRuleMessage(`searchResult.hit.hits.length: ${mergedSearchResults.hits.hits.length}`) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index aec8b6c552b1d..a14c678d27536 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -33,6 +33,7 @@ import { queryExecutor } from './executors/query'; import { mlExecutor } from './executors/ml'; import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.mock'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { allowedExperimentalValues } from '../../../../common/experimental_features'; jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); @@ -188,6 +189,7 @@ describe('signal_rule_alert_type', () => { payload = getPayload(ruleAlert, alertServices) as jest.Mocked; alert = signalRulesAlertType({ + experimentalFeatures: allowedExperimentalValues, logger, eventsTelemetry: undefined, version, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 6eef97b05b697..d524757b7c144 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -69,10 +69,12 @@ import { bulkCreateFactory } from './bulk_create_factory'; import { wrapHitsFactory } from './wrap_hits_factory'; import { wrapSequencesFactory } from './wrap_sequences_factory'; import { ConfigType } from '../../../config'; +import { ExperimentalFeatures } from '../../../../common/experimental_features'; export const signalRulesAlertType = ({ logger, eventsTelemetry, + experimentalFeatures, version, ml, lists, @@ -80,6 +82,7 @@ export const signalRulesAlertType = ({ }: { logger: Logger; eventsTelemetry: TelemetryEventsSender | undefined; + experimentalFeatures: ExperimentalFeatures; version: string; ml: SetupPlugins['ml']; lists: SetupPlugins['lists'] | undefined; @@ -153,7 +156,12 @@ export const signalRulesAlertType = ({ if (!isMachineLearningParams(params)) { const index = params.index; const hasTimestampOverride = timestampOverride != null && !isEmpty(timestampOverride); - const inputIndices = await getInputIndex(services, version, index); + const inputIndices = await getInputIndex({ + services, + version, + index, + experimentalFeatures, + }); const [privileges, timestampFieldCaps] = await Promise.all([ checkPrivileges(services, inputIndices), services.scopedClusterClient.asCurrentUser.fieldCaps({ @@ -268,6 +276,7 @@ export const signalRulesAlertType = ({ rule: thresholdRuleSO, tuple, exceptionItems, + experimentalFeatures, services, version, logger, @@ -285,6 +294,7 @@ export const signalRulesAlertType = ({ tuple, listClient, exceptionItems, + experimentalFeatures, services, version, searchAfterSize, @@ -303,6 +313,7 @@ export const signalRulesAlertType = ({ tuple, listClient, exceptionItems, + experimentalFeatures, services, version, searchAfterSize, @@ -320,6 +331,7 @@ export const signalRulesAlertType = ({ rule: eqlRuleSO, tuple, exceptionItems, + experimentalFeatures, services, version, searchAfterSize, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index 6436da40088b3..ff49fb5892f50 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -16,10 +16,7 @@ import type { SignalSearchResponse, SignalSource } from './types'; import { BuildRuleMessage } from './rule_messages'; import { buildEventsSearchQuery } from './build_events_query'; import { createErrorsFromShard, makeFloatString } from './utils'; -import { - SortOrderOrUndefined, - TimestampOverrideOrUndefined, -} from '../../../../common/detection_engine/schemas/common/schemas'; +import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; interface SingleSearchAfterParams { aggregations?: Record; @@ -30,10 +27,11 @@ interface SingleSearchAfterParams { services: AlertServices; logger: Logger; pageSize: number; - sortOrder?: SortOrderOrUndefined; + sortOrder?: estypes.SearchSortOrder; filter: estypes.QueryDslQueryContainer; timestampOverride: TimestampOverrideOrUndefined; buildRuleMessage: BuildRuleMessage; + trackTotalHits?: boolean; } // utilize search_after for paging results into bulk. @@ -50,6 +48,7 @@ export const singleSearchAfter = async ({ sortOrder, timestampOverride, buildRuleMessage, + trackTotalHits, }: SingleSearchAfterParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; @@ -66,6 +65,7 @@ export const singleSearchAfter = async ({ sortOrder, searchAfterSortIds, timestampOverride, + trackTotalHits, }); const start = performance.now(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index 806f5e47608e4..fb9881b519a16 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -86,7 +86,10 @@ export const createThreatSignal = async ({ enrichment: threatEnrichment, bulkCreate, wrapHits, + sortOrder: 'desc', + trackTotalHits: false, }); + logger.debug( buildRuleMessage( `${ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index edf6d244cfa17..dbc6848335893 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { DslQuery, Filter } from 'src/plugins/data/common'; +import { DslQuery, Filter } from '@kbn/es-query'; import moment, { Moment } from 'moment'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -300,6 +300,8 @@ export interface SearchAfterAndBulkCreateParams { enrichment?: SignalsEnrichment; bulkCreate: BulkCreate; wrapHits: WrapHits; + trackTotalHits?: boolean; + sortOrder?: estypes.SearchSortOrder; } export interface SearchAfterAndBulkCreateReturnType { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 4d5ac05957a4b..72a6ff478ade3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -36,11 +36,12 @@ import { createSearchAfterReturnTypeFromResponse, createSearchAfterReturnType, mergeReturns, - createTotalHitsFromSearchResult, lastValidDate, calculateThresholdSignalUuid, buildChunkedOrFilter, getValidDateFromDoc, + calculateTotal, + getTotalHitsValue, } from './utils'; import { BulkResponseErrorAggregation, SearchAfterAndBulkCreateReturnType } from './types'; import { @@ -53,7 +54,6 @@ import { sampleDocSearchResultsWithSortId, sampleEmptyDocSearchResults, sampleDocSearchResultsNoSortIdNoHits, - repeatedSearchResultsWithSortId, sampleDocSearchResultsNoSortId, sampleDocNoSortId, } from './__mocks__/es_results'; @@ -1569,22 +1569,6 @@ describe('utils', () => { }); }); - describe('createTotalHitsFromSearchResult', () => { - test('it should return 0 for empty results', () => { - const result = createTotalHitsFromSearchResult({ - searchResult: sampleEmptyDocSearchResults(), - }); - expect(result).toEqual(0); - }); - - test('it should return 4 for 4 result sets', () => { - const result = createTotalHitsFromSearchResult({ - searchResult: repeatedSearchResultsWithSortId(4, 1, ['1', '2', '3', '4']), - }); - expect(result).toEqual(4); - }); - }); - describe('calculateThresholdSignalUuid', () => { it('should generate a uuid without key', () => { const startedAt = new Date('2020-12-17T16:27:00Z'); @@ -1620,4 +1604,28 @@ describe('utils', () => { expect(filter).toEqual('field.name: ("id-1" OR "id-2") OR field.name: ("id-3")'); }); }); + + describe('getTotalHitsValue', () => { + test('returns value if present as number', () => { + expect(getTotalHitsValue(sampleDocSearchResultsWithSortId().hits.total)).toBe(1); + }); + + test('returns value if present as value object', () => { + expect(getTotalHitsValue({ value: 1 })).toBe(1); + }); + + test('returns -1 if not present', () => { + expect(getTotalHitsValue(undefined)).toBe(-1); + }); + }); + + describe('calculateTotal', () => { + test('should add totalHits if both totalHits values are numbers', () => { + expect(calculateTotal(1, 2)).toBe(3); + }); + + test('should return -1 if totalHits is undefined', () => { + expect(calculateTotal(undefined, 2)).toBe(-1); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 4dd434156288f..cb1bf9d774359 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -797,9 +797,7 @@ export const mergeSearchResults = (searchResults: SignalSearchResponse[]) => { }, aggregations: newAggregations, hits: { - total: - createTotalHitsFromSearchResult({ searchResult: prev }) + - createTotalHitsFromSearchResult({ searchResult: next }), + total: calculateTotal(prev.hits.total, next.hits.total), max_score: Math.max(newHits.max_score!, existingHits.max_score!), hits: [...existingHits.hits, ...newHits.hits], }, @@ -807,16 +805,23 @@ export const mergeSearchResults = (searchResults: SignalSearchResponse[]) => { }); }; -export const createTotalHitsFromSearchResult = ({ - searchResult, -}: { - searchResult: { hits: { total: number | { value: number } } }; -}): number => { - const totalHits = - typeof searchResult.hits.total === 'number' - ? searchResult.hits.total - : searchResult.hits.total.value; - return totalHits; +export const getTotalHitsValue = (totalHits: number | { value: number } | undefined): number => + typeof totalHits === 'undefined' + ? -1 + : typeof totalHits === 'number' + ? totalHits + : totalHits.value; + +export const calculateTotal = ( + prevTotal: number | { value: number } | undefined, + nextTotal: number | { value: number } | undefined +): number => { + const prevTotalHits = getTotalHitsValue(prevTotal); + const nextTotalHits = getTotalHitsValue(nextTotal); + if (prevTotalHits === -1 || nextTotalHits === -1) { + return -1; + } + return prevTotalHits + nextTotalHits; }; export const calculateThresholdSignalUuid = ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.test.ts index b2a589dacd371..1b7bf048646ed 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getAlertMock, getFindResultWithMultiHits } from '../routes/__mocks__/request_responses'; import { INTERNAL_RULE_ID_KEY, INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { readRawTags, readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; @@ -28,10 +28,10 @@ describe('read_tags', () => { result2.params.ruleId = 'rule-2'; result2.tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4']; - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const tags = await readRawTags({ alertsClient }); + const tags = await readRawTags({ rulesClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -46,10 +46,10 @@ describe('read_tags', () => { result2.params.ruleId = 'rule-2'; result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const tags = await readRawTags({ alertsClient }); + const tags = await readRawTags({ rulesClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -64,10 +64,10 @@ describe('read_tags', () => { result2.params.ruleId = 'rule-2'; result2.tags = []; - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const tags = await readRawTags({ alertsClient }); + const tags = await readRawTags({ rulesClient }); expect(tags).toEqual([]); }); @@ -77,10 +77,10 @@ describe('read_tags', () => { result1.params.ruleId = 'rule-1'; result1.tags = ['tag 1', 'tag 1', 'tag 1', 'tag 2']; - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const tags = await readRawTags({ alertsClient }); + const tags = await readRawTags({ rulesClient }); expect(tags).toEqual(['tag 1', 'tag 2']); }); @@ -90,10 +90,10 @@ describe('read_tags', () => { result1.params.ruleId = 'rule-1'; result1.tags = []; - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const tags = await readRawTags({ alertsClient }); + const tags = await readRawTags({ rulesClient }); expect(tags).toEqual([]); }); }); @@ -110,10 +110,10 @@ describe('read_tags', () => { result2.params.ruleId = 'rule-2'; result2.tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4']; - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const tags = await readTags({ alertsClient }); + const tags = await readTags({ rulesClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -128,10 +128,10 @@ describe('read_tags', () => { result2.params.ruleId = 'rule-2'; result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const tags = await readTags({ alertsClient }); + const tags = await readTags({ rulesClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -146,10 +146,10 @@ describe('read_tags', () => { result2.params.ruleId = 'rule-2'; result2.tags = []; - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const tags = await readTags({ alertsClient }); + const tags = await readTags({ rulesClient }); expect(tags).toEqual([]); }); @@ -159,10 +159,10 @@ describe('read_tags', () => { result1.params.ruleId = 'rule-1'; result1.tags = ['tag 1', 'tag 1', 'tag 1', 'tag 2']; - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const tags = await readTags({ alertsClient }); + const tags = await readTags({ rulesClient }); expect(tags).toEqual(['tag 1', 'tag 2']); }); @@ -172,10 +172,10 @@ describe('read_tags', () => { result1.params.ruleId = 'rule-1'; result1.tags = []; - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const tags = await readTags({ alertsClient }); + const tags = await readTags({ rulesClient }); expect(tags).toEqual([]); }); @@ -189,10 +189,10 @@ describe('read_tags', () => { 'tag 1', ]; - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const tags = await readTags({ alertsClient }); + const tags = await readTags({ rulesClient }); expect(tags).toEqual(['tag 1']); }); @@ -222,10 +222,10 @@ describe('read_tags', () => { 'tag 4', ]; - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); + const rulesClient = rulesClientMock.create(); + rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const tags = await readTags({ alertsClient }); + const tags = await readTags({ rulesClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4', 'tag 5']); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.ts index 2d1966917d287..2314a8a49f567 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.ts @@ -7,7 +7,7 @@ import { has } from 'lodash/fp'; import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; -import { AlertsClient } from '../../../../../alerting/server'; +import { RulesClient } from '../../../../../alerting/server'; import { findRules } from '../rules/find_rules'; export interface TagType { @@ -40,23 +40,23 @@ export const convertTagsToSet = (tagObjects: object[]): Set => { // then this should be replaced with a an aggregation call. // Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html export const readTags = async ({ - alertsClient, + rulesClient, }: { - alertsClient: AlertsClient; + rulesClient: RulesClient; }): Promise => { - const tags = await readRawTags({ alertsClient }); + const tags = await readRawTags({ rulesClient }); return tags.filter((tag) => !tag.startsWith(INTERNAL_IDENTIFIER)); }; export const readRawTags = async ({ - alertsClient, + rulesClient, }: { - alertsClient: AlertsClient; + rulesClient: RulesClient; perPage?: number; }): Promise => { // Get just one record so we can get the total count const firstTags = await findRules({ - alertsClient, + rulesClient, fields: ['tags'], perPage: 1, page: 1, @@ -66,7 +66,7 @@ export const readRawTags = async ({ }); // Get all the rules to aggregate over all the tags of the rules const rules = await findRules({ - alertsClient, + rulesClient, fields: ['tags'], perPage: firstTags.total, sortField: 'createdAt', diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.test.ts index bee39be6cbd5c..f30f80a4cf14c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.test.ts @@ -46,7 +46,7 @@ describe('installPrepackagedTimelines', () => { authz: {}, } as unknown) as SecurityPluginSetup; - clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); jest.doMock('./helpers', () => { return { diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 9d2e918d4f274..4a346581b7767 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -290,6 +290,7 @@ export class Plugin implements IPlugin > = { ...hostsFactory, + ...uebaFactory, ...matrixHistogramFactory, ...networkFactory, ...ctiFactoryTypes, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/helpers.ts new file mode 100644 index 0000000000000..f9c94eea3ff29 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/helpers.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 { getOr } from 'lodash/fp'; +import { HostRulesHit, HostRulesEdges, HostRulesFields } from '../../../../../../common'; + +export const formatHostRulesData = (buckets: HostRulesHit[]): HostRulesEdges[] => + buckets.map((bucket) => ({ + node: { + _id: bucket.key, + [HostRulesFields.hits]: bucket.doc_count, + [HostRulesFields.riskScore]: getOr(0, 'risk_score.value', bucket), + [HostRulesFields.ruleName]: bucket.key, + [HostRulesFields.ruleType]: getOr(0, 'rule_type.buckets[0].key', bucket), + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/index.ts new file mode 100644 index 0000000000000..39fa7193fd5d2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/index.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { SecuritySolutionFactory } from '../../types'; +import { + HostRulesEdges, + HostRulesRequestOptions, + HostRulesStrategyResponse, + UebaQueries, +} from '../../../../../../common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { buildHostRulesQuery } from './query.host_rules.dsl'; +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { formatHostRulesData } from './helpers'; +import { inspectStringifyObject } from '../../../../../utils/build_query'; + +export const hostRules: SecuritySolutionFactory = { + buildDsl: (options: HostRulesRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + + return buildHostRulesQuery(options); + }, + parse: async ( + options: HostRulesRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.rule_count.value', response.rawResponse); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + + const hostRulesEdges: HostRulesEdges[] = formatHostRulesData( + getOr([], 'aggregations.rule_name.buckets', response.rawResponse) + ); + + const edges = hostRulesEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildHostRulesQuery(options))], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + return { + ...response, + inspect, + edges, + totalCount, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount, + showMorePagesIndicator, + }, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/query.host_rules.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/query.host_rules.dsl.ts new file mode 100644 index 0000000000000..4c116104b3e14 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/query.host_rules.dsl.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash/fp'; +import { Direction, HostRulesRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; + +export const buildHostRulesQuery = ({ + defaultIndex, + docValueFields, + filterQuery, + hostName, + timerange: { from, to }, +}: HostRulesRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + return { + allowNoIndices: true, + index: defaultIndex, // can stop getting this from sourcerer and assume default detections index if we want + ignoreUnavailable: true, + track_total_hits: true, + body: { + ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggs: { + risk_score: { + sum: { + field: 'signal.rule.risk_score', + }, + }, + rule_name: { + terms: { + field: 'signal.rule.name', + order: { + risk_score: Direction.desc, + }, + }, + aggs: { + risk_score: { + sum: { + field: 'signal.rule.risk_score', + }, + }, + rule_type: { + terms: { + field: 'signal.rule.type', + }, + }, + }, + }, + rule_count: { + cardinality: { + field: 'signal.rule.name', + }, + }, + }, + query: { + bool: { + filter, + must: [ + { + term: { + 'host.name': hostName, + }, + }, + ], + }, + }, + size: 0, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/helpers.ts new file mode 100644 index 0000000000000..b20cf4582c824 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/helpers.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 { getOr } from 'lodash/fp'; +import { + HostTacticsHit, + HostTacticsEdges, + HostTacticsFields, + HostTechniqueHit, +} from '../../../../../../common'; + +export const formatHostTacticsData = (buckets: HostTacticsHit[]): HostTacticsEdges[] => + buckets.reduce((acc: HostTacticsEdges[], bucket) => { + return [ + ...acc, + ...getOr([], 'technique.buckets', bucket).map((t: HostTechniqueHit) => ({ + node: { + _id: bucket.key + t.key, + [HostTacticsFields.hits]: t.doc_count, + [HostTacticsFields.riskScore]: getOr(0, 'risk_score.value', t), + [HostTacticsFields.tactic]: bucket.key, + [HostTacticsFields.technique]: t.key, + }, + cursor: { + value: bucket.key + t.key, + tiebreaker: null, + }, + })), + ]; + }, []); +// buckets.map((bucket) => ({ +// node: { +// _id: bucket.key, +// [HostTacticsFields.hits]: bucket.doc_count, +// [HostTacticsFields.riskScore]: getOr(0, 'risk_score.value', bucket), +// [HostTacticsFields.tactic]: bucket.key, +// [HostTacticsFields.technique]: getOr(0, 'technique.buckets[0].key', bucket), +// }, +// cursor: { +// value: bucket.key, +// tiebreaker: null, +// }, +// })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/index.ts new file mode 100644 index 0000000000000..0ba8cbef1d144 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/index.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { SecuritySolutionFactory } from '../../types'; +import { + HostTacticsEdges, + HostTacticsRequestOptions, + HostTacticsStrategyResponse, + UebaQueries, +} from '../../../../../../common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { buildHostTacticsQuery } from './query.host_tactics.dsl'; +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { formatHostTacticsData } from './helpers'; +import { inspectStringifyObject } from '../../../../../utils/build_query'; + +export const hostTactics: SecuritySolutionFactory = { + buildDsl: (options: HostTacticsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + + return buildHostTacticsQuery(options); + }, + parse: async ( + options: HostTacticsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.tactic_count.value', response.rawResponse); + const techniqueCount = getOr(0, 'aggregations.technique_count.value', response.rawResponse); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const hostTacticsEdges: HostTacticsEdges[] = formatHostTacticsData( + getOr([], 'aggregations.tactic.buckets', response.rawResponse) + ); + const edges = hostTacticsEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildHostTacticsQuery(options))], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + return { + ...response, + inspect, + edges, + techniqueCount, + totalCount, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount, + showMorePagesIndicator, + }, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/query.host_tactics.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/query.host_tactics.dsl.ts new file mode 100644 index 0000000000000..ec1afe247011b --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/query.host_tactics.dsl.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash/fp'; +import { HostTacticsRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; + +export const buildHostTacticsQuery = ({ + defaultIndex, + docValueFields, + filterQuery, + hostName, + timerange: { from, to }, +}: HostTacticsRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + return { + allowNoIndices: true, + index: defaultIndex, // can stop getting this from sourcerer and assume default detections index if we want + ignoreUnavailable: true, + track_total_hits: true, + body: { + ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggs: { + risk_score: { + sum: { + field: 'signal.rule.risk_score', + }, + }, + tactic: { + terms: { + field: 'signal.rule.threat.tactic.name', + }, + aggs: { + technique: { + terms: { + field: 'signal.rule.threat.technique.name', + }, + aggs: { + risk_score: { + sum: { + field: 'signal.rule.risk_score', + }, + }, + }, + }, + }, + }, + tactic_count: { + cardinality: { + field: 'signal.rule.threat.tactic.name', + }, + }, + technique_count: { + cardinality: { + field: 'signal.rule.threat.technique.name', + }, + }, + }, + query: { + bool: { + filter, + must: [ + { + term: { + 'host.name': hostName, + }, + }, + ], + }, + }, + size: 0, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/index.ts new file mode 100644 index 0000000000000..90db2ec63260a --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + FactoryQueryTypes, + UebaQueries, +} from '../../../../../common/search_strategy/security_solution'; +import { SecuritySolutionFactory } from '../types'; +import { hostRules } from './host_rules'; +import { hostTactics } from './host_tactics'; +import { riskScore } from './risk_score'; +import { userRules } from './user_rules'; + +export const uebaFactory: Record> = { + [UebaQueries.hostRules]: hostRules, + [UebaQueries.hostTactics]: hostTactics, + [UebaQueries.riskScore]: riskScore, + [UebaQueries.userRules]: userRules, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/helpers.ts new file mode 100644 index 0000000000000..ace2faf819877 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/helpers.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { RiskScoreHit, RiskScoreEdges } from '../../../../../../common'; + +export const formatRiskScoreData = (buckets: RiskScoreHit[]): RiskScoreEdges[] => + buckets.map((bucket) => ({ + node: { + _id: bucket.key, + host_name: bucket.key, + risk_score: getOr(0, 'risk_score.value', bucket), + risk_keyword: getOr(0, 'risk_keyword.buckets[0].key', bucket), + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/index.ts new file mode 100644 index 0000000000000..6b3a956c9c1b7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/index.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { SecuritySolutionFactory } from '../../types'; +import { + RiskScoreEdges, + RiskScoreRequestOptions, + RiskScoreStrategyResponse, + UebaQueries, +} from '../../../../../../common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { buildRiskScoreQuery } from './query.risk_score.dsl'; +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { formatRiskScoreData } from './helpers'; +import { inspectStringifyObject } from '../../../../../utils/build_query'; + +export const riskScore: SecuritySolutionFactory = { + buildDsl: (options: RiskScoreRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + + return buildRiskScoreQuery(options); + }, + parse: async ( + options: RiskScoreRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.host_count.value', response.rawResponse); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + + const riskScoreEdges: RiskScoreEdges[] = formatRiskScoreData( + getOr([], 'aggregations.host_data.buckets', response.rawResponse) + ); + + const edges = riskScoreEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildRiskScoreQuery(options))], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + return { + ...response, + inspect, + edges, + totalCount, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount, + showMorePagesIndicator, + }, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/query.risk_score.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/query.risk_score.dsl.ts new file mode 100644 index 0000000000000..79c50d84e3c92 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/query.risk_score.dsl.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash/fp'; +import { Direction, RiskScoreRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; + +export const buildRiskScoreQuery = ({ + defaultIndex, + docValueFields, + filterQuery, + pagination: { querySize }, + sort, + timerange: { from, to }, +}: RiskScoreRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + return { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + track_total_hits: true, + body: { + ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggregations: { + host_data: { + terms: { + field: 'host.name', + order: { + risk_score: Direction.desc, + }, + }, + aggs: { + risk_score: { + sum: { + field: 'risk_score', + }, + }, + risk_keyword: { + terms: { + field: 'risk.keyword', + }, + }, + }, + }, + host_count: { + cardinality: { + field: 'host.name', + }, + }, + }, + query: { bool: { filter } }, + size: 0, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/helpers.ts new file mode 100644 index 0000000000000..c0f38af37c1f5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/helpers.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 { getOr } from 'lodash/fp'; +import { UserRulesHit, UserRulesFields, UserRulesByUser } from '../../../../../../common'; +import { formatHostRulesData } from '../host_rules/helpers'; + +export const formatUserRulesData = (buckets: UserRulesHit[]): UserRulesByUser[] => + buckets.map((user) => ({ + _id: user.key, + [UserRulesFields.userName]: user.key, + [UserRulesFields.riskScore]: getOr(0, 'risk_score.value', user), + [UserRulesFields.ruleCount]: getOr(0, 'rule_count.value', user), + [UserRulesFields.rules]: formatHostRulesData(getOr([], 'rule_name.buckets', user)), + })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/index.ts new file mode 100644 index 0000000000000..aa525f2c5b741 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/index.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { SecuritySolutionFactory } from '../../types'; +import { + UebaQueries, + UserRulesByUser, + UserRulesFields, + UserRulesRequestOptions, + UserRulesStrategyResponse, + UsersRulesHit, +} from '../../../../../../common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { buildUserRulesQuery } from './query.user_rules.dsl'; +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { formatUserRulesData } from './helpers'; +import { inspectStringifyObject } from '../../../../../utils/build_query'; + +export const userRules: SecuritySolutionFactory = { + buildDsl: (options: UserRulesRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + + return buildUserRulesQuery(options); + }, + parse: async ( + options: UserRulesRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + + const userRulesByUser: UserRulesByUser[] = formatUserRulesData( + getOr([], 'aggregations.user_data.buckets', response.rawResponse) + ); + const inspect = { + dsl: [inspectStringifyObject(buildUserRulesQuery(options))], + }; + return { + ...response, + inspect, + data: userRulesByUser.map((user) => { + const edges = user[UserRulesFields.rules].splice(cursorStart, querySize - cursorStart); + const totalCount = user[UserRulesFields.ruleCount]; + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + + const showMorePagesIndicator = totalCount > fakeTotalCount; + return { + [UserRulesFields.userName]: user[UserRulesFields.userName], + [UserRulesFields.riskScore]: user[UserRulesFields.riskScore], + edges, + totalCount, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount, + showMorePagesIndicator, + }, + }; + }), + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/query.user_rules.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/query.user_rules.dsl.ts new file mode 100644 index 0000000000000..c2242ff00a6c1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/query.user_rules.dsl.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash/fp'; +import { Direction, UserRulesRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; + +export const buildUserRulesQuery = ({ + defaultIndex, + docValueFields, + filterQuery, + hostName, + timerange: { from, to }, +}: UserRulesRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + return { + allowNoIndices: true, + index: defaultIndex, // can stop getting this from sourcerer and assume default detections index if we want + ignoreUnavailable: true, + track_total_hits: true, + body: { + ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggs: { + user_data: { + terms: { + field: 'user.name', + order: { + risk_score: Direction.desc, + }, + size: 20, + }, + aggs: { + risk_score: { + sum: { + field: 'signal.rule.risk_score', + }, + }, + rule_name: { + terms: { + field: 'signal.rule.name', + order: { + risk_score: Direction.desc, + }, + }, + aggs: { + risk_score: { + sum: { + field: 'signal.rule.risk_score', + }, + }, + rule_type: { + terms: { + field: 'signal.rule.type', + }, + }, + }, + }, + rule_count: { + cardinality: { + field: 'signal.rule.name', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + must: [ + { + term: { + 'host.name': hostName, + }, + }, + ], + }, + }, + size: 0, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/ui_settings.ts b/x-pack/plugins/security_solution/server/ui_settings.ts index 259c0f2ae2f92..611860929e25e 100644 --- a/x-pack/plugins/security_solution/server/ui_settings.ts +++ b/x-pack/plugins/security_solution/server/ui_settings.ts @@ -13,6 +13,7 @@ import { APP_ID, DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN, + DEFAULT_INDEX_PATTERN_EXPERIMENTAL, DEFAULT_ANOMALY_SCORE, DEFAULT_APP_TIME_RANGE, DEFAULT_APP_REFRESH_INTERVAL, @@ -88,7 +89,9 @@ export const initUiSettings = ( }), sensitive: true, - value: DEFAULT_INDEX_PATTERN, + value: experimentalFeatures.uebaEnabled + ? [...DEFAULT_INDEX_PATTERN, ...DEFAULT_INDEX_PATTERN_EXPERIMENTAL] + : DEFAULT_INDEX_PATTERN, description: i18n.translate('xpack.securitySolution.uiSettings.defaultIndexDescription', { defaultMessage: '

Comma-delimited list of Elasticsearch indices from which the Security app collects events.

', diff --git a/x-pack/plugins/spaces/server/saved_objects/migrations/index.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/index.ts index 01a08afdcefcc..7935aaac3cc92 100644 --- a/x-pack/plugins/spaces/server/saved_objects/migrations/index.ts +++ b/x-pack/plugins/spaces/server/saved_objects/migrations/index.ts @@ -5,4 +5,7 @@ * 2.0. */ -export { migrateToKibana660 } from './migrate_6x'; +import * as spaceMigrations from './space_migrations'; +import * as usageStatsMigrations from './usage_stats_migrations'; + +export { spaceMigrations, usageStatsMigrations }; diff --git a/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.test.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.test.ts similarity index 61% rename from x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.test.ts rename to x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.test.ts index 71d467f6f56d0..62a3d98662939 100644 --- a/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.test.ts @@ -5,23 +5,18 @@ * 2.0. */ -import type { SavedObjectMigrationContext } from 'src/core/server'; +import type { Space } from 'src/plugins/spaces_oss/common'; -import { migrateToKibana660 } from './migrate_6x'; - -const mockContext = {} as SavedObjectMigrationContext; +import { migrateTo660 } from './space_migrations'; describe('migrateTo660', () => { it('adds a "disabledFeatures" attribute initialized as an empty array', () => { expect( - migrateToKibana660( - { - id: 'space:foo', - type: 'space', - attributes: {}, - }, - mockContext - ) + migrateTo660({ + id: 'space:foo', + type: 'space', + attributes: {} as Space, + }) ).toEqual({ id: 'space:foo', type: 'space', @@ -34,16 +29,13 @@ describe('migrateTo660', () => { it('does not initialize "disabledFeatures" if the property already exists', () => { // This scenario shouldn't happen organically. Protecting against defects in the migration. expect( - migrateToKibana660( - { - id: 'space:foo', - type: 'space', - attributes: { - disabledFeatures: ['foo', 'bar', 'baz'], - }, - }, - mockContext - ) + migrateTo660({ + id: 'space:foo', + type: 'space', + attributes: { + disabledFeatures: ['foo', 'bar', 'baz'], + } as Space, + }) ).toEqual({ id: 'space:foo', type: 'space', diff --git a/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.ts similarity index 65% rename from x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts rename to x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.ts index 973d6ec1751a2..d88db2b0181dd 100644 --- a/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts +++ b/x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.ts @@ -5,9 +5,10 @@ * 2.0. */ -import type { SavedObjectMigrationFn } from 'src/core/server'; +import type { SavedObjectUnsanitizedDoc } from 'src/core/server'; +import type { Space } from 'src/plugins/spaces_oss/common'; -export const migrateToKibana660: SavedObjectMigrationFn = (doc) => { +export const migrateTo660 = (doc: SavedObjectUnsanitizedDoc) => { if (!doc.attributes.hasOwnProperty('disabledFeatures')) { doc.attributes.disabledFeatures = []; } diff --git a/x-pack/plugins/spaces/server/saved_objects/migrations/usage_stats_migrations.test.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/usage_stats_migrations.test.ts new file mode 100644 index 0000000000000..3cece7dcbb553 --- /dev/null +++ b/x-pack/plugins/spaces/server/saved_objects/migrations/usage_stats_migrations.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectUnsanitizedDoc } from 'src/core/server'; + +import type { UsageStats } from '../../usage_stats'; +import { migrateTo7141 } from './usage_stats_migrations'; + +const type = 'obj-type'; +const id = 'obj-id'; + +describe('#migrateTo7141', () => { + it('Resets targeted counter fields and leaves others unchanged', () => { + const doc = { + type, + id, + attributes: { + foo: 'bar', + 'apiCalls.copySavedObjects.total': 10, + }, + } as SavedObjectUnsanitizedDoc; + + expect(migrateTo7141(doc)).toEqual({ + type, + id, + attributes: { + foo: 'bar', + 'apiCalls.copySavedObjects.total': 0, + 'apiCalls.copySavedObjects.kibanaRequest.yes': 0, + 'apiCalls.copySavedObjects.kibanaRequest.no': 0, + 'apiCalls.copySavedObjects.createNewCopiesEnabled.yes': 0, + 'apiCalls.copySavedObjects.createNewCopiesEnabled.no': 0, + 'apiCalls.copySavedObjects.overwriteEnabled.yes': 0, + 'apiCalls.copySavedObjects.overwriteEnabled.no': 0, + }, + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/saved_objects/migrations/usage_stats_migrations.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/usage_stats_migrations.ts new file mode 100644 index 0000000000000..974b9ee1078b8 --- /dev/null +++ b/x-pack/plugins/spaces/server/saved_objects/migrations/usage_stats_migrations.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cloneDeep } from 'lodash'; + +import type { SavedObjectUnsanitizedDoc } from 'src/core/server'; + +import type { UsageStats } from '../../usage_stats'; + +export const migrateTo7141 = (doc: SavedObjectUnsanitizedDoc) => { + try { + return resetFields(doc, [ + // Prior to this, we were counting the `overwrite` option incorrectly; reset all copy API counter fields so we get clean data + 'apiCalls.copySavedObjects.total', + 'apiCalls.copySavedObjects.kibanaRequest.yes', + 'apiCalls.copySavedObjects.kibanaRequest.no', + 'apiCalls.copySavedObjects.createNewCopiesEnabled.yes', + 'apiCalls.copySavedObjects.createNewCopiesEnabled.no', + 'apiCalls.copySavedObjects.overwriteEnabled.yes', + 'apiCalls.copySavedObjects.overwriteEnabled.no', + ]); + } catch (err) { + // fail-safe + } + return doc; +}; + +function resetFields( + doc: SavedObjectUnsanitizedDoc, + fieldsToReset: Array +) { + const newDoc = cloneDeep(doc); + const { attributes = {} } = newDoc; + for (const field of fieldsToReset) { + attributes[field] = 0; + } + return { ...newDoc, attributes }; +} diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts index b1bf7fe580a03..2e2bad9ff0994 100644 --- a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts @@ -10,7 +10,7 @@ import type { CoreSetup } from 'src/core/server'; import type { SpacesServiceStart } from '../spaces_service'; import { SPACES_USAGE_STATS_TYPE } from '../usage_stats'; import { SpacesSavedObjectMappings, UsageStatsMappings } from './mappings'; -import { migrateToKibana660 } from './migrations'; +import { spaceMigrations, usageStatsMigrations } from './migrations'; import { spacesSavedObjectsClientWrapperFactory } from './saved_objects_client_wrapper_factory'; interface SetupDeps { @@ -26,7 +26,7 @@ export class SpacesSavedObjectsService { namespaceType: 'agnostic', mappings: SpacesSavedObjectMappings, migrations: { - '6.6.0': migrateToKibana660, + '6.6.0': spaceMigrations.migrateTo660, }, }); @@ -35,6 +35,9 @@ export class SpacesSavedObjectsService { hidden: true, namespaceType: 'agnostic', mappings: UsageStatsMappings, + migrations: { + '7.14.1': usageStatsMigrations.migrateTo7141, + }, }); core.savedObjects.addClientWrapper( diff --git a/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.test.ts b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.test.ts index c5b63a4d007b9..aa3fa29e87ce1 100644 --- a/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.test.ts +++ b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.test.ts @@ -117,14 +117,32 @@ describe('UsageStatsClient', () => { createNewCopies: true, overwrite: true, } as IncrementCopySavedObjectsOptions); - expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); - expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + await usageStatsClient.incrementCopySavedObjects({ + headers: firstPartyRequestHeaders, + createNewCopies: false, + overwrite: true, + } as IncrementCopySavedObjectsOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(2); + expect(repositoryMock.incrementCounter).toHaveBeenNthCalledWith( + 1, SPACES_USAGE_STATS_TYPE, SPACES_USAGE_STATS_ID, [ `${COPY_STATS_PREFIX}.total`, `${COPY_STATS_PREFIX}.kibanaRequest.yes`, `${COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`, + // excludes 'overwriteEnabled.yes' and 'overwriteEnabled.no' when createNewCopies is true + ], + incrementOptions + ); + expect(repositoryMock.incrementCounter).toHaveBeenNthCalledWith( + 2, + SPACES_USAGE_STATS_TYPE, + SPACES_USAGE_STATS_ID, + [ + `${COPY_STATS_PREFIX}.total`, + `${COPY_STATS_PREFIX}.kibanaRequest.yes`, + `${COPY_STATS_PREFIX}.createNewCopiesEnabled.no`, `${COPY_STATS_PREFIX}.overwriteEnabled.yes`, ], incrementOptions diff --git a/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts index 5dcf106d6cfb4..e73d6db948bb4 100644 --- a/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts +++ b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts @@ -71,7 +71,7 @@ export class UsageStatsClient { 'total', `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, `createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`, - `overwriteEnabled.${overwrite ? 'yes' : 'no'}`, + ...(!createNewCopies ? [`overwriteEnabled.${overwrite ? 'yes' : 'no'}`] : []), // the overwrite option is ignored when createNewCopies is true ]; await this.updateUsageStats(counterFieldNames, COPY_STATS_PREFIX); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts index 37e0a293b03a0..9a95517986bee 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts @@ -9,12 +9,12 @@ import { ElasticsearchClient } from 'kibana/server'; import { Logger } from 'src/core/server'; import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { - Query, - IIndexPattern, fromKueryExpression, toElasticsearchQuery, luceneStringToDsl, -} from '../../../../../../src/plugins/data/common'; + IndexPatternBase, + Query, +} from '@kbn/es-query'; export const OTHER_CATEGORY = 'other'; // Consider dynamically obtaining from config? @@ -22,7 +22,7 @@ const MAX_TOP_LEVEL_QUERY_SIZE = 0; const MAX_SHAPES_QUERY_SIZE = 10000; const MAX_BUCKETS_LIMIT = 65535; -export const getEsFormattedQuery = (query: Query, indexPattern?: IIndexPattern) => { +export const getEsFormattedQuery = (query: Query, indexPattern?: IndexPatternBase) => { let esFormattedQuery; const queryLanguage = query.language; diff --git a/x-pack/plugins/task_manager/server/routes/health.test.ts b/x-pack/plugins/task_manager/server/routes/health.test.ts index fd7e37e0fe9a5..68913fef43a2a 100644 --- a/x-pack/plugins/task_manager/server/routes/health.test.ts +++ b/x-pack/plugins/task_manager/server/routes/health.test.ts @@ -29,7 +29,8 @@ jest.mock('../lib/log_health_metrics', () => ({ logHealthMetrics: jest.fn(), })); -describe('healthRoute', () => { +// FLAKY: https://github.com/elastic/kibana/issues/106388 +describe.skip('healthRoute', () => { beforeEach(() => { jest.resetAllMocks(); }); diff --git a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts index 76ab48a8243db..8fbab31b49266 100644 --- a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts @@ -5,12 +5,8 @@ * 2.0. */ -import { IIndexPattern } from 'src/plugins/data/public'; -import { - IEsSearchRequest, - IEsSearchResponse, - IFieldSubType, -} from '../../../../../../src/plugins/data/common'; +import { IFieldSubType, IndexPatternBase } from '@kbn/es-query'; +import { IEsSearchRequest, IEsSearchResponse } from '../../../../../../src/plugins/data/common'; import { DocValueFields, Maybe } from '../common'; export type BeatFieldsFactoryQueryType = 'beatFields'; @@ -83,7 +79,7 @@ export type BrowserFields = Readonly>>; export const EMPTY_BROWSER_FIELDS = {}; export const EMPTY_DOCVALUE_FIELD: DocValueFields[] = []; -export const EMPTY_INDEX_PATTERN: IIndexPattern = { +export const EMPTY_INDEX_PATTERN: IndexPatternBase = { fields: [], title: '', }; diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts index f29dc4a3c7450..9a2d884af948f 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts @@ -14,6 +14,7 @@ export enum LastEventIndexKey { hosts = 'hosts', ipDetails = 'ipDetails', network = 'network', + ueba = 'ueba', // TODO: Steph/ueba implement this } export interface LastTimeDetails { diff --git a/x-pack/plugins/timelines/common/typed_json.ts b/x-pack/plugins/timelines/common/typed_json.ts index 71ece54777871..c639c1c0322dc 100644 --- a/x-pack/plugins/timelines/common/typed_json.ts +++ b/x-pack/plugins/timelines/common/typed_json.ts @@ -6,7 +6,7 @@ */ import { JsonObject } from '@kbn/common-utils'; -import { DslQuery, Filter } from 'src/plugins/data/common'; +import { DslQuery, Filter } from '@kbn/es-query'; export type ESQuery = | ESRangeQuery diff --git a/x-pack/plugins/timelines/common/types/timeline/columns/index.ts b/x-pack/plugins/timelines/common/types/timeline/columns/index.ts index 61f0c6a0b8f23..9161ea3ea78ce 100644 --- a/x-pack/plugins/timelines/common/types/timeline/columns/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/columns/index.ts @@ -6,8 +6,7 @@ */ import { EuiDataGridColumn } from '@elastic/eui'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IFieldSubType } from '../../../../../../../src/plugins/data/public'; +import { IFieldSubType } from '../../../../../../../src/plugins/data/common'; import { TimelineNonEcsData } from '../../../search_strategy/timeline'; export type ColumnHeaderType = 'not-filtered' | 'text-filter'; diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts index c0bc1c305b970..36a5d31bd6904 100644 --- a/x-pack/plugins/timelines/common/types/timeline/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -314,6 +314,7 @@ export enum TimelineId { detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', + uebaPageExternalAlerts = 'ueba-page-external-alerts', active = 'timeline-1', casePage = 'timeline-case', test = 'test', // Reserved for testing purposes @@ -326,6 +327,7 @@ export const TimelineIdLiteralRt = runtimeTypes.union([ runtimeTypes.literal(TimelineId.detectionsRulesDetailsPage), runtimeTypes.literal(TimelineId.detectionsPage), runtimeTypes.literal(TimelineId.networkPageExternalAlerts), + runtimeTypes.literal(TimelineId.uebaPageExternalAlerts), runtimeTypes.literal(TimelineId.active), runtimeTypes.literal(TimelineId.test), ]); diff --git a/x-pack/plugins/timelines/public/components/clipboard/index.tsx b/x-pack/plugins/timelines/public/components/clipboard/index.tsx index ed4b143b2bca1..23a0a629d17a1 100644 --- a/x-pack/plugins/timelines/public/components/clipboard/index.tsx +++ b/x-pack/plugins/timelines/public/components/clipboard/index.tsx @@ -66,6 +66,7 @@ export const Clipboard = ({ aria-label={i18n.COPY_TO_THE_CLIPBOARD} className={className} data-test-subj="clipboard" + iconSize="s" iconType="copyClipboard" onClick={onClick} > diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx index f62d29c8b648f..421cd0089c1b7 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx @@ -23,11 +23,16 @@ export const filterForValueFn = ({ filterManager, onFilterAdded, }: FilterValueFnArgs): void => { - const filter = value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); + const makeFilter = (currentVal: string | null | undefined) => + currentVal?.length === 0 ? createFilter(field, undefined) : createFilter(field, currentVal); + const filters = Array.isArray(value) + ? value.map((currentVal: string | null | undefined) => makeFilter(currentVal)) + : makeFilter(value); + const activeFilterManager = filterManager; if (activeFilterManager != null) { - activeFilterManager.addFilters(filter); + activeFilterManager.addFilters(filters); if (onFilterAdded != null) { onFilterAdded(); } diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx index a0888ac7c6e11..bfa7848025bf4 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx @@ -24,12 +24,18 @@ export const filterOutValueFn = ({ filterManager, onFilterAdded, }: FilterValueFnArgs) => { - const filter = - value?.length === 0 ? createFilter(field, null, false) : createFilter(field, value, true); + const makeFilter = (currentVal: string | null | undefined) => + currentVal?.length === 0 + ? createFilter(field, null, false) + : createFilter(field, currentVal, true); + const filters = Array.isArray(value) + ? value.map((currentVal: string | null | undefined) => makeFilter(currentVal)) + : makeFilter(value); + const activeFilterManager = filterManager; if (activeFilterManager != null) { - activeFilterManager.addFilters(filter); + activeFilterManager.addFilters(filters); if (onFilterAdded != null) { onFilterAdded(); } diff --git a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx index fc040522f3e15..6cfd97c8bad55 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import type { Filter, EsQueryConfig, Query } from '@kbn/es-query'; import { isEmpty, get } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import { @@ -14,12 +15,7 @@ import { handleSkipFocus, stopPropagationAndPreventDefault, } from '../../../common'; -import type { - EsQueryConfig, - Filter, - IIndexPattern, - Query, -} from '../../../../../../src/plugins/data/public'; +import type { IIndexPattern } from '../../../../../../src/plugins/data/public'; import type { BrowserFields } from '../../../common/search_strategy/index_fields'; import { DataProviderType, EXISTS_OPERATOR } from '../../../common/types/timeline'; // eslint-disable-next-line no-duplicate-imports diff --git a/x-pack/plugins/timelines/public/components/utils/keury/index.ts b/x-pack/plugins/timelines/public/components/utils/keury/index.ts index e31d682fd7021..391b15e8fdbac 100644 --- a/x-pack/plugins/timelines/public/components/utils/keury/index.ts +++ b/x-pack/plugins/timelines/public/components/utils/keury/index.ts @@ -7,15 +7,9 @@ import { isEmpty, isString, flow } from 'lodash/fp'; import { JsonObject } from '@kbn/common-utils'; +import { EsQueryConfig, Filter, Query } from '@kbn/es-query'; -import { - EsQueryConfig, - Query, - Filter, - esQuery, - esKuery, - IIndexPattern, -} from '../../../../../../../src/plugins/data/public'; +import { esQuery, esKuery, IIndexPattern } from '../../../../../../../src/plugins/data/public'; export const convertKueryToElasticSearchQuery = ( kueryExpression: string, diff --git a/x-pack/plugins/timelines/public/mock/mock_hover_actions.tsx b/x-pack/plugins/timelines/public/mock/mock_hover_actions.tsx new file mode 100644 index 0000000000000..52ea1fa827136 --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/mock_hover_actions.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +/* eslint-disable react/display-name */ +export const mockHoverActions = { + addToTimeline: { + AddToTimelineButton: () => <>{'Add To Timeline'}, + keyboardShortcut: 'timelineAddShortcut', + useGetHandleStartDragToTimeline: () => jest.fn, + }, + columnToggle: { + ColumnToggleButton: () => <>{'Column Toggle'}, + columnToggleFn: jest.fn, + keyboardShortcut: 'columnToggleShortcut', + }, + copy: { + CopyButton: () => <>{'Copy button'}, + keyboardShortcut: 'copyShortcut', + }, + filterForValue: { + FilterForValueButton: () => <>{'Filter button'}, + filterForValueFn: jest.fn, + keyboardShortcut: 'filterForShortcut', + }, + filterOutValue: { + FilterOutValueButton: () => <>{'Filter out button'}, + filterOutValueFn: jest.fn, + keyboardShortcut: 'filterOutShortcut', + }, +}; diff --git a/x-pack/plugins/timelines/public/mock/plugin_mock.tsx b/x-pack/plugins/timelines/public/mock/plugin_mock.tsx index 64be3306e7aa6..90fb076592001 100644 --- a/x-pack/plugins/timelines/public/mock/plugin_mock.tsx +++ b/x-pack/plugins/timelines/public/mock/plugin_mock.tsx @@ -13,8 +13,10 @@ import { useDraggableKeyboardWrapper, } from '../components'; import { useAddToTimeline, useAddToTimelineSensor } from '../hooks/use_add_to_timeline'; +import { mockHoverActions } from './mock_hover_actions'; export const createTGridMocks = () => ({ + getHoverActions: () => mockHoverActions, // eslint-disable-next-line react/display-name getTGrid: () => <>{'hello grid'}, // eslint-disable-next-line react/display-name diff --git a/x-pack/plugins/timelines/public/store/t_grid/types.ts b/x-pack/plugins/timelines/public/store/t_grid/types.ts index c8c72e0310958..41f69b9f55d0d 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/types.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/types.ts @@ -45,6 +45,7 @@ export enum TimelineId { detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', + uebaPageExternalAlerts = 'ueba-page-external-alerts', active = 'timeline-1', casePage = 'timeline-case', test = 'test', // Reserved for testing purposes diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts index c1b567b99cfb1..b40dc728741ed 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts @@ -42,7 +42,8 @@ export const timelineEventsAll: TimelineFactory = { const hits = response.rawResponse.hits.hits; if (fieldRequested.includes('*') && hits.length > 0) { - fieldRequested = Object.keys(hits[0]?.fields ?? {}).reduce((acc, f) => { + const fieldsReturned = hits.flatMap((hit) => Object.keys(hit.fields ?? {})); + fieldRequested = fieldsReturned.reduce((acc, f) => { if (!acc.includes(f)) { return [...acc, f]; } @@ -59,6 +60,7 @@ export const timelineEventsAll: TimelineFactory = { ) ) ); + const inspect = { dsl: [inspectStringifyObject(buildTimelineEventsAllQuery(queryOptions))], }; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts index 6d3d8ac3c55aa..0fc6ce78ee982 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts @@ -82,6 +82,7 @@ export const buildLastEventTimeQuery = ({ throw new Error('buildLastEventTimeQuery - no hostName argument provided'); case LastEventIndexKey.hosts: case LastEventIndexKey.network: + case LastEventIndexKey.ueba: return { allowNoIndices: true, index: indicesToQuery[indexKey], diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ad3a58e0e9584..5a09667e2a327 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -114,6 +114,7 @@ "advancedSettings.form.cancelButtonLabel": "変更をキャンセル", "advancedSettings.form.clearNoSearchResultText": " (検索結果を消去) ", "advancedSettings.form.clearSearchResultText": " (検索結果を消去) ", + "advancedSettings.form.noSearchResultText": "{queryText} {clearSearch}の設定が見つかりません", "advancedSettings.form.requiresPageReloadToastButtonLabel": "ページを再読み込み", "advancedSettings.form.requiresPageReloadToastDescription": "設定を有効にするためにページの再読み込みが必要です。", "advancedSettings.form.saveButtonLabel": "変更を保存", @@ -124,7 +125,9 @@ "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", "advancedSettings.searchBarAriaLabel": "高度な設定を検索", "advancedSettings.voiceAnnouncement.ariaLabel": "詳細設定結果情報", - "charts.advancedSettings.visualization.colorMappingText": "互換性パレットを使用して、値をグラフの特定の色にマッピングします。", + "bfetch.disableBfetchCompression": "バッチ圧縮を無効にする", + "bfetch.disableBfetchCompressionDesc": "バッチ圧縮を無効にします。個別の要求をデバッグできますが、応答サイズが大きくなります。", + "charts.advancedSettings.visualization.colorMappingText": "互換性パレットを使用して、グラフで値を特定の色にマッピング色にマッピングします。", "charts.advancedSettings.visualization.colorMappingTextDeprecation": "この設定はサポートが終了し、Kibana 8.0 ではサポートされません。", "charts.advancedSettings.visualization.colorMappingTitle": "カラーマッピング", "charts.colormaps.bluesText": "青", @@ -139,6 +142,7 @@ "charts.functions.palette.args.colorHelpText": "パレットの色です。{html} カラー名、{hex}、{hsl}、{hsla}、{rgb}、または {rgba} を使用できます。", "charts.functions.palette.args.gradientHelpText": "サポートされている場合グラデーションパレットを作成しますか?", "charts.functions.palette.args.reverseHelpText": "パレットを反転させますか?", + "charts.functions.palette.args.stopHelpText": "パレットの色経由点。使用するときには、各色に関連付ける必要があります。", "charts.functions.paletteHelpText": "カラーパレットを作成します。", "charts.functions.systemPalette.args.nameHelpText": "パレットリストのパレットの名前", "charts.functions.systemPaletteHelpText": "動的カラーパレットを作成します。", @@ -158,6 +162,7 @@ "charts.partialData.bucketTooltipText": "選択された時間範囲にはこのバケット全体は含まれていません。一部データが含まれている可能性があります。", "console.autocomplete.addMethodMetaText": "メソド", "console.consoleDisplayName": "コンソール", + "console.consoleMenu.copyAsCurlFailedMessage": "要求をcURLとしてコピーできませんでした", "console.consoleMenu.copyAsCurlMessage": "リクエストが URL としてコピーされました", "console.devToolsDescription": "コンソールでデータを操作するには、cURLをスキップして、JSONインターフェイスを使用します。", "console.devToolsTitle": "Elasticsearch APIとの連携", @@ -233,6 +238,7 @@ "console.welcomePage.quickTipsTitle": "今のうちにいくつか簡単なコツをお教えします", "console.welcomePage.supportedRequestFormatDescription": "リクエストの入力中、コンソールが候補を提案するので、Enter/Tabを押して確定できます。これらの候補はリクエストの構造、およびインデックス、タイプに基づくものです。", "console.welcomePage.supportedRequestFormatTitle": "コンソールは cURL と同様に、コンパクトなフォーマットのリクエストを理解できます。", + "core.application.appContainer.loadingAriaLabel": "アプリケーションを読み込んでいます", "core.application.appNotFound.pageDescription": "この URL にアプリケーションが見つかりませんでした。前の画面に戻るか、メニューからアプリを選択してみてください。", "core.application.appNotFound.title": "アプリケーションが見つかりません", "core.application.appRenderError.defaultTitle": "アプリケーションエラー", @@ -244,7 +250,6 @@ "core.euiBasicTable.selectThisRow": "この行を選択", "core.euiBasicTable.tableAutoCaptionWithoutPagination": "この表には{itemCount}行あります。", "core.euiBasicTable.tableCaptionWithPagination": "{tableCaption}; {page}/{pageCount}ページ。", - "core.euiBasicTable.tableDescriptionWithoutPagination": "この表には{totalItemCount}行中{itemCount}行あります; {page}/{pageCount}ページ。", "core.euiBasicTable.tablePagination": "前の表のページネーション: {tableCaption}", "core.euiBasicTable.tableSimpleAutoCaptionWithPagination": "この表には{itemCount}行あります; {page}/{pageCount}ページ。", "core.euiBottomBar.customScreenReaderAnnouncement": "ドキュメントの最後には、新しいリージョンランドマーク{landmarkHeading}とページレベルのコントロールがあります。", @@ -266,23 +271,23 @@ "core.euiColorPicker.closeLabel": "下矢印キーを押すと、色オプションを含むポップオーバーが開きます", "core.euiColorPicker.colorErrorMessage": "無効な色値", "core.euiColorPicker.colorLabel": "色値", - "core.euiColorPicker.openLabel": "Escキーを押すと、ポップオーバーを閉じます", + "core.euiColorPicker.openLabel": "Escapeキーを押すと、ポップオーバーを閉じます", "core.euiColorPicker.screenReaderAnnouncement": "選択可能な色の範囲を表示するポップアップが開きました。選択可能な色を閲覧するには Tab を押し、Esc でこのポップアップを閉じます。", "core.euiColorPicker.swatchAriaLabel": "{swatch} を色として選択します", "core.euiColorPicker.transparent": "透明", "core.euiColorStops.screenReaderAnnouncement": "{label}:{readOnly} {disabled}色終了位置ピッカー。各終了には数値と対応するカラー値があります。上下矢印キーを使用して、個別の終了を選択します。Enterキーを押すと、新しい終了を作成します。", - "core.euiColorStopThumb.buttonAriaLabel": "Enterキーを押すと、この点を変更します。Escキーを押すと、グループにフォーカスします", + "core.euiColorStopThumb.buttonAriaLabel": "Enterキーを押すと、この点を変更します。Escapeキーを押すと、グループにフォーカスします", "core.euiColorStopThumb.buttonTitle": "クリックすると編集できます。ドラッグすると再配置できます", "core.euiColorStopThumb.removeLabel": "この終了を削除", "core.euiColorStopThumb.screenReaderAnnouncement": "カラー終了編集フォームのポップアップが開きました。Tabを押してフォームコントロールを閲覧するか、Escでこのポップアップを閉じます。", "core.euiColorStopThumb.stopErrorMessage": "値が範囲外です", - "core.euiColorStopThumb.stopLabel": "終了値", + "core.euiColorStopThumb.stopLabel": "点値", "core.euiColumnActions.moveLeft": "左に移動", "core.euiColumnActions.moveRight": "右に移動", "core.euiColumnActions.sort": "{schemaLabel}を並べ替える", "core.euiColumnSelector.button": "列", - "core.euiColumnSelector.buttonActivePlural": "{numberOfHiddenFields}列が非表示です", - "core.euiColumnSelector.buttonActiveSingular": "{numberOfHiddenFields}列が非表示です", + "core.euiColumnSelector.buttonActivePlural": "{numberOfHiddenFields}個の列が非表示です", + "core.euiColumnSelector.buttonActiveSingular": "{numberOfHiddenFields}個の列が非表示です", "core.euiColumnSelector.hideAll": "すべて非表示", "core.euiColumnSelector.search": "検索", "core.euiColumnSelector.searchcolumns": "列を検索", @@ -329,10 +334,9 @@ "core.euiDataGridSchema.numberSortTextAsc": "低-高", "core.euiDataGridSchema.numberSortTextDesc": "高-低", "core.euiFieldPassword.maskPassword": "パスワードをマスク", - "core.euiFieldPassword.showPassword": "パスワードをプレーンテキストとして表示します。注記:こうすると、パスワードが誰にでも見える形で画面に表示されます。", + "core.euiFieldPassword.showPassword": "プレーンテキストとしてパスワードを表示します。注記:パスワードは画面上に見えるように表示されます。", "core.euiFilePicker.clearSelectedFiles": "選択したファイルを消去", "core.euiFilePicker.filesSelected": "選択されたファイル", - "core.euiFilterButton.filterBadge": "${count} ${filterCountLabel} 個のフィルター", "core.euiFlyout.closeAriaLabel": "このダイアログを閉じる", "core.euiForm.addressFormErrors": "ハイライトされたエラーを修正してください。", "core.euiFormControlLayoutClearButton.label": "インプットを消去", @@ -357,8 +361,6 @@ "core.euiMarkdownEditorToolbar.editor": "エディター", "core.euiMarkdownEditorToolbar.previewMarkdown": "プレビュー", "core.euiModal.closeModal": "このモーダルウィンドウを閉じます", - "core.euiNotificationEventMessages.accordionAriaLabelButtonText": "+ {messagesLength}以上", - "core.euiNotificationEventMessages.accordionButtonText": "+ {eventName}の{messagesLength}メッセージ", "core.euiNotificationEventMessages.accordionHideText": "非表示", "core.euiNotificationEventMeta.contextMenuButton": "{eventName}のメニュー", "core.euiNotificationEventReadButton.markAsRead": "既読に設定", @@ -367,8 +369,8 @@ "core.euiNotificationEventReadButton.markAsUnreadAria": "{eventName}を未読に設定", "core.euiPagination.disabledNextPage": "次のページ", "core.euiPagination.disabledPreviousPage": "前のページ", - "core.euiPagination.firstRangeAriaLabel": "ページ2から{lastPage}にスキップしています", - "core.euiPagination.lastRangeAriaLabel": "ページ{firstPage}から{lastPage}にスキップしています", + "core.euiPagination.firstRangeAriaLabel": "ページ2を{lastPage}にスキップしています", + "core.euiPagination.lastRangeAriaLabel": "ページ{firstPage}を{lastPage}にスキップしています", "core.euiPagination.nextPage": "次のページ、{page}", "core.euiPagination.previousPage": "前のページ、{page}", "core.euiPaginationButton.longPageString": "{page}/{totalPages}ページ", @@ -555,6 +557,10 @@ "core.ui.primaryNavSection.screenReaderLabel": "プライマリナビゲーションリンク、{category}", "core.ui.primaryNavSection.undockAriaLabel": "プライマリナビゲーションリンクの固定を解除する", "core.ui.primaryNavSection.undockLabel": "ナビゲーションの固定を解除する", + "core.ui.publicBaseUrlWarning.configMissingDescription": "{configKey}が見つかりません。本番環境を実行するときに構成してください。一部の機能が正常に動作しない場合があります。", + "core.ui.publicBaseUrlWarning.configMissingTitle": "構成がありません", + "core.ui.publicBaseUrlWarning.muteWarningButtonLabel": "ミュート警告", + "core.ui.publicBaseUrlWarning.seeDocumentationLinkLabel": "ドキュメントを参照してください。", "core.ui.recentLinks.linkItem.screenReaderLabel": "{recentlyAccessedItemLinklabel}、タイプ:{pageType}", "core.ui.recentlyViewed": "最近閲覧", "core.ui.recentlyViewedAriaLabel": "最近閲覧したリンク", @@ -608,7 +614,10 @@ "dashboard.featureCatalogue.dashboardTitle": "ダッシュボード", "dashboard.fillDashboardTitle": "このダッシュボードは空です。コンテンツを追加しましょう!", "dashboard.helpMenu.appName": "ダッシュボード", + "dashboard.howToStartWorkingOnNewDashboardDescription": "上のメニューバーで[編集]をクリックすると、パネルの追加を開始します。", "dashboard.howToStartWorkingOnNewDashboardEditLinkAriaLabel": "ダッシュボードを編集", + "dashboard.labs.enableLabsDescription": "このフラグはビューアーで[ラボ]ボタンを使用できるかどうかを決定します。ダッシュボードで実験的機能を有効および無効にするための簡単な方法です。", + "dashboard.labs.enableUI": "ダッシュボードで[ラボ]ボタンを有効にする", "dashboard.listing.createNewDashboard.combineDataViewFromKibanaAppDescription": "あらゆるKibanaアプリからダッシュボードにデータビューを組み合わせて、すべてを1か所に表示できます。", "dashboard.listing.createNewDashboard.createButtonLabel": "新規ダッシュボードを作成", "dashboard.listing.createNewDashboard.newToKibanaDescription": "Kibanaは初心者ですか?{sampleDataInstallLink}してお試しください。", @@ -652,6 +661,8 @@ "dashboard.panelStorageError.setError": "保存されていない変更の設定中にエラーが発生しました。{message}", "dashboard.placeholder.factory.displayName": "プレースホルダー", "dashboard.savedDashboard.newDashboardTitle": "新規ダッシュボード", + "dashboard.solutionToolbar.addPanelButtonLabel": "ビジュアライゼーションを作成", + "dashboard.solutionToolbar.editorMenuButtonLabel": "すべてのタイプ", "dashboard.strings.dashboardEditTitle": "{title}を編集中", "dashboard.topNav.cloneModal.cancelButtonLabel": "キャンセル", "dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "ダッシュボードのクローンを作成", @@ -660,11 +671,12 @@ "dashboard.topNav.cloneModal.dashboardExistsDescription": "{confirmClone}をクリックして重複タイトルでダッシュボードのクローンを作成します。", "dashboard.topNav.cloneModal.dashboardExistsTitle": "「{newDashboardName}」というタイトルのダッシュボードがすでに存在します。", "dashboard.topNav.cloneModal.enterNewNameForDashboardDescription": "ダッシュボードの新しい名前を入力してください。", + "dashboard.topNav.labsButtonAriaLabel": "ラボ", + "dashboard.topNav.labsConfigDescription": "ラボ", "dashboard.topNav.options.hideAllPanelTitlesSwitchLabel": "パネルタイトルを表示", "dashboard.topNav.options.syncColorsBetweenPanelsSwitchLabel": "パネル全体でカラーパレットを同期", "dashboard.topNav.options.useMarginsBetweenPanelsSwitchLabel": "パネルの間に余白を使用", "dashboard.topNav.saveModal.descriptionFormRowLabel": "説明", - "dashboard.topNav.saveModal.objectType": "dashboard", "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText": "有効化すると、ダッシュボードが読み込まれるごとに現在選択された時刻の時間フィルターが変更されます。", "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel": "ダッシュボードに時刻を保存", "dashboard.topNav.showCloneModal.dashboardCopyTitle": "{title}のコピー", @@ -688,9 +700,9 @@ "dashboard.urlWasRemovedInSixZeroWarningMessage": "URL「dashboard/create」は6.0で廃止されました。ブックマークを更新してください。", "data.advancedSettings.autocompleteIgnoreTimerange": "時間範囲を使用", "data.advancedSettings.autocompleteIgnoreTimerangeText": "このプロパティを無効にすると、現在の時間範囲からではなく、データセットからオートコンプリートの候補を取得します。", - "data.advancedSettings.courier.batchSearchesText": "Kibana は新しい検索とバッチインフラストラクチャを使用します。\n レガシー同期動作にフォールバックする場合は、このオプションを有効にします", + "data.advancedSettings.courier.batchSearchesText": "Kibana は新しい非同期検索とインフラストラクチャを使用します。\n レガシー同期動作にフォールバックする場合は、このオプションを有効にします", "data.advancedSettings.courier.batchSearchesTextDeprecation": "この設定はサポートが終了し、Kibana 8.0 では削除されます。", - "data.advancedSettings.courier.batchSearchesTitle": "レガシー検索を使用", + "data.advancedSettings.courier.batchSearchesTitle": "同期検索を使用", "data.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText": "リクエスト設定", "data.advancedSettings.courier.customRequestPreferenceText": "{setRequestReferenceSetting} が {customSettingValue} に設定されている時に使用される {requestPreferenceLink} です。", "data.advancedSettings.courier.customRequestPreferenceTitle": "カスタムリクエスト設定", @@ -745,6 +757,8 @@ "data.advancedSettings.searchQueryLanguageLucene": "Lucene", "data.advancedSettings.searchQueryLanguageText": "クエリ言語はクエリバーで使用されます。KQLはKibana用に特別に開発された新しい言語です。", "data.advancedSettings.searchQueryLanguageTitle": "クエリ言語", + "data.advancedSettings.searchTimeout": "検索タイムアウト", + "data.advancedSettings.searchTimeoutDesc": "検索セッションの最大タイムアウトを変更するか、0 に設定してタイムアウトを無効にすると、クエリは完了するまで実行されます。", "data.advancedSettings.shortenFieldsText": "長いフィールドを短くします。例:foo.bar.bazの代わりにf.b.bazと表示", "data.advancedSettings.shortenFieldsTitle": "フィールドの短縮", "data.advancedSettings.sortOptions.optionsLinkText": "オプション", @@ -771,12 +785,6 @@ "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", "data.aggTypes.buckets.ranges.rangesFormatMessageArrowRight": "{from} → {to}", - "data.common.kql.errors.endOfInputText": "インプットの終わり", - "data.common.kql.errors.fieldNameText": "フィールド名", - "data.common.kql.errors.literalText": "文字通り", - "data.common.kql.errors.syntaxError": "{expectedList} を期待しましたが {foundInput} が検出されました。", - "data.common.kql.errors.valueText": "値", - "data.common.kql.errors.whitespaceText": "空白類", "data.errors.fetchError": "ネットワークとプロキシ構成を確認してください。問題が解決しない場合は、ネットワーク管理者に問い合わせてください。", "data.fieldFormats.boolean.title": "ブール", "data.fieldFormats.bytes.title": "バイト", @@ -796,13 +804,23 @@ "data.fieldFormats.duration.inputFormats.years": "年", "data.fieldFormats.duration.negativeLabel": "マイナス", "data.fieldFormats.duration.outputFormats.asDays": "日", + "data.fieldFormats.duration.outputFormats.asDays.short": "d", "data.fieldFormats.duration.outputFormats.asHours": "時間", + "data.fieldFormats.duration.outputFormats.asHours.short": "h", "data.fieldFormats.duration.outputFormats.asMilliseconds": "ミリ秒", + "data.fieldFormats.duration.outputFormats.asMilliseconds.short": "ms", "data.fieldFormats.duration.outputFormats.asMinutes": "分", + "data.fieldFormats.duration.outputFormats.asMinutes.short": "分", "data.fieldFormats.duration.outputFormats.asMonths": "か月", + "data.fieldFormats.duration.outputFormats.asMonths.short": "mon", "data.fieldFormats.duration.outputFormats.asSeconds": "秒", + "data.fieldFormats.duration.outputFormats.asSeconds.short": "s", "data.fieldFormats.duration.outputFormats.asWeeks": "週間", + "data.fieldFormats.duration.outputFormats.asWeeks.short": "w", "data.fieldFormats.duration.outputFormats.asYears": "年", + "data.fieldFormats.duration.outputFormats.asYears.short": "y", + "data.fieldFormats.duration.outputFormats.humanize.approximate": "人間が読み取り可能 (近似値) ", + "data.fieldFormats.duration.outputFormats.humanize.precise": "人間が読み取り可能 (正確な値) ", "data.fieldFormats.duration.title": "期間", "data.fieldFormats.histogram.title": "ヒストグラム", "data.fieldFormats.ip.title": "IP アドレス", @@ -810,6 +828,7 @@ "data.fieldFormats.percent.title": "割合 (%) ", "data.fieldFormats.relative_date.title": "相対日付", "data.fieldFormats.static_lookup.title": "静的ルックアップ", + "data.fieldFormats.string.emptyLabel": " (空) ", "data.fieldFormats.string.title": "文字列", "data.fieldFormats.string.transformOptions.base64": "Base64 デコード", "data.fieldFormats.string.transformOptions.lower": "小文字", @@ -1052,7 +1071,7 @@ "data.search.aggs.buckets.ipRange.ranges.help": "このアグリゲーションで使用するシリアル化された範囲。", "data.search.aggs.buckets.ipRange.schema.help": "このアグリゲーションで使用するスキーマ", "data.search.aggs.buckets.ipRangeLabel": "{fieldName} IP 範囲", - "data.search.aggs.buckets.ipRangeTitle": "IPv4 範囲", + "data.search.aggs.buckets.ipRangeTitle": "IP範囲", "data.search.aggs.buckets.range.customLabel.help": "このアグリゲーションのカスタムラベルを表します", "data.search.aggs.buckets.range.enabled.help": "このアグリゲーションが有効かどうかを指定します", "data.search.aggs.buckets.range.field.help": "このアグリゲーションで使用するフィールド", @@ -1338,6 +1357,7 @@ "data.search.aggs.metrics.sumBucketTitle": "合計バケット", "data.search.aggs.metrics.sumLabel": "{field} の合計", "data.search.aggs.metrics.sumTitle": "合計", + "data.search.aggs.metrics.timeShift.help": "設定した時間の分だけメトリックの時間範囲をシフトします。例:1時間、7日。[前へ]は日付ヒストグラムまたは時間範囲フィルターから最も近い時間範囲を使用します。", "data.search.aggs.metrics.top_hit.aggregate.help": "アグリゲーションタイプ", "data.search.aggs.metrics.top_hit.customLabel.help": "このアグリゲーションのカスタムラベルを表します", "data.search.aggs.metrics.top_hit.enabled.help": "このアグリゲーションが有効かどうかを指定します", @@ -1402,6 +1422,7 @@ "data.search.functions.kibana_context.savedSearchId.help": "クエリとフィルターに使用する保存検索ID を指定します。", "data.search.functions.kibana_context.timeRange.help": "Kibana 時間範囲フィルターを指定します", "data.search.functions.kibana.help": "Kibana グローバルコンテキストを取得します", + "data.search.functions.kibanaFilter.disabled.help": "フィルターは無効でなければなりません", "data.search.functions.kibanaFilter.field.help": "フリーフォームesdslクエリを指定", "data.search.functions.kibanaFilter.help": "Kibanaフィルターを作成", "data.search.functions.kibanaFilter.negate.help": "フィルターは否定でなければなりません", @@ -1492,11 +1513,18 @@ "data.search.timeoutIncreaseSetting": "クエリがタイムアウトしました。検索タイムアウト詳細設定で実行時間を延長します。", "data.search.timeoutIncreaseSettingActionText": "設定を編集", "data.search.unableToGetSavedQueryToastTitle": "保存したクエリ {savedQueryId} を読み込めません", + "data.searchSession.warning.readDocs": "続きを読む", "data.searchSessionIndicator.noCapability": "検索セッションを作成するアクセス権がありません。", "data.searchSessions.sessionService.sessionEditNameError": "検索セッションの名前を編集できませんでした", "data.searchSessions.sessionService.sessionObjectFetchError": "検索セッション情報を取得できませんでした", "data.triggers.applyFilterDescription": "Kibanaフィルターが適用されるとき。単一の値または範囲フィルターにすることができます。", "data.triggers.applyFilterTitle": "フィルターを適用", + "esQuery.kql.errors.endOfInputText": "インプットの終わり", + "esQuery.kql.errors.fieldNameText": "フィールド名", + "esQuery.kql.errors.literalText": "文字通り", + "esQuery.kql.errors.syntaxError": "{expectedList} を期待しましたが {foundInput} が検出されました。", + "esQuery.kql.errors.valueText": "値", + "esQuery.kql.errors.whitespaceText": "空白類", "devTools.badge.readOnly.text": "読み取り専用", "devTools.badge.readOnly.tooltip": "を保存できませんでした", "devTools.devToolsTitle": "開発ツール", @@ -1512,10 +1540,15 @@ "discover.advancedSettings.defaultColumnsTitle": "デフォルトの列", "discover.advancedSettings.discover.modifyColumnsOnSwitchText": "新しいインデックスパターンで使用できない列を削除します。", "discover.advancedSettings.discover.modifyColumnsOnSwitchTitle": "インデックスパターンを変更するときに列を修正", + "discover.advancedSettings.discover.multiFieldsLinkText": "マルチフィールド", + "discover.advancedSettings.discover.readFieldsFromSource": "_sourceからフィールドを読み取る", + "discover.advancedSettings.discover.readFieldsFromSourceDescription": "有効にすると、「_source」から直接ドキュメントを読み込みます。これはまもなく廃止される予定です。無効にすると、上位レベルの検索サービスで新しいフィールドAPI経由でフィールドを取得します。", + "discover.advancedSettings.discover.showMultifields": "マルチフィールドを表示", + "discover.advancedSettings.discover.showMultifieldsDescription": "拡張ドキュメントビューに{multiFields}が表示されるかどうかを制御します。ほとんどの場合、マルチフィールドは元のフィールドと同じです。「searchFieldsFromSource」がオフのときにのみこのオプションを使用できます。", "discover.advancedSettings.docTableHideTimeColumnText": "Discover と、ダッシュボードのすべての保存された検索で、「時刻」列を非表示にします。", "discover.advancedSettings.docTableHideTimeColumnTitle": "「時刻」列を非表示", - "discover.advancedSettings.docTableVersionDescription": "Discover は、データの並べ替え、列のドラッグアンドドロップ、全画面表示を含む新しいテーブルレイアウトを使用します。レガシーテーブルにフォールバックする場合は、このオプションを有効にします。", - "discover.advancedSettings.docTableVersionName": "レガシーテーブルを使用", + "discover.advancedSettings.docTableVersionDescription": "Discover は、データの並べ替え、列のドラッグアンドドロップ、全画面表示を含む新しいテーブルレイアウトを使用します。このオプションをオンにすると、クラシックテーブルを使用します。オフにすると、新しいテーブルを使用します。", + "discover.advancedSettings.docTableVersionName": "クラシックテーブルを使用", "discover.advancedSettings.fieldsPopularLimitText": "最も頻繁に使用されるフィールドのトップNを表示します", "discover.advancedSettings.fieldsPopularLimitTitle": "頻繁に使用されるフィールドの制限", "discover.advancedSettings.maxDocFieldsDisplayedText": "ドキュメント列でレンダリングされたフィールドの最大数", @@ -1535,8 +1568,11 @@ "discover.bucketIntervalTooltip.tooLargeBucketsText": "大きすぎるバケット", "discover.bucketIntervalTooltip.tooManyBucketsText": "バケットが多すぎます", "discover.clearSelection": "選択した項目をクリア", + "discover.context.breadcrumb": "周りのドキュメント", + "discover.context.contextOfTitle": "#{anchorId}の周りのドキュメント", "discover.context.failedToLoadAnchorDocumentDescription": "アンカードキュメントの読み込みに失敗しました", "discover.context.failedToLoadAnchorDocumentErrorDescription": "アンカードキュメントの読み込みに失敗しました。", + "discover.context.invalidTieBreakerFiledSetting": "無効なタイブレーカーフィールド設定", "discover.context.loadButtonLabel": "読み込み", "discover.context.loadingDescription": "読み込み中...", "discover.context.newerDocumentsAriaLabel": "新しいドキュメントの数", @@ -1637,7 +1673,10 @@ "discover.fieldChooser.filter.popularTitle": "人気", "discover.fieldChooser.filter.searchableLabel": "検索可能", "discover.fieldChooser.filter.selectedFieldsTitle": "スクリプトフィールド", - "discover.fieldChooser.filter.typeLabel": "タイプ", + "discover.fieldChooser.filter.toggleButton.any": "すべて", + "discover.fieldChooser.filter.toggleButton.no": "いいえ", + "discover.fieldChooser.filter.toggleButton.yes": "はい", + "discover.fieldChooser.filter.typeLabel": "型", "discover.fieldChooser.indexPattern.changeIndexPatternTitle": "インデックスパターンを変更", "discover.fieldChooser.indexPatterns.actionsPopoverLabel": "インデックスパターン設定", "discover.fieldChooser.indexPatterns.addFieldButton": "フィールドをインデックスパターンに追加", @@ -1645,6 +1684,7 @@ "discover.fieldChooser.searchPlaceHolder": "検索フィールド名", "discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "フィールド設定を非表示", "discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "フィールド設定を表示", + "discover.fieldChooser.visualizeButton.label": "可視化", "discover.fieldList.flyoutBackIcon": "戻る", "discover.fieldList.flyoutHeading": "フィールドリスト", "discover.fieldNameIcons.booleanAriaLabel": "ブールフィールド", @@ -1679,8 +1719,13 @@ "discover.howToChangeTheTimeTooltip": "時刻を変更するには、グローバル時刻フィルターを使用します。", "discover.howToSeeOtherMatchingDocumentsDescription": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。", "discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。", + "discover.inspectorRequestDataTitle": "データ", + "discover.inspectorRequestDescriptionDocument": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。", "discover.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", "discover.json.copyToClipboardLabel": "クリップボードにコピー", + "discover.loadingJSON": "JSONを読み込んでいます", + "discover.loadingResults": "結果を読み込み中", + "discover.localMenu.fallbackReportTitle": "Discover検索[{date}]", "discover.localMenu.inspectTitle": "検査", "discover.localMenu.localMenu.newSearchTitle": "新規", "discover.localMenu.localMenu.optionsTitle": "オプション", @@ -1690,31 +1735,23 @@ "discover.localMenu.openTitle": "開く", "discover.localMenu.optionsDescription": "オプション", "discover.localMenu.saveSaveSearchDescription": "ビジュアライゼーションとダッシュボードで使用できるように Discover の検索を保存します", - "discover.localMenu.saveSaveSearchObjectType": "discover", "discover.localMenu.saveSearchDescription": "検索を保存します", "discover.localMenu.saveTitle": "保存", "discover.localMenu.shareSearchDescription": "検索を共有します", "discover.localMenu.shareTitle": "共有", "discover.noResults.expandYourTimeRangeTitle": "時間範囲を拡大", "discover.noResults.queryMayNotMatchTitle": "1つ以上の表示されているインデックスに日付フィールドが含まれています。クエリが現在の時間範囲のデータと一致しないか、現在選択された時間範囲にデータがまったく存在しない可能性があります。データが存在する時間範囲に変えることができます。", - "discover.noResults.searchExamples.400to499StatusCodeExampleTitle": "400-499のすべてのステータスコードを検索", - "discover.noResults.searchExamples.400to499StatusCodeWithPhpExtensionExampleTitle": "400-499のphp拡張子のステータスコードを検索", - "discover.noResults.searchExamples.400to499StatusCodeWithPhpOrHtmlExtensionExampleTitle": "400-499のphpまたはhtml拡張子のステータスコードを検索", - "discover.noResults.searchExamples.anyField200StatusCodeExampleTitle": "いずれかのフィールドに数字200が含まれているリクエストを検索", - "discover.noResults.searchExamples.howTosearchForWebServerLogsDescription": "画面上部の検索バーは、ElasticsearchのLucene {queryStringSyntaxLink}サポートを利用します。新規フィールドにパースされたウェブサーバーログの検索方法の例は、次のとおりです。", "discover.noResults.searchExamples.noResultsBecauseOfError": "検索結果の取得中にエラーが発生しました", "discover.noResults.searchExamples.noResultsMatchSearchCriteriaTitle": "検索条件と一致する結果がありません。", - "discover.noResults.searchExamples.queryStringSyntaxLinkText": "クエリ文字列の構文", - "discover.noResults.searchExamples.refineYourQueryTitle": "クエリの調整", - "discover.noResults.searchExamples.statusField200StatusCodeExampleTitle": "ステータスフィールドの200を検索", "discover.noResultsFound": "結果が見つかりませんでした", "discover.notifications.invalidTimeRangeText": "指定された時間範囲が無効です。 (開始:'{from}'、終了:'{to}') ", "discover.notifications.invalidTimeRangeTitle": "無効な時間範囲", "discover.notifications.notSavedSearchTitle": "検索「{savedSearchTitle}」は保存されませんでした。", "discover.notifications.savedSearchTitle": "検索「{savedSearchTitle}」が保存されました。", - "discover.openOptionsPopover.dataGridText": "データグリッド", - "discover.openOptionsPopover.goToAdvancedSettings": "高度な設定に移動", - "discover.openOptionsPopover.legacyTableText": "レガシーテーブル", + "discover.openOptionsPopover.dataGridText": "新しいテーブル", + "discover.openOptionsPopover.goToAdvancedSettings": "使ってみる", + "discover.openOptionsPopover.gotToAllSettings": "すべてのDiscoverオプション", + "discover.openOptionsPopover.legacyTableText": "クラシックテーブル", "discover.reloadSavedSearchButton": "検索をリセット", "discover.removeColumnLabel": "列を削除", "discover.rootBreadcrumb": "Discover", @@ -1731,11 +1768,14 @@ "discover.showingSavedIndexPatternWarningDescription": "保存されたインデックスパターン「{ownIndexPatternTitle}」 ({ownIndexPatternId}) を表示中", "discover.showSelectedDocumentsOnly": "選択したドキュメントのみを表示", "discover.skipToBottomButtonLabel": "テーブルの最後に移動", + "discover.sourceViewer.errorMessage": "現在データを取得できませんでした。タブを更新して、再試行してください。", + "discover.sourceViewer.errorMessageTitle": "エラーが発生しました", + "discover.sourceViewer.refresh": "更新", "discover.timechartHeader.timeIntervalSelect.ariaLabel": "時間間隔", "discover.timechartHeader.timeIntervalSelect.per": "ごと", "discover.timeLabel": "時間", "discover.toggleSidebarAriaLabel": "サイドバーを切り替える", - "discover.topNav.openOptionsPopover.description": "新しいデータグリッドレイアウトには、強化されたデータの並べ替え機能、列のドラッグアンドドロップ、複数ドキュメント選択、全画面表示機能があります。詳細設定で[Use legacy table (レガシーテーブルを使用) ]を切り替えて、モードを切り替えます。", + "discover.topNav.openOptionsPopover.description": "お知らせDiscoverでは、データの並べ替え、列のドラッグアンドドロップ、ドキュメントの比較を行う方法が改善されました。詳細設定で[クラシックテーブルを使用]を切り替えて、開始します。", "discover.topNav.openSearchPanel.manageSearchesButtonLabel": "検索の管理", "discover.topNav.openSearchPanel.noSearchesFoundDescription": "一致する検索が見つかりませんでした。", "discover.topNav.openSearchPanel.openSearchTitle": "検索を開く", @@ -1871,6 +1911,8 @@ "expressions.functions.fontHelpText": "フォントスタイルを作成します。", "expressions.functions.mapColumn.args.copyMetaFromHelpText": "設定されている場合、指定した列IDのメタオブジェクトが指定したターゲット列にコピーされます。列が存在しない場合は失敗し、エラーは表示されません。", "expressions.functions.mapColumn.args.expressionHelpText": "すべての行で実行される式。単一行の{DATATABLE}と一緒に指定され、セル値を返します。", + "expressions.functions.mapColumn.args.idHelpText": "結果列の任意のID。IDが指定されていないときには、指定された名前引数で既存の列からルックアップされます。この名前の列が存在しない場合、この名前と同じIDの新しい列がテーブルに追加されます。", + "expressions.functions.mapColumn.args.nameHelpText": "結果の列の名前です。名前は一意である必要はありません。", "expressions.functions.mapColumnHelpText": "他の列の結果として計算された列を追加します。引数が指定された場合のみ変更が加えられます。{alterColumnFn}と{staticColumnFn}もご参照ください。", "expressions.functions.math.args.expressionHelpText": "評価された {TINYMATH} 表現です。{TINYMATH_URL} をご覧ください。", "expressions.functions.math.args.onErrorHelpText": "{TINYMATH}評価が失敗するか、NaNが返される場合、戻り値はonErrorで指定されます。「'throw'」の場合、例外が発生し、式の実行が終了します (デフォルト) 。", @@ -1878,6 +1920,12 @@ "expressions.functions.math.emptyExpressionErrorMessage": "空の表現", "expressions.functions.math.executionFailedErrorMessage": "数式の実行に失敗しました。列名を確認してください", "expressions.functions.math.tooManyResultsErrorMessage": "式は 1 つの数字を返す必要があります。表現を {mean} または {sum} で囲んでみてください", + "expressions.functions.mathColumn.args.copyMetaFromHelpText": "設定されている場合、指定した列IDのメタオブジェクトが指定したターゲット列にコピーされます。列が存在しない場合は失敗し、エラーは表示されません。", + "expressions.functions.mathColumn.args.idHelpText": "結果の列のIDです。一意でなければなりません。", + "expressions.functions.mathColumn.args.nameHelpText": "結果の列の名前です。名前は一意である必要はありません。", + "expressions.functions.mathColumn.arrayValueError": "{name}で配列値に対する演算を実行できません", + "expressions.functions.mathColumn.uniqueIdError": "IDは一意でなければなりません", + "expressions.functions.mathColumnHelpText": "他の列の結果として計算された列を追加します。引数が指定された場合のみ変更が加えられます。{alterColumnFn}と{staticColumnFn}もご参照ください。", "expressions.functions.mathHelpText": "{TYPE_NUMBER}または{DATATABLE}を{CONTEXT}として使用して、{TINYMATH}数式を解釈します。{DATATABLE}列は列名で表示されます。{CONTEXT}が数字の場合は、{value}と表示されます。", "expressions.functions.movingAverage.args.byHelpText": "移動平均計算を分割する列", "expressions.functions.movingAverage.args.inputColumnIdHelpText": "移動平均を計算する列", @@ -1885,10 +1933,21 @@ "expressions.functions.movingAverage.args.outputColumnNameHelpText": "結果の移動平均を格納する列の名前", "expressions.functions.movingAverage.args.windowHelpText": "ヒストグラム全体でスライドするウィンドウのサイズ。", "expressions.functions.movingAverage.help": "データテーブルの列の移動平均を計算します", + "expressions.functions.overallMetric.args.byHelpText": "全体の計算を分割する列", + "expressions.functions.overallMetric.args.inputColumnIdHelpText": "全体のメトリックを計算する列", + "expressions.functions.overallMetric.args.outputColumnIdHelpText": "結果の全体のメトリックを格納する列", + "expressions.functions.overallMetric.args.outputColumnNameHelpText": "結果の全体のメトリックを格納する列の名前", + "expressions.functions.overallMetric.help": "データテーブルの列の合計値、最小値、最大値、」または平均を計算します", + "expressions.functions.overallMetric.metricHelpText": "計算するメトリック", "expressions.functions.seriesCalculations.columnConflictMessage": "指定した outputColumnId {columnId} はすでに存在します。別の列 ID を選択してください。", "expressions.functions.theme.args.defaultHelpText": "テーマ情報がない場合のデフォルト値。", "expressions.functions.theme.args.variableHelpText": "読み取るテーマ変数名。", "expressions.functions.themeHelpText": "テーマ設定を読み取ります。", + "expressions.functions.uiSetting.args.default": "パラメーターが設定されていない場合のデフォルト値。", + "expressions.functions.uiSetting.args.parameter": "パラメーター名。", + "expressions.functions.uiSetting.error.kibanaRequest": "サーバーでUI設定を取得するには、KibanaRequest が必要です。式実行パラメーターに要求オブジェクトを渡してください。", + "expressions.functions.uiSetting.error.parameter": "無効なパラメーター\"{parameter}\"です。", + "expressions.functions.uiSetting.help": "UI設定パラメーター値を返します。", "expressions.functions.var.help": "Kibanaグローバルコンテキストを更新します。", "expressions.functions.var.name.help": "変数の名前を指定します。", "expressions.functions.varset.help": "Kibanaグローバルコンテキストを更新します。", @@ -1935,20 +1994,12 @@ "home.manageData.sectionTitle": "データを管理", "home.pageTitle": "ホーム", "home.recentlyAccessed.recentlyViewedTitle": "最近閲覧", - "home.sampleData.ecommerceSpec.averageSalesPriceTitle": "[e コマース] 平均販売価格", - "home.sampleData.ecommerceSpec.averageSoldQuantityTitle": "[e コマース] 平均販売数", - "home.sampleData.ecommerceSpec.controlsTitle": "[e コマース] コントロール", - "home.sampleData.ecommerceSpec.markdownTitle": "[e コマース] マークダウン", "home.sampleData.ecommerceSpec.ordersTitle": "[e コマース] 注文", "home.sampleData.ecommerceSpec.promotionTrackingTitle": "[e コマース] プロモーショントラッキング", "home.sampleData.ecommerceSpec.revenueDashboardDescription": "サンプルの e コマースの注文と収益を分析します", "home.sampleData.ecommerceSpec.revenueDashboardTitle": "[e コマース] 収益ダッシュボード", - "home.sampleData.ecommerceSpec.salesByCategoryTitle": "[e コマース] カテゴリーごとの売上", - "home.sampleData.ecommerceSpec.salesByGenderTitle": "[e コマース] 性別ごとの売上", "home.sampleData.ecommerceSpec.salesCountMapTitle": "[eコマース] 売上カウントマップ", "home.sampleData.ecommerceSpec.soldProductsPerDayTitle": "[e コマース] 1 日の販売製品", - "home.sampleData.ecommerceSpec.topSellingProductsTitle": "[e コマース] トップセラー製品", - "home.sampleData.ecommerceSpec.totalRevenueTitle": "[e コマース] 合計収益", "home.sampleData.ecommerceSpecDescription": "e コマースの注文をトラッキングするサンプルデータ、ビジュアライゼーション、ダッシュボードです。", "home.sampleData.ecommerceSpecTitle": "サンプル e コマース注文", "home.sampleData.flightsSpec.airportConnectionsTitle": "[フライト] 空港乗り継ぎ (空港にカーソルを合わせてください) ", @@ -2001,6 +2052,7 @@ "home.tutorial.card.sampleDataDescription": "これらの「ワンクリック」データセットで Kibana の探索を始めましょう。", "home.tutorial.card.sampleDataTitle": "サンプルデータ", "home.tutorial.elasticCloudButtonLabel": "Elastic Cloud", + "home.tutorial.instruction_variant.fleet": "FleetのElastic APM (ベータ版) ", "home.tutorial.instruction.copyButtonLabel": "スニペットをコピー", "home.tutorial.instructionSet.checkStatusButtonLabel": "ステータスを確認", "home.tutorial.instructionSet.customizeLabel": "コードスニペットのカスタマイズ", @@ -2767,9 +2819,11 @@ "indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningChangingFields": "名前または型を変更すると、このフィールドに依存する検索およびビジュアライゼーションが破損する可能性があります。", "indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningRemovingFields": "フィールドを削除すると、このフィールドに依存する検索およびビジュアライゼーションが破損する可能性があります。", "indexPatternFieldEditor.duration.decimalPlacesLabel": "小数部分の桁数", + "indexPatternFieldEditor.duration.includeSpace": "サフィックスと値の間にスペースを入れる", "indexPatternFieldEditor.duration.inputFormatLabel": "インプット形式", "indexPatternFieldEditor.duration.outputFormatLabel": "アウトプット形式", "indexPatternFieldEditor.duration.showSuffixLabel": "接尾辞を表示", + "indexPatternFieldEditor.duration.showSuffixLabel.short": "短縮サフィックスを使用", "indexPatternFieldEditor.durationErrorMessage": "小数部分の桁数は0から20までの間で指定する必要があります", "indexPatternFieldEditor.editor.flyoutCloseButtonLabel": "閉じる", "indexPatternFieldEditor.editor.flyoutDefaultTitle": "フィールドを作成", @@ -3005,6 +3059,15 @@ "indexPatternManagement.editIndexPattern.timeFilterHeader": "時刻フィールド:「{timeFieldName}」", "indexPatternManagement.editIndexPattern.timeFilterLabel.mappingAPILink": "フィールドマッピング", "indexPatternManagement.editIndexPattern.timeFilterLabel.timeFilterDetail": "{indexPatternTitle}でフィールドを表示して編集します。型や検索可否などのフィールド属性はElasticsearchで{mappingAPILink}に基づきます。", + "indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonDescription": "要約データに制限された集約を実行します。", + "indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonText": "ロールアップインデックスパターン", + "indexPatternManagement.editRollupIndexPattern.createIndex.defaultTypeName": "ロールアップインデックスパターン", + "indexPatternManagement.editRollupIndexPattern.createIndex.indexLabel": "ロールアップ", + "indexPatternManagement.editRollupIndexPattern.createIndex.noMatchError": "ロールアップインデックスパターンエラー:ロールアップインデックスの 1 つと一致している必要があります", + "indexPatternManagement.editRollupIndexPattern.createIndex.tooManyMatchesError": "ロールアップインデックスパターンエラー:一致できるロールアップインデックスは 1 つだけです", + "indexPatternManagement.editRollupIndexPattern.createIndex.uncaughtError": "ロールアップインデックスパターンエラー:{error}", + "indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text": "ロールアップインデックスパターンのKibanaのサポートはベータ版です。保存された検索、可視化、ダッシュボードでこれらのパターンを使用すると問題が発生する場合があります。Timelionや機械学習などの一部の高度な機能ではサポートされていません。", + "indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text": "ロールアップインデックスパターンは、1つのロールアップインデックスとゼロ以上の標準インデックスと一致させることができます。ロールアップインデックスパターンでは、メトリック、フィールド、間隔、アグリゲーションが制限されています。ロールアップインデックスは、1つのジョブ構成があるインデックス、または複数のジョブと互換する構成があるインデックスに制限されています。", "indexPatternManagement.emptyIndexPatternPrompt.documentation": "ドキュメンテーションを表示", "indexPatternManagement.emptyIndexPatternPrompt.indexPatternExplanation": "Kibanaでは、検索するインデックスを特定するためにインデックスパターンが必要です。インデックスパターンは、昨日のログデータなど特定のインデックス、またはログデータを含むすべてのインデックスを参照できます。", "indexPatternManagement.emptyIndexPatternPrompt.learnMore": "詳細について", @@ -3084,15 +3147,6 @@ "indexPatternManagement.warningHeader": "廃止警告:", "indexPatternManagement.warningLabel.painlessLinkLabel": "Painless", "indexPatternManagement.warningLabel.warningDetail": "{language}は廃止され、KibanaとElasticsearchの次のメジャーバージョンではサポートされなくなります。新規スクリプトフィールドには{painlessLink}を使うことをお勧めします。", - "indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonDescription": "要約データに制限された集約を実行します。", - "indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonText": "ロールアップインデックスパターン", - "indexPatternManagement.editRollupIndexPattern.createIndex.defaultTypeName": "ロールアップインデックスパターン", - "indexPatternManagement.editRollupIndexPattern.createIndex.indexLabel": "ロールアップ", - "indexPatternManagement.editRollupIndexPattern.createIndex.noMatchError": "ロールアップインデックスパターンエラー:ロールアップインデックスの 1 つと一致している必要があります", - "indexPatternManagement.editRollupIndexPattern.createIndex.tooManyMatchesError": "ロールアップインデックスパターンエラー:一致できるロールアップインデックスは 1 つだけです", - "indexPatternManagement.editRollupIndexPattern.createIndex.uncaughtError": "ロールアップインデックスパターンエラー:{error}", - "indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text": "ロールアップインデックスパターンのKibanaのサポートはベータ版です。保存された検索、可視化、ダッシュボードでこれらのパターンを使用すると問題が発生する場合があります。Timelionや機械学習などの一部の高度な機能ではサポートされていません。", - "indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text": "ロールアップインデックスパターンは、1つのロールアップインデックスとゼロ以上の標準インデックスと一致させることができます。ロールアップインデックスパターンでは、メトリック、フィールド、間隔、アグリゲーションが制限されています。ロールアップインデックスは、1つのジョブ構成があるインデックス、または複数のジョブと互換する構成があるインデックスに制限されています。", "inputControl.control.noIndexPatternTooltip": "index-pattern id が見つかりませんでした:{indexPatternId}.", "inputControl.control.notInitializedTooltip": "コントロールが初期化されていません", "inputControl.control.noValuesDisableTooltip": "「{indexPatternName}」インデックスパターンでいずれのドキュメントにも存在しない「{fieldName}」フィールドがフィルターの対象になっています。異なるフィールドを選択するか、このフィールドに値が入力されているドキュメントをインデックスしてください。", @@ -3202,6 +3256,7 @@ "kibana-react.tableListView.listing.listingLimitExceeded.advancedSettingsLinkText": "高度な設定", "kibana-react.tableListView.listing.listingLimitExceededDescription": "{totalItems} 件の {entityNamePlural} がありますが、{listingLimitText} の設定により {listingLimitValue} 件までしか下の表に表示できません。{advancedSettingsLink} の下でこの設定を変更できます。", "kibana-react.tableListView.listing.listingLimitExceededTitle": "リスティング制限超過", + "kibana-react.tableListView.listing.noAvailableItemsMessage": "利用可能な {entityNamePlural} がありません。", "kibana-react.tableListView.listing.noMatchedItemsMessage": "検索条件に一致する {entityNamePlural} がありません。", "kibana-react.tableListView.listing.table.actionTitle": "アクション", "kibana-react.tableListView.listing.table.editActionDescription": "編集", @@ -3225,6 +3280,14 @@ "kibanaOverview.manageData.sectionTitle": "データを管理", "kibanaOverview.more.title": "Elasticではさまざまなことが可能です", "kibanaOverview.news.title": "新機能", + "lists.exceptions.doesNotExistOperatorLabel": "存在しない", + "lists.exceptions.existsOperatorLabel": "存在する", + "lists.exceptions.isInListOperatorLabel": "リストにある", + "lists.exceptions.isNotInListOperatorLabel": "リストにない", + "lists.exceptions.isNotOneOfOperatorLabel": "is not one of", + "lists.exceptions.isNotOperatorLabel": "is not", + "lists.exceptions.isOneOfOperatorLabel": "is one of", + "lists.exceptions.isOperatorLabel": "is", "management.breadcrumb": "スタック管理", "management.landing.header": "Stack Management {version}へようこそ", "management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibanaの設定、その他を管理します。", @@ -3289,18 +3352,26 @@ "newsfeed.headerButton.unreadAriaLabel": "ニュースフィードメニュー - 未読の項目があります", "newsfeed.loadingPrompt.gettingNewsText": "最新ニュースを取得しています...", "presentationUtil.dashboardPicker.searchDashboardPlaceholder": "ダッシュボードを検索...", - "presentationUtil.labs.components.browserSwitchHelp": "ブラウザーのラボプロジェクトを有効または無効にします。ブラウザーインスタンスの間は永続します。", + "presentationUtil.labs.components.browserSwitchHelp": "このブラウザーでラボを有効にします。ブラウザーを閉じた後も永続します。", "presentationUtil.labs.components.browserSwitchName": "ブラウザー", "presentationUtil.labs.components.calloutHelp": "変更を適用するには更新します", - "presentationUtil.labs.components.kibanaSwitchHelp": "Kibanaでこのラボプロジェクトの対応する詳細設定を設定します", + "presentationUtil.labs.components.closeButtonLabel": "閉じる", + "presentationUtil.labs.components.descriptionMessage": "開発中の機能や実験的な機能を試します。", + "presentationUtil.labs.components.disabledStatusMessage": "デフォルト: {status}", + "presentationUtil.labs.components.enabledStatusMessage": "デフォルト: {status}", + "presentationUtil.labs.components.kibanaSwitchHelp": "すべてのKibanaユーザーでこのラボを有効にします。", "presentationUtil.labs.components.kibanaSwitchName": "Kibana", "presentationUtil.labs.components.labFlagsLabel": "ラボフラグ", - "presentationUtil.labs.components.noProjectsMessage": "ラボプロジェクトがありません", - "presentationUtil.labs.components.overrideFlagsLabel": "フラグを無効にする", + "presentationUtil.labs.components.noProjectsinSolutionMessage": "現在{solutionName}にはラボがありません。", + "presentationUtil.labs.components.noProjectsMessage": "現在ラボはありません。", + "presentationUtil.labs.components.overrideFlagsLabel": "上書き", + "presentationUtil.labs.components.overridenIconTipLabel": "デフォルトの上書き", "presentationUtil.labs.components.resetToDefaultLabel": "デフォルトにリセット", - "presentationUtil.labs.components.sessionSwitchHelp": "このタブのラボプロジェクトを有効または無効にします。ブラウザータブが閉じたときにリセットします", + "presentationUtil.labs.components.sessionSwitchHelp": "このブラウザーセッションのラボを有効にします。ブラウザーを閉じるとリセットされます。", "presentationUtil.labs.components.sessionSwitchName": "セッション", - "presentationUtil.labs.components.titleLabel": "ラボプロジェクト", + "presentationUtil.labs.components.titleLabel": "ラボ", + "presentationUtil.labs.enableDeferBelowFoldProjectDescription": "「区切り」の下のすべてのパネル (ウィンドウ下部の下にある非表示の領域) はすぐに読み込まれません。ビューポートを入力するときにのみ読み込まれます", + "presentationUtil.labs.enableDeferBelowFoldProjectName": "「区切り」の下のパネルの読み込みを延期", "presentationUtil.saveModalDashboard.addToDashboardLabel": "ダッシュボードに追加", "presentationUtil.saveModalDashboard.dashboardInfoTooltip": "Visualizeライブラリに追加された項目はすべてのダッシュボードで使用できます。ライブラリ項目の編集は、使用されるすべての場所に表示されます。", "presentationUtil.saveModalDashboard.existingDashboardOptionLabel": "既存", @@ -3389,6 +3460,9 @@ "savedObjectsManagement.managementSectionLabel": "保存されたオブジェクト", "savedObjectsManagement.objects.savedObjectsDescription": "保存された検索、ビジュアライゼーション、ダッシュボードのインポート、エクスポート、管理を行います。", "savedObjectsManagement.objects.savedObjectsTitle": "保存されたオブジェクト", + "savedObjectsManagement.objectsTable.deleteConfirmModal.cannotDeleteCallout.content": "一部の選択したオブジェクトは削除できません。テーブル概要の一覧には表示されません", + "savedObjectsManagement.objectsTable.deleteConfirmModal.cannotDeleteCallout.title": "一部のオブジェクトを削除できません", + "savedObjectsManagement.objectsTable.deleteConfirmModal.sharedObjectsCallout.content": "共有オブジェクトは属しているすべてのスペースから削除されます。", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel": "キャンセル", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName": "Id", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName": "タイトル", @@ -3396,6 +3470,7 @@ "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModalTitle": "保存されたオブジェクトの削除", "savedObjectsManagement.objectsTable.export.dangerNotification": "エクスポートを生成できません", "savedObjectsManagement.objectsTable.export.successNotification": "ファイルはバックグラウンドでダウンロード中です", + "savedObjectsManagement.objectsTable.export.successWithExcludedObjectsNotification": "ファイルはバックグラウンドでダウンロード中です。一部のオブジェクトはエクスポートから除外されました。除外されたオブジェクトの一覧は、エクスポートされたファイルの最後の行をご覧ください。", "savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification": "ファイルはバックグラウンドでダウンロード中です。一部の関連オブジェクトが見つかりませんでした。足りないオブジェクトの一覧は、エクスポートされたファイルの最後の行をご覧ください。", "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.cancelButtonLabel": "キャンセル", "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel": "すべてエクスポート", @@ -3492,6 +3567,7 @@ "savedObjectsManagement.objectsTable.table.typeFilterName": "型", "savedObjectsManagement.objectsTable.unableFindSavedObjectNotificationMessage": "保存されたオブジェクトが見つかりません", "savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage": "保存されたオブジェクトが見つかりません", + "savedObjectsManagement.objectView.unableFindSavedObjectNotificationMessage": "保存されたオブジェクトが見つかりません", "savedObjectsManagement.parsingFieldErrorMessage": "{fieldName}をインデックスパターン{indexName}用にパース中にエラーが発生しました:{errorMessage}", "savedObjectsManagement.view.cancelButtonAriaLabel": "キャンセル", "savedObjectsManagement.view.cancelButtonLabel": "キャンセル", @@ -3609,6 +3685,7 @@ "timelion.cells.actions.reorderTooltip": "ドラッグして並べ替え", "timelion.chart.seriesList.noSchemaWarning": "次のパネルタイプは存在しません:{renderType}", "timelion.deprecation.here": "ダッシュボードに移行します。", + "timelion.deprecation.message": "Timelionアプリは7.0以降で非推奨となっています。7.16では削除される予定です。Timelionワークシートを引き続き使用するには、{timeLionDeprecationLink}。", "timelion.emptyExpressionErrorMessage": "Timelion エラー:式が入力されていません", "timelion.expressionInputAriaLabel": "Timelion 式", "timelion.expressionInputPlaceholder": "{esQuery} でのクエリを試してみてください。", @@ -4055,7 +4132,7 @@ "visDefaultEditor.controls.ranges.fromLabel": "開始:", "visDefaultEditor.controls.ranges.greaterThanOrEqualPrepend": "≧", "visDefaultEditor.controls.ranges.greaterThanOrEqualTooltip": "よりも大きいまたは等しい", - "visDefaultEditor.controls.ranges.lessThanPrepend": "<", + "visDefaultEditor.controls.ranges.lessThanPrepend": "<", "visDefaultEditor.controls.ranges.lessThanTooltip": "より小さい", "visDefaultEditor.controls.ranges.removeRangeButtonAriaLabel": "{from}から{to}の範囲を削除", "visDefaultEditor.controls.ranges.toLabel": "終了:", @@ -4111,6 +4188,7 @@ "visDefaultEditor.sidebar.tabs.dataLabel": "データ", "visDefaultEditor.sidebar.tabs.optionsLabel": "オプション", "visDefaultEditor.sidebar.updateChartButtonLabel": "更新", + "visDefaultEditor.sidebar.updateInfoTooltip": "CTRL + Enterは更新のショートカットです。", "visTypeMarkdown.function.font.help": "フォント設定です。", "visTypeMarkdown.function.help": "マークダウンビジュアライゼーション", "visTypeMarkdown.function.markdown.help": "レンダリングするマークダウン", @@ -4148,6 +4226,59 @@ "visTypeMetric.params.style.styleTitle": "スタイル", "visTypeMetric.schemas.metricTitle": "メトリック", "visTypeMetric.schemas.splitGroupTitle": "グループを分割", + "visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.deprecation": "Visualizeの円グラフのレガシーグラフライブラリは廃止予定であり、8.0以降ではサポートされません。", + "visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.description": "Visualizeで円グラフのレガシーグラフライブラリを有効にします。", + "visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.name": "円グラフのレガシーグラフライブラリ", + "visTypePie.controls.truncateLabel": "切り捨て", + "visTypePie.editors.pie.addLegendLabel": "凡例を表示", + "visTypePie.editors.pie.decimalSliderLabel": "割合の最大小数点桁数", + "visTypePie.editors.pie.distinctColorsLabel": "スライスごとに異なる色を使用", + "visTypePie.editors.pie.donutLabel": "ドーナッツ", + "visTypePie.editors.pie.labelPositionLabel": "ラベル位置", + "visTypePie.editors.pie.labelsSettingsTitle": "ラベル設定", + "visTypePie.editors.pie.nestedLegendLabel": "ネスト凡例", + "visTypePie.editors.pie.pieSettingsTitle": "パイ設定", + "visTypePie.editors.pie.showLabelsLabel": "ラベルを表示", + "visTypePie.editors.pie.showTopLevelOnlyLabel": "トップレベルのみ表示", + "visTypePie.editors.pie.showValuesLabel": "値を表示", + "visTypePie.editors.pie.valueFormatsLabel": "値", + "visTypePie.function.args.addLegendHelpText": "グラフ凡例を表示", + "visTypePie.function.args.addTooltipHelpText": "スライスにカーソルを置いたときにツールチップを表示", + "visTypePie.function.args.bucketsHelpText": "バケットディメンション構成", + "visTypePie.function.args.distinctColorsHelpText": "スライスごとに異なる色をマッピングします。同じ値のスライスは同じ色になります", + "visTypePie.function.args.isDonutHelpText": "円グラフをドーナツグラフとして表示します", + "visTypePie.function.args.labelsHelpText": "円グラフラベル構成", + "visTypePie.function.args.legendPositionHelpText": "グラフの上、下、左、右に凡例を配置", + "visTypePie.function.args.metricHelpText": "メトリックディメンション構成", + "visTypePie.function.args.nestedLegendHelpText": "詳細凡例を表示", + "visTypePie.function.args.paletteHelpText": "グラフパレット名を定義します", + "visTypePie.function.args.splitColumnHelpText": "列ディメンション構成で分割", + "visTypePie.function.args.splitRowHelpText": "行ディメンション構成で分割", + "visTypePie.function.pieLabels.help": "円グラフラベルオブジェクトを生成します", + "visTypePie.function.pieLabels.lastLevel.help": "最上位のラベルのみを表示", + "visTypePie.function.pieLabels.percentDecimals.help": "割合として値に表示される10進数を定義します", + "visTypePie.function.pieLabels.position.help": "ラベル位置を定義します", + "visTypePie.function.pieLabels.show.help": "円グラフのラベルを表示します", + "visTypePie.function.pieLabels.truncate.help": "スライス値が表示される文字数を定義します", + "visTypePie.function.pieLabels.values.help": "スライス内の値を定義します", + "visTypePie.function.pieLabels.valuesFormat.help": "値の形式を定義します", + "visTypePie.functions.help": "パイビジュアライゼーション", + "visTypePie.labelPositions.insideOrOutsideText": "内部または外部", + "visTypePie.labelPositions.insideText": "内部", + "visTypePie.legend.filterForValueButtonAriaLabel": "値でフィルター", + "visTypePie.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", + "visTypePie.legend.filterOutValueButtonAriaLabel": "値を除外", + "visTypePie.legendPositions.bottomText": "一番下", + "visTypePie.legendPositions.leftText": "左", + "visTypePie.legendPositions.rightText": "右", + "visTypePie.legendPositions.topText": "トップ", + "visTypePie.pie.metricTitle": "スライスサイズ", + "visTypePie.pie.pieDescription": "全体に対する比率でデータを比較します。", + "visTypePie.pie.pieTitle": "円", + "visTypePie.pie.segmentTitle": "スライスの分割", + "visTypePie.pie.splitTitle": "チャートを分割", + "visTypePie.valuesFormats.percent": "割合を表示", + "visTypePie.valuesFormats.value": "値を表示", "visTypeTable.aggTable.exportLabel": "エクスポート:", "visTypeTable.aggTable.formattedLabel": "フォーマット済み", "visTypeTable.aggTable.rawLabel": "未加工", @@ -4203,6 +4334,7 @@ "visTypeTagCloud.function.help": "タグクラウドのビジュアライゼーションです。", "visTypeTagCloud.function.metric.help": "メトリックディメンションの構成です。", "visTypeTagCloud.function.orientation.help": "タグクラウド内の単語の方向です。", + "visTypeTagCloud.function.paletteHelpText": "グラフパレット名を定義します", "visTypeTagCloud.function.scale.help": "単語のフォントサイズを決定するスケールです", "visTypeTagCloud.orientations.multipleText": "複数", "visTypeTagCloud.orientations.rightAngledText": "直角", @@ -4327,6 +4459,7 @@ "visTypeTimeseries.colorRules.adjustChartSizeAriaLabel": "上下の矢印を押してチャートサイズを調整します", "visTypeTimeseries.colorRules.defaultPrimaryNameLabel": "背景", "visTypeTimeseries.colorRules.defaultSecondaryNameLabel": "テキスト", + "visTypeTimeseries.colorRules.emptyLabel": "空", "visTypeTimeseries.colorRules.greaterThanLabel": "> greater than", "visTypeTimeseries.colorRules.greaterThanOrEqualLabel": ">= greater than or equal", "visTypeTimeseries.colorRules.ifMetricIsLabel": "メトリックが", @@ -4404,6 +4537,7 @@ "visTypeTimeseries.getInterval.secondsLabel": "秒", "visTypeTimeseries.getInterval.weeksLabel": "週間", "visTypeTimeseries.getInterval.yearsLabel": "年", + "visTypeTimeseries.handleErrorResponse.unexpectedError": "予期しないエラー", "visTypeTimeseries.iconSelect.asteriskLabel": "アスタリスク", "visTypeTimeseries.iconSelect.bellLabel": "ベル", "visTypeTimeseries.iconSelect.boltLabel": "ボルト", @@ -4421,6 +4555,7 @@ "visTypeTimeseries.iconSelect.tagLabel": "タグ", "visTypeTimeseries.indexPattern.detailLevel": "詳細レベル", "visTypeTimeseries.indexPattern.detailLevelAriaLabel": "詳細レベル", + "visTypeTimeseries.indexPattern.detailLevelHelpText": "時間範囲に基づき自動間隔を制御します。デフォルトの間隔は詳細設定の{histogramTargetBars}と{histogramMaxBars}の影響を受けます。", "visTypeTimeseries.indexPattern.dropLastBucketLabel": "最後のバケットをドロップしますか?", "visTypeTimeseries.indexPattern.finest": "最も細かい", "visTypeTimeseries.indexPattern.intervalHelpText": "例:auto、1m、1d、7d、1y、>=1m", @@ -4690,6 +4825,7 @@ "visTypeTimeseries.timeseries.optionsTab.backgroundColorLabel": "背景色:", "visTypeTimeseries.timeseries.optionsTab.dataLabel": "データ", "visTypeTimeseries.timeseries.optionsTab.displayGridLabel": "グリッドを表示", + "visTypeTimeseries.timeseries.optionsTab.ignoreDaylightTimeLabel": "夏時間を無視しますか?", "visTypeTimeseries.timeseries.optionsTab.ignoreGlobalFilterLabel": "グローバルフィルターを無視しますか?", "visTypeTimeseries.timeseries.optionsTab.legendPositionLabel": "凡例の配置", "visTypeTimeseries.timeseries.optionsTab.panelFilterLabel": "パネルフィルター", @@ -4765,6 +4901,10 @@ "visTypeTimeseries.visEditorVisualization.changesHaveNotBeenAppliedMessage": "ビジュアライゼーションへの変更が適用されました。", "visTypeTimeseries.visEditorVisualization.changesSuccessfullyAppliedMessage": "最新の変更が適用されました。", "visTypeTimeseries.visEditorVisualization.changesWillBeAutomaticallyAppliedMessage": "変更が自動的に適用されます。", + "visTypeTimeseries.visEditorVisualization.indexPatternMode.dismissNoticeButtonText": "閉じる", + "visTypeTimeseries.visEditorVisualization.indexPatternMode.link": "確認してください。", + "visTypeTimeseries.visEditorVisualization.indexPatternMode.notificationMessage": "お知らせElasticsearchインデックスまたはKibanaインデックスパターンからデータを可視化できるようになりました。{indexPatternModeLink}。", + "visTypeTimeseries.visEditorVisualization.indexPatternMode.notificationTitle": "TSVBはインデックスパターンをサポートします", "visTypeTimeseries.visPicker.gaugeLabel": "ゲージ", "visTypeTimeseries.visPicker.metricLabel": "メトリック", "visTypeTimeseries.visPicker.tableLabel": "表", @@ -4838,6 +4978,7 @@ "visTypeVega.vegaParser.unrecognizedDirValueErrorMessage": "認識されていない {dirParam} 値。[{expectedValues}] のいずれかが想定されます。", "visTypeVega.vegaParser.VLCompilerShouldHaveGeneratedSingleProtectionObjectErrorMessage": "内部エラー:Vega-Lite コンパイラーがシングルプロジェクションオブジェクトを生成したはずです", "visTypeVega.vegaParser.widthAndHeightParamsAreIgnored": "{autoSizeParam}が有効であるため、{widthParam}および{heightParam}パラメーターは無視されます。無効にする{autoSizeParam}: {noneParam}を設定", + "visTypeVega.vegaParser.widthAndHeightParamsAreRequired": "{autoSizeParam}が{noneParam}に設定されているときには、カットまたは繰り返された{vegaLiteParam}仕様を使用している間に何も表示されません。修正するには、{autoSizeParam}を削除するか、{vegaParam}を使用してください。", "visTypeVega.visualization.renderErrorTitle": "Vega エラー", "visTypeVega.visualization.unableToRenderWithoutDataWarningMessage": "データなしにはレンダリングできません", "visTypeVislib.advancedSettings.visualization.dimmingOpacityText": "チャートの別のエレメントが選択された時に暗くなるチャート項目の透明度です。この数字が小さければ小さいほど、ハイライトされたエレメントが目立ちます。0と1の間の数字で設定します。", @@ -4904,54 +5045,9 @@ "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}、トグルオプション", "visTypeVislib.vislib.tooltip.fieldLabel": "フィールド", "visTypeVislib.vislib.tooltip.valueLabel": "値", - "visTypePie.pie.metricTitle": "スライスサイズ", - "visTypePie.pie.pieDescription": "全体に対する比率でデータを比較します。", - "visTypePie.pie.pieTitle": "円", - "visTypePie.pie.segmentTitle": "スライスの分割", - "visTypePie.pie.splitTitle": "チャートを分割", - "visTypePie.editors.pie.donutLabel": "ドーナッツ", - "visTypePie.editors.pie.labelsSettingsTitle": "ラベル設定", - "visTypePie.editors.pie.pieSettingsTitle": "パイ設定", - "visTypePie.editors.pie.showLabelsLabel": "ラベルを表示", - "visTypePie.editors.pie.showTopLevelOnlyLabel": "トップレベルのみ表示", - "visTypePie.editors.pie.showValuesLabel": "値を表示", - "visualizations.advancedSettings.visualizeEnableLabsText": "ユーザーが実験的なビジュアライゼーションを作成、表示、編集できるようになります。無効の場合、\n ユーザーは本番準備が整ったビジュアライゼーションのみを利用できます。", - "visualizations.advancedSettings.visualizeEnableLabsTitle": "実験的なビジュアライゼーションを有効にする", - "visualizations.disabledLabVisualizationLink": "ドキュメンテーションを表示", - "visualizations.disabledLabVisualizationMessage": "ラボビジュアライゼーションを表示するには、高度な設定でラボモードをオンにしてください。", - "visualizations.disabledLabVisualizationTitle": "{title} はラボビジュアライゼーションです。", - "visualizations.displayName": "ビジュアライゼーション", - "visualizations.embeddable.placeholderTitle": "プレースホルダータイトル", - "visualizations.function.range.from.help": "範囲の開始", - "visualizations.function.range.help": "範囲オブジェクトを生成します", - "visualizations.function.range.to.help": "範囲の終了", - "visualizations.function.visDimension.accessor.help": "使用するデータセット内の列 (列インデックスまたは列名) ", - "visualizations.function.visDimension.error.accessor": "入力された列名は無効です。", - "visualizations.function.visDimension.format.help": "フォーマット", - "visualizations.function.visDimension.formatParams.help": "フォーマットパラメーター", - "visualizations.function.visDimension.help": "visConfig ディメンションオブジェクトを生成します", - "visualizations.initializeWithoutIndexPatternErrorMessage": "インデックスパターンなしで集約を初期化しようとしています", - "visualizations.newVisWizard.aggBasedGroupDescription": "クラシック Visualize ライブラリを使用して、アグリゲーションに基づいてグラフを作成します。", - "visualizations.newVisWizard.aggBasedGroupTitle": "アグリゲーションに基づく", - "visualizations.newVisWizard.chooseSourceTitle": "ソースの選択", - "visualizations.newVisWizard.experimentalTitle": "実験的", - "visualizations.newVisWizard.experimentalTooltip": "このビジュアライゼーションは今後のリリースで変更または削除される可能性があり、SLA のサポート対象になりません。", - "visualizations.newVisWizard.exploreOptionLinkText": "探索オプション", - "visualizations.newVisWizard.filterVisTypeAriaLabel": "ビジュアライゼーションのタイプでフィルタリング", - "visualizations.newVisWizard.goBackLink": "別のビジュアライゼーションを選択", - "visualizations.newVisWizard.helpTextAriaLabel": "タイプを選択してビジュアライゼーションの作成を始めましょう。ESC を押してこのモーダルを閉じます。Tab キーを押して次に進みます。", - "visualizations.newVisWizard.learnMoreText": "詳細について", - "visualizations.newVisWizard.newVisTypeTitle": "新規 {visTypeName}", - "visualizations.newVisWizard.readDocumentationLink": "ドキュメンテーションを表示", - "visualizations.newVisWizard.searchSelection.notFoundLabel": "一致インデックスまたは保存した検索が見つかりません。", - "visualizations.newVisWizard.searchSelection.savedObjectType.indexPattern": "インデックスパターン", - "visualizations.newVisWizard.searchSelection.savedObjectType.search": "保存検索", - "visualizations.newVisWizard.title": "新規ビジュアライゼーション", - "visualizations.newVisWizard.toolsGroupTitle": "ツール", - "visualizations.noResultsFoundTitle": "結果が見つかりませんでした", - "visualizations.savedObjectName": "ビジュアライゼーション", - "visualizations.savingVisualizationFailed.errorMsg": "ビジュアライゼーションの保存が失敗しました", - "visualizations.visualizationTypeInvalidMessage": "無効なビジュアライゼーションタイプ \"{visType}\"", + "visTypeXy.advancedSettings.visualization.legacyChartsLibrary.deprecation": "Visualizeのエリアグラフ、折れ線グラフ、棒グラフのレガシーグラフライブラリは廃止予定であり、8.0以降ではサポートされません。", + "visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description": "Visualizeでエリア、折れ線、棒グラフのレガシーグラフライブラリを有効にします。", + "visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name": "XY軸レガシーグラフライブラリ", "visTypeXy.aggResponse.allDocsTitle": "すべてのドキュメント", "visTypeXy.area.areaDescription": "軸と線の間のデータを強調します。", "visTypeXy.area.areaTitle": "エリア", @@ -4987,6 +5083,7 @@ "visTypeXy.controls.pointSeries.gridAxis.yAxisLinesDisabledTooltip": "ヒストグラムに X 軸線は表示できません。", "visTypeXy.controls.pointSeries.gridAxis.yAxisLinesLabel": "Y 軸線を表示", "visTypeXy.controls.pointSeries.series.chartTypeLabel": "チャートタイプ", + "visTypeXy.controls.pointSeries.series.circlesRadius": "点のサイズ", "visTypeXy.controls.pointSeries.series.lineModeLabel": "線のモード", "visTypeXy.controls.pointSeries.series.lineWidthLabel": "線の幅", "visTypeXy.controls.pointSeries.series.metricsTitle": "メトリック", @@ -5018,6 +5115,7 @@ "visTypeXy.controls.truncateLabel": "切り捨て", "visTypeXy.editors.elasticChartsOptions.detailedTooltip.label": "詳細ツールチップを表示", "visTypeXy.editors.elasticChartsOptions.detailedTooltip.tooltip": "単一の値を表示するためのレガシー詳細ツールチップを有効にします。無効にすると、新しい概要のツールチップに複数の値が表示されます。", + "visTypeXy.editors.elasticChartsOptions.fillOpacity": "塗りつぶしの透明度", "visTypeXy.editors.elasticChartsOptions.missingValuesLabel": "欠測値を埋める", "visTypeXy.editors.pointSeries.currentTimeMarkerLabel": "現在時刻マーカー", "visTypeXy.editors.pointSeries.orderBucketsBySumLabel": "バケットを合計で並べ替え", @@ -5034,6 +5132,88 @@ "visTypeXy.fittingFunctionsTitle.lookahead": "次 (ギャップを次の値で埋める) ", "visTypeXy.fittingFunctionsTitle.none": "非表示 (ギャップを埋めない) ", "visTypeXy.fittingFunctionsTitle.zero": "ゼロ (ギャップをゼロで埋める) ", + "visTypeXy.function.args.addLegend.help": "グラフ凡例を表示", + "visTypeXy.function.args.addTimeMarker.help": "時刻マーカーを表示", + "visTypeXy.function.args.addTooltip.help": "カーソルを置いたときにツールチップを表示", + "visTypeXy.function.args.args.chartType.help": "グラフの種類。折れ線、エリア、ヒストグラムを選択できます", + "visTypeXy.function.args.categoryAxes.help": "カテゴリ軸構成", + "visTypeXy.function.args.detailedTooltip.help": "詳細ツールチップを表示", + "visTypeXy.function.args.fillOpacity.help": "エリアグラフの塗りつぶしの透明度を定義します", + "visTypeXy.function.args.fittingFunction.help": "適合関数の名前", + "visTypeXy.function.args.gridCategoryLines.help": "グラフにグリッドカテゴリ線を表示", + "visTypeXy.function.args.gridValueAxis.help": "グリッドを表示する値軸の名前", + "visTypeXy.function.args.isVislibVis.help": "古いvislib可視化を示すフラグ。色を含む後方互換性のために使用されます", + "visTypeXy.function.args.labels.help": "グラフラベル構成", + "visTypeXy.function.args.legendPosition.help": "グラフの上、下、左、右に凡例を配置", + "visTypeXy.function.args.orderBucketsBySum.help": "バケットを合計で並べ替え", + "visTypeXy.function.args.palette.help": "グラフパレット名を定義します", + "visTypeXy.function.args.radiusRatio.help": "点サイズ率", + "visTypeXy.function.args.seriesDimension.help": "系列ディメンション構成", + "visTypeXy.function.args.seriesParams.help": "系列パラメーター構成", + "visTypeXy.function.args.splitColumnDimension.help": "列ディメンション構成で分割", + "visTypeXy.function.args.splitRowDimension.help": "行ディメンション構成で分割", + "visTypeXy.function.args.thresholdLine.help": "しきい値線構成", + "visTypeXy.function.args.times.help": "時刻マーカー構成", + "visTypeXy.function.args.valueAxes.help": "値軸構成", + "visTypeXy.function.args.widthDimension.help": "幅ディメンション構成", + "visTypeXy.function.args.xDimension.help": "X軸ディメンション構成", + "visTypeXy.function.args.yDimension.help": "Y軸ディメンション構成", + "visTypeXy.function.args.zDimension.help": "Z軸ディメンション構成", + "visTypeXy.function.categoryAxis.help": "カテゴリ軸オブジェクトを生成します", + "visTypeXy.function.categoryAxis.id.help": "カテゴリ軸のID", + "visTypeXy.function.categoryAxis.labels.help": "軸ラベル構成", + "visTypeXy.function.categoryAxis.position.help": "カテゴリ軸の位置", + "visTypeXy.function.categoryAxis.scale.help": "スケール構成", + "visTypeXy.function.categoryAxis.show.help": "カテゴリ軸を表示", + "visTypeXy.function.categoryAxis.title.help": "カテゴリ軸のタイトル", + "visTypeXy.function.categoryAxis.type.help": "カテゴリ軸の種類。カテゴリまたは値を選択できます", + "visTypeXy.function.label.color.help": "ラベルの色", + "visTypeXy.function.label.filter.help": "軸の重なるラベルと重複を非表示にします", + "visTypeXy.function.label.help": "ラベルオブジェクトを生成します", + "visTypeXy.function.label.overwriteColor.help": "色を上書き", + "visTypeXy.function.label.rotate.help": "角度を回転", + "visTypeXy.function.label.show.help": "ラベルを表示", + "visTypeXy.function.label.truncate.help": "切り捨てる前の記号の数", + "visTypeXy.function.scale.boundsMargin.help": "境界のマージン", + "visTypeXy.function.scale.defaultYExtents.help": "データ境界にスケールできるフラグ", + "visTypeXy.function.scale.help": "スケールオブジェクトを生成します", + "visTypeXy.function.scale.max.help": "最高値", + "visTypeXy.function.scale.min.help": "最低値", + "visTypeXy.function.scale.mode.help": "スケールモード。標準、割合、小刻み、シルエットを選択できます", + "visTypeXy.function.scale.setYExtents.help": "独自の範囲を設定できるフラグ", + "visTypeXy.function.scale.type.help": "スケールタイプ。線形、対数、平方根を選択できます", + "visTypeXy.function.seriesParam.circlesRadius.help": "円のサイズ (半径) を定義します", + "visTypeXy.function.seriesParam.drawLinesBetweenPoints.help": "点の間に線を描画", + "visTypeXy.function.seriesparam.help": "系列パラメーターオブジェクトを生成します", + "visTypeXy.function.seriesParam.id.help": "系列パラメーターのID", + "visTypeXy.function.seriesParam.interpolate.help": "補間モード。線形、カーディナル、階段状を選択できます", + "visTypeXy.function.seriesParam.label.help": "系列パラメーターの名前", + "visTypeXy.function.seriesParam.lineWidth.help": "線の幅", + "visTypeXy.function.seriesParam.mode.help": "グラフモード。積み上げまたは割合を選択できます", + "visTypeXy.function.seriesParam.show.help": "パラメーターを表示", + "visTypeXy.function.seriesParam.showCircles.help": "円を表示", + "visTypeXy.function.seriesParam.type.help": "グラフの種類。折れ線、エリア、ヒストグラムを選択できます", + "visTypeXy.function.seriesParam.valueAxis.help": "値軸の名前", + "visTypeXy.function.thresholdLine.color.help": "しきい線の色", + "visTypeXy.function.thresholdLine.help": "しきい値線オブジェクトを生成します", + "visTypeXy.function.thresholdLine.show.help": "しきい線を表示", + "visTypeXy.function.thresholdLine.style.help": "しきい線のスタイル。実線、点線、一点鎖線を選択できます", + "visTypeXy.function.thresholdLine.value.help": "しきい値", + "visTypeXy.function.thresholdLine.width.help": "しきい値線の幅", + "visTypeXy.function.timeMarker.class.help": "CSSクラス名", + "visTypeXy.function.timeMarker.color.help": "時刻マーカーの色", + "visTypeXy.function.timemarker.help": "時刻マーカーオブジェクトを生成します", + "visTypeXy.function.timeMarker.opacity.help": "時刻マーカーの透明度", + "visTypeXy.function.timeMarker.time.help": "正確な時刻", + "visTypeXy.function.timeMarker.width.help": "時刻マーカーの幅", + "visTypeXy.function.valueAxis.axisParams.help": "値軸パラメーター", + "visTypeXy.function.valueaxis.help": "値軸オブジェクトを生成します", + "visTypeXy.function.valueAxis.name.help": "値軸の名前", + "visTypeXy.function.xyDimension.aggType.help": "集約タイプ", + "visTypeXy.function.xydimension.help": "XYディメンションオブジェクトを生成します", + "visTypeXy.function.xyDimension.label.help": "ラベル", + "visTypeXy.function.xyDimension.params.help": "パラメーター", + "visTypeXy.function.xyDimension.visDimension.help": "ディメンションオブジェクト構成", "visTypeXy.functions.help": "XYビジュアライゼーション", "visTypeXy.histogram.groupTitle": "系列を分割", "visTypeXy.histogram.histogramDescription": "軸の縦棒にデータを表示します。", @@ -5072,6 +5252,43 @@ "visTypeXy.thresholdLine.style.dashedText": "鎖線", "visTypeXy.thresholdLine.style.dotdashedText": "点線", "visTypeXy.thresholdLine.style.fullText": "完全", + "visualizations.advancedSettings.visualizeEnableLabsText": "ユーザーが実験的なビジュアライゼーションを作成、表示、編集できるようになります。無効の場合、\n ユーザーは本番準備が整ったビジュアライゼーションのみを利用できます。", + "visualizations.advancedSettings.visualizeEnableLabsTitle": "実験的なビジュアライゼーションを有効にする", + "visualizations.disabledLabVisualizationLink": "ドキュメンテーションを表示", + "visualizations.disabledLabVisualizationMessage": "ラボビジュアライゼーションを表示するには、高度な設定でラボモードをオンにしてください。", + "visualizations.disabledLabVisualizationTitle": "{title} はラボビジュアライゼーションです。", + "visualizations.displayName": "ビジュアライゼーション", + "visualizations.embeddable.placeholderTitle": "プレースホルダータイトル", + "visualizations.function.range.from.help": "範囲の開始", + "visualizations.function.range.help": "範囲オブジェクトを生成します", + "visualizations.function.range.to.help": "範囲の終了", + "visualizations.function.visDimension.accessor.help": "使用するデータセット内の列 (列インデックスまたは列名) ", + "visualizations.function.visDimension.error.accessor": "入力された列名は無効です。", + "visualizations.function.visDimension.format.help": "フォーマット", + "visualizations.function.visDimension.formatParams.help": "フォーマットパラメーター", + "visualizations.function.visDimension.help": "visConfig ディメンションオブジェクトを生成します", + "visualizations.initializeWithoutIndexPatternErrorMessage": "インデックスパターンなしで集約を初期化しようとしています", + "visualizations.newVisWizard.aggBasedGroupDescription": "クラシック Visualize ライブラリを使用して、アグリゲーションに基づいてグラフを作成します。", + "visualizations.newVisWizard.aggBasedGroupTitle": "アグリゲーションに基づく", + "visualizations.newVisWizard.chooseSourceTitle": "ソースの選択", + "visualizations.newVisWizard.experimentalTitle": "実験的", + "visualizations.newVisWizard.experimentalTooltip": "このビジュアライゼーションは今後のリリースで変更または削除される可能性があり、SLA のサポート対象になりません。", + "visualizations.newVisWizard.exploreOptionLinkText": "探索オプション", + "visualizations.newVisWizard.filterVisTypeAriaLabel": "ビジュアライゼーションのタイプでフィルタリング", + "visualizations.newVisWizard.goBackLink": "別のビジュアライゼーションを選択", + "visualizations.newVisWizard.helpTextAriaLabel": "タイプを選択してビジュアライゼーションの作成を始めましょう。ESC を押してこのモーダルを閉じます。Tab キーを押して次に進みます。", + "visualizations.newVisWizard.learnMoreText": "詳細について", + "visualizations.newVisWizard.newVisTypeTitle": "新規 {visTypeName}", + "visualizations.newVisWizard.readDocumentationLink": "ドキュメンテーションを表示", + "visualizations.newVisWizard.searchSelection.notFoundLabel": "一致インデックスまたは保存した検索が見つかりません。", + "visualizations.newVisWizard.searchSelection.savedObjectType.indexPattern": "インデックスパターン", + "visualizations.newVisWizard.searchSelection.savedObjectType.search": "保存検索", + "visualizations.newVisWizard.title": "新規ビジュアライゼーション", + "visualizations.newVisWizard.toolsGroupTitle": "ツール", + "visualizations.noResultsFoundTitle": "結果が見つかりませんでした", + "visualizations.savedObjectName": "ビジュアライゼーション", + "visualizations.savingVisualizationFailed.errorMsg": "ビジュアライゼーションの保存が失敗しました", + "visualizations.visualizationTypeInvalidMessage": "無効なビジュアライゼーションタイプ \"{visType}\"", "visualize.badge.readOnly.text": "読み取り専用", "visualize.badge.readOnly.tooltip": "ビジュアライゼーションをライブラリに保存できません", "visualize.byValue_pageHeading": "{originatingApp}アプリに埋め込まれた{chartType}タイプのビジュアライゼーション", @@ -5118,7 +5335,6 @@ "visualize.topNavMenu.saveVisualizationButtonAriaLabel": "ビジュアライゼーションを保存", "visualize.topNavMenu.saveVisualizationButtonLabel": "保存", "visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "保存する前に変更を適用または破棄", - "visualize.topNavMenu.saveVisualizationObjectType": "visualize", "visualize.topNavMenu.saveVisualizationToLibraryButtonLabel": "ライブラリに保存", "visualize.topNavMenu.shareVisualizationButtonAriaLabel": "ビジュアライゼーションを共有", "visualize.topNavMenu.shareVisualizationButtonLabel": "共有", @@ -5133,6 +5349,7 @@ "xpack.actions.actionTypeRegistry.register.duplicateActionTypeErrorMessage": "アクションタイプ「{id}」はすでに登録されています。", "xpack.actions.alertHistoryEsIndexConnector.name": "アラート履歴Elasticsearchインデックス", "xpack.actions.appName": "アクション", + "xpack.actions.builtin.case.swimlaneTitle": "スイムレーン", "xpack.actions.builtin.cases.jiraTitle": "Jira", "xpack.actions.builtin.cases.resilientTitle": "IBM Resilient", "xpack.actions.builtin.configuration.apiAllowedHostsError": "コネクターアクションの構成エラー:{message}", @@ -5165,6 +5382,8 @@ "xpack.actions.builtin.slack.unexpectedHttpResponseErrorMessage": "slack からの予期せぬ http 応答:{httpStatus} {httpStatusText}", "xpack.actions.builtin.slack.unexpectedNullResponseErrorMessage": "Slack から予期せぬ null 応答", "xpack.actions.builtin.slackTitle": "Slack", + "xpack.actions.builtin.swimlane.configuration.apiAllowedHostsError": "コネクターアクションの構成エラー:{message}", + "xpack.actions.builtin.swimlaneTitle": "スイムレーン", "xpack.actions.builtin.teams.errorPostingRetryDateErrorMessage": "Microsoft Teams メッセージの投稿エラーです。{retryString} に再試行します", "xpack.actions.builtin.teams.errorPostingRetryLaterErrorMessage": "Microsoft Teams メッセージの投稿エラーです。しばらくたってから再試行します", "xpack.actions.builtin.teams.invalidResponseErrorMessage": "Microsoft Teams への投稿エラーです。無効な応答です", @@ -5183,6 +5402,7 @@ "xpack.actions.builtin.webhookTitle": "Web フック", "xpack.actions.disabledActionTypeError": "アクションタイプ \"{actionType}\" は、Kibana 構成 xpack.actions.enabledActionTypes では有効化されません", "xpack.actions.featureRegistry.actionsFeatureName": "アクションとコネクター", + "xpack.actions.savedObjects.goToConnectorsButtonText": "コネクターに移動", "xpack.actions.serverSideErrors.expirerdLicenseErrorMessage": "{licenseType} ライセンスの期限が切れたのでアクションタイプ {actionTypeId} は無効です。", "xpack.actions.serverSideErrors.invalidLicenseErrorMessage": "{licenseType} ライセンスでサポートされないのでアクションタイプ {actionTypeId} は無効です。ライセンスをアップグレードしてください。", "xpack.actions.serverSideErrors.predefinedActionDeleteDisabled": "あらかじめ構成されたアクション{id}は削除できません。", @@ -5193,15 +5413,16 @@ "xpack.alerting.alertNavigationRegistry.get.missingNavigationError": "「{consumer}」内のアラートタイプ「{alertType}」のナビゲーションは登録されていません。", "xpack.alerting.alertNavigationRegistry.register.duplicateDefaultError": "「{consumer}」内のデフォルトナビゲーションはすでに登録されています。", "xpack.alerting.alertNavigationRegistry.register.duplicateNavigationError": "「{consumer}」内のアラートタイプ「{alertType}」のナビゲーションはすでに登録されています。", - "xpack.alerting.alertsClient.invalidDate": "パラメーター{field}の無効な日付:「{dateValue}」", - "xpack.alerting.alertsClient.validateActions.invalidGroups": "無効なアクショングループ:{groups}", + "xpack.alerting.rulesClient.invalidDate": "パラメーター{field}の無効な日付:「{dateValue}」", + "xpack.alerting.rulesClient.validateActions.invalidGroups": "無効なアクショングループ:{groups}", "xpack.alerting.alertTypeRegistry.get.missingAlertTypeError": "アラートタイプ「{id}」は登録されていません。", "xpack.alerting.alertTypeRegistry.register.customRecoveryActionGroupUsageError": "アラートタイプ [id=\"{id}\"] を登録できません。アクショングループ [{actionGroup}] は、復元とアクティブなアクショングループの両方として使用できません。", "xpack.alerting.alertTypeRegistry.register.duplicateAlertTypeError": "アラートタイプ\"{id}\"はすでに登録されています。", "xpack.alerting.api.error.disabledApiKeys": "アラートは API キーに依存しますがキーが無効になっているようです", "xpack.alerting.appName": "アラート", "xpack.alerting.builtinActionGroups.recovered": "回復済み", - "xpack.alerting.injectActionParams.email.kibanaFooterLinkText": "Kibana でアラートを表示", + "xpack.alerting.injectActionParams.email.kibanaFooterLinkText": "Kibanaでルールを表示", + "xpack.alerting.savedObjects.goToRulesButtonText": "ルールに移動", "xpack.alerting.server.healthStatus.available": "アラートフレームワークを使用できます", "xpack.alerting.server.healthStatus.degraded": "アラートフレームワークは劣化しました", "xpack.alerting.server.healthStatus.unavailable": "アラートフレームワークを使用できません", @@ -5223,7 +5444,7 @@ "xpack.apm.agentConfig.chooseService.editButton": "編集", "xpack.apm.agentConfig.chooseService.service.environment.label": "環境", "xpack.apm.agentConfig.chooseService.service.name.label": "サービス名", - "xpack.apm.agentConfig.circuitBreakerEnabled.description": "Circuit Breaker を有効にすべきかどうかを指定するブール値。 有効にすると、エージェントは定期的にストレス監視をポーリングして、システム/プロセス/JVMのストレス状態を検出します。監視のいずれかがストレスの兆候を検出した場合、`recording`構成オプションの設定が「false」であるかのようにエージェントは一時停止し、リソース消費を最小限に抑えられます。一時停止した場合、エージェントはストレス状態が緩和されたかどうかを検出するために同じ監視のポーリングを継続します。すべての監視でシステム/プロセス/JVMにストレスがないことが認められると、エージェントは再開して完全に機能します。", + "xpack.apm.agentConfig.circuitBreakerEnabled.description": "Circuit Breakerを有効にすべきかどうかを指定するブール値。 有効にすると、エージェントは定期的にストレス監視をポーリングして、システム/プロセス/JVMのストレス状態を検出します。監視のいずれかがストレスの兆候を検出した場合、`recording`構成オプションの設定が「false」であるかのようにエージェントは一時停止し、リソース消費を最小限に抑えられます。一時停止した場合、エージェントはストレス状態が緩和されたかどうかを検出するために同じ監視のポーリングを継続します。すべての監視でシステム/プロセス/JVMにストレスがないことが認められると、エージェントは再開して完全に機能します。", "xpack.apm.agentConfig.circuitBreakerEnabled.label": "Cirtcuit Breaker が有効", "xpack.apm.agentConfig.configTable.appliedTooltipMessage": "1 つ以上のエージェントにより適用されました", "xpack.apm.agentConfig.configTable.configTable.failurePromptText": "エージェントの構成一覧を取得できませんでした。ユーザーに十分なパーミッションがない可能性があります。", @@ -5320,6 +5541,10 @@ "xpack.apm.agentMetrics.java.threadCount": "平均カウント", "xpack.apm.agentMetrics.java.threadCountChartTitle": "スレッド数", "xpack.apm.agentMetrics.java.threadCountMax": "最高カウント", + "xpack.apm.alertAnnotationButtonAriaLabel": "アラート詳細を表示", + "xpack.apm.alertAnnotationCriticalTitle": "重大アラート", + "xpack.apm.alertAnnotationNoSeverityTitle": "アラート", + "xpack.apm.alertAnnotationWarningTitle": "警告アラート", "xpack.apm.alerting.fields.all_option": "すべて", "xpack.apm.alerting.fields.environment": "環境", "xpack.apm.alerting.fields.service": "サービス", @@ -5347,6 +5572,10 @@ "xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage": "次の条件のため、\\{\\{alertName\\}\\}アラートが実行されています。\n\n- サービス名:\\{\\{context.serviceName\\}\\}\n- タイプ:\\{\\{context.transactionType\\}\\}\n- 環境:\\{\\{context.environment\\}\\}\n- しきい値:\\{\\{context.threshold\\}\\}%\n- トリガーされた値:過去\\{\\{context.interval\\}\\}にエラーの\\{\\{context.triggerValue\\}\\}%", "xpack.apm.alertTypes.transactionErrorRate.description": "サービスのトランザクションエラー率が定義されたしきい値を超過したときにアラートを発行します。", "xpack.apm.alertTypes.transactionErrorRate.reason": "トランザクションエラー率が{serviceName}の{threshold}を超えています (現在の値は{measured}) ", + "xpack.apm.analyzeDataButton.label": "データを分析", + "xpack.apm.analyzeDataButton.tooltip": "実験 - データの分析では、任意のディメンションの結果データを選択してフィルタリングし、パフォーマンスの問題の原因または影響を調査することができます", + "xpack.apm.analyzeDataButtonLabel": "データを分析", + "xpack.apm.analyzeDataButtonLabel.message": "実験 - データの分析では、任意のディメンションの結果データを選択してフィルタリングし、パフォーマンスの問題の原因または影響を調査することができます。", "xpack.apm.anomaly_detection.error.invalid_license": "異常検知を使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。このライセンスがあれば、機械学習を活用して、サービスを監視できます。", "xpack.apm.anomaly_detection.error.missing_read_privileges": "異常検知ジョブを表示するには、機械学習およびAPMの「読み取り」権限が必要です", "xpack.apm.anomaly_detection.error.missing_write_privileges": "異常検知ジョブを作成するには、機械学習およびAPMの「書き込み」権限が必要です", @@ -5359,7 +5588,10 @@ "xpack.apm.anomalyDetectionSetup.linkLabel": "異常検知", "xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText": "「{currentEnvironment}」環境では、まだ異常検知が有効ではありません。クリックすると、セットアップを続行します。", "xpack.apm.anomalyDetectionSetup.notEnabledText": "異常検知はまだ有効ではありません。クリックすると、セットアップを続行します。", + "xpack.apm.api.fleet.cloud_apm_package_policy.requiredRoleOnCloud": "スーパーユーザーロールが付与されたElastic Cloudユーザーのみが操作できます。", + "xpack.apm.api.fleet.fleetSecurityRequired": "FleetおよびSecurityプラグインが必要です", "xpack.apm.apmDescription": "アプリケーション内から自動的に詳細なパフォーマンスメトリックやエラーを集めます。", + "xpack.apm.apmSchema.index": "APMサーバースキーマ - インデックス", "xpack.apm.apmSettings.index": "APM 設定 - インデックス", "xpack.apm.apply.label": "適用", "xpack.apm.chart.annotation.version": "バージョン", @@ -5385,6 +5617,7 @@ "xpack.apm.correlations.correlationsTable.impactLabel": "インパクト", "xpack.apm.correlations.correlationsTable.loadingText": "読み込み中", "xpack.apm.correlations.correlationsTable.noDataText": "データなし", + "xpack.apm.correlations.correlationsTable.percentageLabel": "割合 (%) ", "xpack.apm.correlations.customize.buttonLabel": "フィールドのカスタマイズ", "xpack.apm.correlations.customize.fieldHelpText": "相関関係を分析するフィールドをカスタマイズまたは{reset}します。{docsLink}", "xpack.apm.correlations.customize.fieldHelpTextDocsLink": "デフォルトフィールドの詳細。", @@ -5393,27 +5626,48 @@ "xpack.apm.correlations.customize.fieldPlaceholder": "オプションを選択または作成", "xpack.apm.correlations.customize.thresholdLabel": "しきい値", "xpack.apm.correlations.customize.thresholdPercentile": "{percentile}パーセンタイル", + "xpack.apm.correlations.environmentLabel": "環境", "xpack.apm.correlations.error.chart.overallErrorRateLabel": "全体のエラー率", "xpack.apm.correlations.error.chart.selectedTermErrorRateLabel": "{fieldName}:{fieldValue}", "xpack.apm.correlations.error.chart.title": "経時的なエラー率", "xpack.apm.correlations.error.description": "一部のトランザクションが失敗してエラーが返される理由。相関関係は、データの特定のコホートで想定される原因を検出するのに役立ちます。ホスト、バージョン、または他のカスタムフィールドのいずれか。", "xpack.apm.correlations.error.percentageColumnName": "失敗したトランザクションの%", "xpack.apm.correlations.filteringByLabel": "フィルタリング条件", + "xpack.apm.correlations.latency.chart.numberOfTransactionsLabel": "# トランザクション", "xpack.apm.correlations.latency.chart.overallLatencyDistributionLabel": "全体のレイテンシ分布", "xpack.apm.correlations.latency.chart.selectedTermLatencyDistributionLabel": "{fieldName}:{fieldValue}", "xpack.apm.correlations.latency.chart.title": "レイテンシ分布", "xpack.apm.correlations.latency.description": "サービスが低速になっている原因。相関関係は、データの特定のコホートにあるパフォーマンス低下を特定するのに役立ちます。ホスト、バージョン、または他のカスタムフィールドのいずれか。", "xpack.apm.correlations.latency.percentageColumnName": "低速なトランザクションの%", + "xpack.apm.correlations.latencyCorrelations.cancelButtonTitle": "キャンセル", + "xpack.apm.correlations.latencyCorrelations.chartTitle": "{name}の遅延分布", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel": "フィルター", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationColumnDescription": "サービスの遅延に対するフィールドの影響。0~1の範囲。", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel": "相関関係", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeDescription": "値を除外", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeLabel": "除外", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldNameLabel": "フィールド名", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldValueLabel": "フィールド値", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.filterDescription": "値でフィルタリング", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.filterLabel": "フィルター", + "xpack.apm.correlations.latencyCorrelations.errorTitle": "相関関係の取得中にエラーが発生しました", + "xpack.apm.correlations.latencyCorrelations.noCorrelationsText": "有意な相関関係が見つかりません", + "xpack.apm.correlations.latencyCorrelations.progressAriaLabel": "進捗", + "xpack.apm.correlations.latencyCorrelations.progressTitle": "進捗状況: {progress}%", + "xpack.apm.correlations.latencyCorrelations.refreshButtonTitle": "更新", "xpack.apm.correlations.licenseCheckText": "相関関係を使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。使用すると、パフォーマンスの低下に関連しているフィールドを検出できます。", + "xpack.apm.correlations.serviceLabel": "サービス", "xpack.apm.correlations.tabs.errorRateLabel": "エラー率", "xpack.apm.correlations.tabs.latencyLabel": "レイテンシ", "xpack.apm.correlations.title": "相関関係", + "xpack.apm.correlations.transactionLabel": "トランザクション", "xpack.apm.csm.breakdownFilter.browser": "ブラウザー", "xpack.apm.csm.breakdownFilter.device": "デバイス", "xpack.apm.csm.breakdownFilter.location": "場所", "xpack.apm.csm.breakDownFilter.noBreakdown": "内訳なし", "xpack.apm.csm.breakdownFilter.os": "OS", "xpack.apm.csm.pageViews.analyze": "分析", + "xpack.apm.csm.search.url.close": "閉じる", "xpack.apm.customLink.buttom.create": "カスタムリンクを作成", "xpack.apm.customLink.buttom.create.title": "作成", "xpack.apm.customLink.buttom.manage": "カスタムリンクを管理", @@ -5462,9 +5716,13 @@ "xpack.apm.header.badge.readOnly.text": "読み取り専用", "xpack.apm.header.badge.readOnly.tooltip": "を保存できませんでした", "xpack.apm.helpMenu.upgradeAssistantLink": "アップグレードアシスタント", - "xpack.apm.home.alertsMenu.createAnomalyAlert": "異常アラートを作成", + "xpack.apm.home.alertsMenu.alerts": "アラートとルール", + "xpack.apm.home.alertsMenu.createAnomalyAlert": "異常ルールを作成", + "xpack.apm.home.alertsMenu.createThresholdAlert": "しきい値ルールを作成", "xpack.apm.home.alertsMenu.errorCount": "エラー数", + "xpack.apm.home.alertsMenu.transactionDuration": "レイテンシ", "xpack.apm.home.alertsMenu.transactionErrorRate": "トランザクションエラー率", + "xpack.apm.home.alertsMenu.viewActiveAlerts": "ルールの管理", "xpack.apm.home.serviceMapTabLabel": "サービスマップ", "xpack.apm.instancesLatencyDistributionChartLegend": "インスタンス", "xpack.apm.instancesLatencyDistributionChartLegend.previousPeriod": "前の期間", @@ -5493,7 +5751,7 @@ "xpack.apm.localFilters.titles.location": "場所", "xpack.apm.localFilters.titles.os": "OS", "xpack.apm.localFilters.titles.serviceName": "サービス名", - "xpack.apm.localFilters.titles.transactionUrl": "Url", + "xpack.apm.localFilters.titles.transactionUrl": "URL", "xpack.apm.localFiltersTitle": "フィルター", "xpack.apm.metadataTable.section.agentLabel": "エージェント", "xpack.apm.metadataTable.section.clientLabel": "クライアント", @@ -5518,6 +5776,9 @@ "xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "フィルタリングで検索バーを使用しているときには、機械学習結果が表示されません", "xpack.apm.metrics.transactionChart.throughputLabel": "スループット", "xpack.apm.metrics.transactionChart.viewJob": "ジョブを表示", + "xpack.apm.navigation.serviceMapTitle": "サービスマップ", + "xpack.apm.navigation.servicesTitle": "サービス", + "xpack.apm.navigation.tracesTitle": "トレース", "xpack.apm.notAvailableLabel": "N/A", "xpack.apm.profiling.collapseSimilarFrames": "類似した項目を折りたたむ", "xpack.apm.profiling.highlightFrames": "検索", @@ -5558,6 +5819,7 @@ "xpack.apm.rum.filterGroup.coreWebVitals": "コアWebバイタル", "xpack.apm.rum.filterGroup.seconds": "秒", "xpack.apm.rum.filterGroup.selectBreakdown": "内訳を選択", + "xpack.apm.rum.filters.filterByUrl": "IDでフィルタリング", "xpack.apm.rum.filters.searchResults": "{total}件の検索結果", "xpack.apm.rum.filters.select": "選択してください", "xpack.apm.rum.filters.topPages": "上位のページ", @@ -5579,13 +5841,15 @@ "xpack.apm.rum.visitorBreakdown.operatingSystem": "オペレーティングシステム", "xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration": "平均ページ読み込み時間", "xpack.apm.rum.visitorBreakdownMap.pageLoadDurationByRegion": "地域別ページ読み込み時間 (平均) ", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description": "ブラウザーの開発者ツールを開き、API応答を確認すると、すべてのElasticsearchクエリを検査できます。この設定はKibanaの{advancedSettingsLink}で無効にできます", + "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description": "ブラウザーの開発者ツールを開き、API応答を確認すると、すべてのElasticsearchクエリを検査できます。この設定はKibanaの{advancedSettingsLink}で無効にでkます", "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description.advancedSettings": "高度な設定", "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.title": "調査可能なESクエリ (`apm:enableInspectEsQueries`) ", "xpack.apm.searchInput.filter": "フィルター...", "xpack.apm.selectPlaceholder": "オプションを選択:", "xpack.apm.serviceDetails.errorsTabLabel": "エラー", "xpack.apm.serviceDetails.metrics.cpuUsageChartTitle": "CPU 使用状況", + "xpack.apm.serviceDetails.metrics.errorOccurrencesChart.title": "エラーのオカレンス", + "xpack.apm.serviceDetails.metrics.errorsList.title": "エラー", "xpack.apm.serviceDetails.metrics.memoryUsageChartTitle": "システムメモリー使用状況", "xpack.apm.serviceDetails.metricsTabLabel": "メトリック", "xpack.apm.serviceDetails.nodesTabLabel": "JVM", @@ -5767,6 +6031,7 @@ "xpack.apm.settings.apmIndices.spanIndicesLabel": "スパンインデックス", "xpack.apm.settings.apmIndices.title": "インデックス", "xpack.apm.settings.apmIndices.transactionIndicesLabel": "トランザクションインデックス", + "xpack.apm.settings.createApmPackagePolicy.errorToast.title": "クラウドエージェントポリシーでAPMパッケージポリシーを作成できません", "xpack.apm.settings.customizeApp": "アプリをカスタマイズ", "xpack.apm.settings.customizeUI.customLink": "カスタムリンク", "xpack.apm.settings.customizeUI.customLink.create.failed": "リンクを保存できませんでした!", @@ -5816,6 +6081,41 @@ "xpack.apm.settings.customizeUI.customLink.table.noResultFound": "\"{value}\"に対する結果が見つかりませんでした。", "xpack.apm.settings.customizeUI.customLink.table.url": "URL", "xpack.apm.settings.indices": "インデックス", + "xpack.apm.settings.schema": "スキーマ", + "xpack.apm.settings.schema.confirm.apmServerSettingsCloudLinkText": "クラウドでAPMサーバー設定に移動", + "xpack.apm.settings.schema.confirm.cancelText": "キャンセル", + "xpack.apm.settings.schema.confirm.checkboxLabel": "データストリームに切り替えることを確認する", + "xpack.apm.settings.schema.confirm.descriptionText": "現在、スタック監視はFleetで管理されたAPMではサポートされていません。", + "xpack.apm.settings.schema.confirm.irreversibleWarning.message": "移行中には一時的にAPMデータ収集に影響する可能性があります。移行プロセスは数分で完了します。", + "xpack.apm.settings.schema.confirm.irreversibleWarning.title": "データストリームへの切り替えは元に戻せません。", + "xpack.apm.settings.schema.confirm.switchButtonText": "データストリームに切り替える", + "xpack.apm.settings.schema.confirm.title": "選択内容を確認してください", + "xpack.apm.settings.schema.confirm.unsupportedConfigs.descriptionText": "互換性のあるカスタムapm-server.ymlユーザー設定がFleetサーバー設定に移動されます。削除する前に互換性のない設定について通知されます。", + "xpack.apm.settings.schema.confirm.unsupportedConfigs.title": "次のapm-server.ymlユーザー設定は互換性がないため削除されます", + "xpack.apm.settings.schema.descriptionText": "クラシックAPMインデックスから切り替え、新しいデータストリーム機能をすぐに活用するためのシンプルでシームレスなプロセスを構築しました。このアクションは{irreversibleEmphasis}。また、Fleetへのアクセス権が付与された{superuserEmphasis}のみが実行できます。{dataStreamsDocLink}の詳細を参照してください。", + "xpack.apm.settings.schema.descriptionText.dataStreamsDocLinkText": "データストリーム", + "xpack.apm.settings.schema.descriptionText.irreversibleEmphasisText": "元に戻せません", + "xpack.apm.settings.schema.descriptionText.superuserEmphasisText": "スーパーユーザー", + "xpack.apm.settings.schema.disabledReason": "データストリームへの切り替えを使用できません: {reasons}", + "xpack.apm.settings.schema.disabledReason.cloudApmMigrationEnabled": "クラウド移行が有効ではありません", + "xpack.apm.settings.schema.disabledReason.hasCloudAgentPolicy": "クラウドエージェントポリシーが存在しません", + "xpack.apm.settings.schema.disabledReason.hasRequiredRole": "ユーザーにはスーパーユーザーロールがありません", + "xpack.apm.settings.schema.migrate.classicIndices.currentSetup": "現在の設定", + "xpack.apm.settings.schema.migrate.classicIndices.description": "現在、データのクラシックAPMインデックスを使用しています。このデータスキーマは廃止予定であり、Elastic Stackバージョン8.0でデータストリームに置換されます。", + "xpack.apm.settings.schema.migrate.classicIndices.title": "クラシックAPMインデックス", + "xpack.apm.settings.schema.migrate.dataStreams.betaBadge.description": "データストリームへの切り替えはGAではありません。不具合が発生したら報告してください。", + "xpack.apm.settings.schema.migrate.dataStreams.betaBadge.label": "ベータ", + "xpack.apm.settings.schema.migrate.dataStreams.betaBadge.title": "データストリーム", + "xpack.apm.settings.schema.migrate.dataStreams.buttonText": "データストリームに切り替える", + "xpack.apm.settings.schema.migrate.dataStreams.description": "今後、新しく取り込まれたデータはすべてデータストリームに格納されます。以前に取り込まれたデータはクラシックAPMインデックスに残ります。APMおよびUXアプリは引き続き両方のインデックスをサポートします。", + "xpack.apm.settings.schema.migrate.dataStreams.title": "データストリーム", + "xpack.apm.settings.schema.success.description": "APM統合が設定されました。現在導入されているエージェントからデータを受信できます。統合に適用されたポリシーは自由に確認できます。", + "xpack.apm.settings.schema.success.returnText": "あるいは、{serviceInventoryLink}に戻ることができます。", + "xpack.apm.settings.schema.success.returnText.serviceInventoryLink": "サービスインベントリ", + "xpack.apm.settings.schema.success.title": "データストリームが正常に設定されました。", + "xpack.apm.settings.schema.success.viewIntegrationInFleet.buttonText": "FleetでAPM統合を表示", + "xpack.apm.settings.title": "設定", + "xpack.apm.settings.unsupportedConfigs.errorToast.title": "APMサーバー設定を取り込めません", "xpack.apm.settingsLinkLabel": "設定", "xpack.apm.setupInstructionsButtonLabel": "セットアップの手順", "xpack.apm.significanTerms.license.text": "相関関係APIを使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。", @@ -5885,6 +6185,7 @@ "xpack.apm.transactionDetails.traceNotFound": "選択されたトレースが見つかりません", "xpack.apm.transactionDetails.traceSampleTitle": "トレースのサンプル", "xpack.apm.transactionDetails.transactionLabel": "トランザクション", + "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSamplesAvailable": "サンプルがありません", "xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel": "{transCount} 件のトランザクション", "xpack.apm.transactionDetails.transactionsDurationDistributionChartTitle": "レイテンシ分布", "xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingDescription": "各バケットはサンプルトランザクションを示します。利用可能なサンプルがない場合、おそらくエージェントの構成で設定されたサンプリング制限が原因です。", @@ -5917,14 +6218,153 @@ "xpack.apm.transactionsTable.nameColumnLabel": "名前", "xpack.apm.transactionsTable.notFoundLabel": "トランザクションが見つかりませんでした。", "xpack.apm.transactionsTable.throughputColumnLabel": "スループット", + "xpack.apm.tutorial.agent_config.choosePolicy.helper": "選択したポリシー構成を下のスニペットに追加します。", + "xpack.apm.tutorial.agent_config.choosePolicyLabel": "ポリシーを選択", + "xpack.apm.tutorial.agent_config.defaultStandaloneConfig": "デフォルトのダッシュボード構成", + "xpack.apm.tutorial.agent_config.fleetPoliciesLabel": "Fleetポリシー", + "xpack.apm.tutorial.agent_config.getStartedWithFleet": "Fleetの基本", + "xpack.apm.tutorial.agent_config.manageFleetPolicies": "Fleetポリシーの管理", + "xpack.apm.tutorial.apmAgents.statusCheck.btnLabel": "エージェントステータスを確認", + "xpack.apm.tutorial.apmAgents.statusCheck.errorMessage": "エージェントからまだデータを受け取っていません", + "xpack.apm.tutorial.apmAgents.statusCheck.successMessage": "1 つまたは複数のエージェントからデータを受け取りました", + "xpack.apm.tutorial.apmAgents.statusCheck.text": "アプリケーションが実行されていてエージェントがデータを送信していることを確認してください。", + "xpack.apm.tutorial.apmAgents.statusCheck.title": "エージェントステータス", + "xpack.apm.tutorial.apmAgents.title": "APM エージェント", + "xpack.apm.tutorial.apmServer.callOut.message": "ご使用の APM Server を 7.0 以上に更新してあることを確認してください。 Kibana の管理セクションにある移行アシスタントで 6.x データを移行することもできます。", + "xpack.apm.tutorial.apmServer.callOut.title": "重要:7.0 以上に更新中", + "xpack.apm.tutorial.apmServer.fleet.apmIntegration.button": "APM統合", + "xpack.apm.tutorial.apmServer.fleet.manageApmIntegration.button": "FleetでAPM統合を管理", + "xpack.apm.tutorial.apmServer.fleet.message": "APMA統合は、APMデータ用にElasticsearchテンプレートとIngest Nodeパイプラインをインストールします。", + "xpack.apm.tutorial.apmServer.fleet.title": "Elastic APM (ベータ版) がFleetで提供されました。", + "xpack.apm.tutorial.apmServer.statusCheck.btnLabel": "APM Server ステータスを確認", + "xpack.apm.tutorial.apmServer.statusCheck.errorMessage": "APM Server が検出されました。7.0 以上に更新され、動作中であることを確認してください。", + "xpack.apm.tutorial.apmServer.statusCheck.successMessage": "APM Server が正しくセットアップされました", + "xpack.apm.tutorial.apmServer.statusCheck.text": "APM エージェントの導入を開始する前に、APM Server が動作していることを確認してください。", + "xpack.apm.tutorial.apmServer.statusCheck.title": "APM Server ステータス", "xpack.apm.tutorial.apmServer.title": "APM Server", + "xpack.apm.tutorial.copySnippet": "スニペットをコピー", + "xpack.apm.tutorial.djangoClient.configure.commands.addAgentComment": "インストールされたアプリにエージェントを追加します", + "xpack.apm.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment": "パフォーマンスメトリックを送信するには、追跡ミドルウェアを追加します。", + "xpack.apm.tutorial.djangoClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース", + "xpack.apm.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト:{defaultApmServerUrl}) を設定します", + "xpack.apm.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment": "任意のサービス名を設定します。使用できる文字:", + "xpack.apm.tutorial.djangoClient.configure.commands.setServiceEnvironmentComment": "サービス環境を設定します", + "xpack.apm.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server でシークレットトークンが必要な場合に使います", + "xpack.apm.tutorial.djangoClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション] ({documentationLink}) をご覧ください。", + "xpack.apm.tutorial.djangoClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。", + "xpack.apm.tutorial.djangoClient.configure.title": "エージェントの構成", + "xpack.apm.tutorial.djangoClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。", + "xpack.apm.tutorial.djangoClient.install.title": "APM エージェントのインストール", + "xpack.apm.tutorial.dotNetClient.configureAgent.textPost": "エージェントに「IConfiguration」インスタンスが渡されていない場合、 (例:非 ASP.NET Core アプリケーションの場合) 、エージェントを環境変数で構成することもできます。\n 高度な用途に関しては [ドキュメンテーション] ({documentationLink}) をご覧ください。", + "xpack.apm.tutorial.dotNetClient.configureAgent.title": "appsettings.json ファイルの例:", + "xpack.apm.tutorial.dotNetClient.configureApplication.textPost": "「IConfiguration」インスタンスを渡すのは任意であり、これにより、エージェントはこの「IConfiguration」インスタンス (例:「appsettings.json」ファイル) から構成を読み込みます。", + "xpack.apm.tutorial.dotNetClient.configureApplication.textPre": "「Elastic.Apm.NetCoreAll」パッケージの ASP.NET Core の場合、「Startup.cs」ファイル内の「Configure」メソドの「UseElasticApm」メソドを呼び出します。", + "xpack.apm.tutorial.dotNetClient.configureApplication.title": "エージェントをアプリケーションに追加", + "xpack.apm.tutorial.dotNetClient.download.textPre": "[NuGet] ({allNuGetPackagesLink}) から .NET アプリケーションにエージェントパッケージを追加してください。用途の異なる複数の NuGet パッケージがあります。\n\nEntity Framework Core の ASP.NET Core アプリケーションの場合は、[Elastic.Apm.NetCoreAll] ({netCoreAllApmPackageLink}) パッケージをダウンロードしてください。このパッケージは、自動的にすべてのエージェントコンポーネントをアプリケーションに追加します。\n\n 依存性を最低限に抑えたい場合、ASP.NET Coreの監視のみに[Elastic.Apm.AspNetCore] ({aspNetCorePackageLink}) パッケージ、またはEntity Framework Coreの監視のみに[Elastic.Apm.EfCore] ({efCorePackageLink}) パッケージを使用することができます。\n\n 手動インストルメンテーションのみにパブリック Agent API を使用する場合は、[Elastic.Apm] ({elasticApmPackageLink}) パッケージを使用してください。", + "xpack.apm.tutorial.dotNetClient.download.title": "APM エージェントのダウンロード", + "xpack.apm.tutorial.downloadServer.title": "APM Server をダウンロードして展開します", + "xpack.apm.tutorial.downloadServerRpm": "32 ビットパッケージをお探しですか?[ダウンロードページ] ({downloadPageLink}) をご覧ください。", + "xpack.apm.tutorial.downloadServerTitle": "32 ビットパッケージをお探しですか?[ダウンロードページ] ({downloadPageLink}) をご覧ください。", + "xpack.apm.tutorial.editConfig.textPre": "Elastic Stack の X-Pack セキュアバージョンをご使用の場合、「apm-server.yml」構成ファイルで認証情報を指定する必要があります。", + "xpack.apm.tutorial.editConfig.title": "構成を編集する", "xpack.apm.tutorial.elasticCloudInstructions.title": "APM エージェント", + "xpack.apm.tutorial.flaskClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース", + "xpack.apm.tutorial.flaskClient.configure.commands.configureElasticApmComment": "またはアプリケーションの設定で ELASTIC_APM を使用するよう構成します。", + "xpack.apm.tutorial.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します", + "xpack.apm.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト:{defaultApmServerUrl}) を設定します", + "xpack.apm.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment": "任意のサービス名を設定します。使用できる文字:", + "xpack.apm.tutorial.flaskClient.configure.commands.setServiceEnvironmentComment": "サービス環境を設定します", + "xpack.apm.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server でシークレットトークンが必要な場合に使います", + "xpack.apm.tutorial.flaskClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション] ({documentationLink}) をご覧ください。", + "xpack.apm.tutorial.flaskClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。", + "xpack.apm.tutorial.flaskClient.configure.title": "エージェントの構成", + "xpack.apm.tutorial.flaskClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。", + "xpack.apm.tutorial.flaskClient.install.title": "APM エージェントのインストール", + "xpack.apm.tutorial.goClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します:", + "xpack.apm.tutorial.goClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト:{defaultApmServerUrl}) を設定します", + "xpack.apm.tutorial.goClient.configure.commands.setServiceEnvironment": "サービス環境を設定します", + "xpack.apm.tutorial.goClient.configure.commands.setServiceNameComment": "サービス名を設定します。使用できる文字は # a-z、A-Z、0-9、-、_、スペースです。", + "xpack.apm.tutorial.goClient.configure.commands.usedExecutableNameComment": "ELASTIC_APM_SERVICE_NAME が指定されていない場合、実行ファイルの名前が使用されます。", + "xpack.apm.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment": "APM Server でシークレットトークンが必要な場合に使います", + "xpack.apm.tutorial.goClient.configure.textPost": "高度な構成に関しては [ドキュメンテーション] ({documentationLink}) をご覧ください。", + "xpack.apm.tutorial.goClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは実行ファイル名または「ELASTIC_APM_SERVICE_NAME」環境変数に基づいてプログラムで作成されます。", + "xpack.apm.tutorial.goClient.configure.title": "エージェントの構成", + "xpack.apm.tutorial.goClient.install.textPre": "Go の APM エージェントパッケージをインストールします。", + "xpack.apm.tutorial.goClient.install.title": "APM エージェントのインストール", + "xpack.apm.tutorial.goClient.instrument.textPost": "Go のソースコードのインストルメンテーションの詳細ガイドは、[ドキュメンテーション] ({documentationLink}) をご覧ください。", + "xpack.apm.tutorial.goClient.instrument.textPre": "提供されたインストルメンテーションモジュールの 1 つ、またはトレーサー API を直接使用して、Go アプリケーションにインストルメンテーションを設定します。", + "xpack.apm.tutorial.goClient.instrument.title": "アプリケーションのインストルメンテーション", + "xpack.apm.tutorial.introduction": "アプリケーション内から詳細なパフォーマンスメトリックやエラーを収集します。", + "xpack.apm.tutorial.javaClient.download.textPre": "[Maven Central] ({mavenCentralLink}) からエージェントをダウンロードします。アプリケーションにエージェントを依存関係として「追加しない」でください。", + "xpack.apm.tutorial.javaClient.download.title": "APM エージェントのダウンロード", + "xpack.apm.tutorial.javaClient.startApplication.textPost": "構成オプションと高度な用途に関しては、[ドキュメンテーション] ({documentationLink}) をご覧ください。", + "xpack.apm.tutorial.javaClient.startApplication.textPre": "「-javaagent」フラグを追加し、システムプロパティを使用してエージェントを構成します。\n\n * 任意のサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです) \n * カスタム APM Server URL (デフォルト:{customApmServerUrl}) を設定します\n * APM Server シークレットトークンを設定します\n * サービス環境を設定します\n * アプリケーションのベースパッケージを設定します", + "xpack.apm.tutorial.javaClient.startApplication.title": "javaagent フラグでアプリケーションを起動", + "xpack.apm.tutorial.jsClient.enableRealUserMonitoring.textPre": "デフォルトでは、APM Server を実行すると RUM サポートは無効になります。RUM サポートを有効にする手順については、[ドキュメンテーション] ({documentationLink}) をご覧ください。", + "xpack.apm.tutorial.jsClient.enableRealUserMonitoring.title": "APM Server のリアルユーザー監視サポートを有効にする", + "xpack.apm.tutorial.jsClient.installDependency.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト:{defaultApmServerUrl}) を設定します", + "xpack.apm.tutorial.jsClient.installDependency.commands.setRequiredServiceNameComment": "任意のサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです) ", + "xpack.apm.tutorial.jsClient.installDependency.commands.setServiceEnvironmentComment": "サービス環境を設定します", + "xpack.apm.tutorial.jsClient.installDependency.commands.setServiceVersionComment": "サービスバージョンを設定します (ソースマップ機能に必要) ", + "xpack.apm.tutorial.jsClient.installDependency.textPost": "React や Angular などのフレームワーク統合には、カスタム依存関係があります。詳細は [統合ドキュメント] ({docLink}) をご覧ください。", + "xpack.apm.tutorial.jsClient.installDependency.textPre": "「npm install @elastic/apm-rum --save」でエージェントをアプリケーションへの依存関係としてインストールできます。\n\nその後で以下のようにアプリケーションでエージェントを初期化して構成できます。", + "xpack.apm.tutorial.jsClient.installDependency.title": "エージェントを依存関係としてセットアップ", + "xpack.apm.tutorial.jsClient.scriptTags.textPre": "または、スクリプトタグを使用してエージェントのセットアップと構成ができます。` を追加