diff --git a/.i18nrc.json b/.i18nrc.json index dc01a10b6a686b..57dffa4147e525 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -3,6 +3,7 @@ "console": "src/plugins/console", "core": "src/core", "discover": "src/plugins/discover", + "bfetch": "src/plugins/bfetch", "dashboard": "src/plugins/dashboard", "data": "src/plugins/data", "embeddableApi": "src/plugins/embeddable", diff --git a/api_docs/apm.json b/api_docs/apm.json index 95cc86814e99a0..7eee0349fa349e 100644 --- a/api_docs/apm.json +++ b/api_docs/apm.json @@ -309,7 +309,7 @@ ], "source": { "path": "x-pack/plugins/apm/server/plugin.ts", - "lineNumber": 270 + "lineNumber": 269 }, "deprecated": false, "children": [ @@ -331,7 +331,7 @@ ], "source": { "path": "x-pack/plugins/apm/server/plugin.ts", - "lineNumber": 270 + "lineNumber": 269 }, "deprecated": false, "isRequired": true @@ -351,7 +351,7 @@ ], "source": { "path": "x-pack/plugins/apm/server/plugin.ts", - "lineNumber": 289 + "lineNumber": 288 }, "deprecated": false, "children": [], @@ -653,7 +653,13 @@ ">; delete: (deleteParams: { id: string; }) => Promise<", "DeleteResponse", ">; } | undefined>; }; start: () => Promise; }; ruleRegistry: { setup: ", - "RuleDataPluginService", + { + "pluginId": "ruleRegistry", + "scope": "server", + "docId": "kibRuleRegistryPluginApi", + "section": "def-server.RuleRegistryPluginSetupContract", + "text": "RuleRegistryPluginSetupContract" + }, "; start: () => Promise; }; security?: { setup: ", { "pluginId": "security", diff --git a/api_docs/charts.json b/api_docs/charts.json index d74e0209d6395e..29c1c163d19a03 100644 --- a/api_docs/charts.json +++ b/api_docs/charts.json @@ -179,7 +179,9 @@ "type": "Array", "tags": [], "label": "xValues", - "description": [], + "description": [ + "sorted and unquie x values" + ], "signature": [ "number[]" ], @@ -417,7 +419,9 @@ "type": "Object", "tags": [], "label": "splitSeriesAccessorFnMap", - "description": [], + "description": [ + "needed when using `splitSeriesAccessors` as `AccessorFn`" + ], "signature": [ "Map; readonly filebeat: { readonly base: string; readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; }; readonly auditbeat: { readonly base: string; }; readonly metricbeat: { readonly base: string; readonly configure: string; readonly httpEndpoint: string; readonly install: string; readonly start: string; }; readonly enterpriseSearch: { readonly base: string; readonly appSearchBase: string; readonly workplaceSearchBase: string; }; readonly heartbeat: { readonly base: string; }; readonly logstash: { readonly base: string; }; readonly functionbeat: { readonly base: string; }; readonly winlogbeat: { readonly base: string; }; readonly aggs: { readonly composite: string; readonly composite_missing_bucket: string; readonly date_histogram: string; readonly date_range: string; readonly date_format_pattern: string; readonly filter: string; readonly filters: string; readonly geohash_grid: string; readonly histogram: string; readonly ip_range: string; readonly range: string; readonly significant_terms: string; readonly terms: string; readonly avg: string; readonly avg_bucket: string; readonly max_bucket: string; readonly min_bucket: string; readonly sum_bucket: string; readonly cardinality: string; readonly count: string; readonly cumulative_sum: string; readonly derivative: string; readonly geo_bounds: string; readonly geo_centroid: string; readonly max: string; readonly median: string; readonly min: string; readonly moving_avg: string; readonly percentile_ranks: string; readonly serial_diff: string; readonly std_dev: string; readonly sum: string; readonly top_hits: string; }; readonly runtimeFields: { readonly overview: string; readonly mapping: string; }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; readonly painless: string; readonly painlessApi: string; readonly painlessLangSpec: string; readonly painlessSyntax: string; readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; readonly search: { readonly sessions: string; }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; }; readonly addData: string; readonly kibana: string; readonly upgradeAssistant: string; readonly elasticsearch: Record; readonly siem: { readonly guide: string; readonly gettingStarted: string; }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; readonly luceneQuerySyntax: string; readonly percolate: string; readonly queryDsl: string; }; readonly date: { readonly dateMath: string; readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; readonly transforms: Record; readonly visualize: Record; readonly apis: Readonly<{ bulkIndexAlias: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; putComponentTemplate: string; painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; timeUnits: string; updateTransform: string; }>; readonly observability: Record; readonly alerting: Record; readonly maps: Record; readonly monitoring: Record; readonly security: Readonly<{ apiKeyServiceSettings: string; clusterPrivileges: string; elasticsearchSettings: string; elasticsearchEnableSecurity: string; indicesPrivileges: string; kibanaTLS: string; kibanaPrivileges: string; mappingRoles: string; mappingRolesFieldRules: string; runAsPrivilege: string; }>; readonly watcher: Record; readonly ccs: Record; readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; }" + "{ readonly canvas: { readonly guide: string; }; readonly dashboard: { readonly guide: string; readonly drilldowns: string; readonly drilldownsTriggerPicker: string; readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; readonly discover: Record; readonly filebeat: { readonly base: string; readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; }; readonly auditbeat: { readonly base: string; }; readonly metricbeat: { readonly base: string; readonly configure: string; readonly httpEndpoint: string; readonly install: string; readonly start: string; }; readonly enterpriseSearch: { readonly base: string; readonly appSearchBase: string; readonly workplaceSearchBase: string; }; readonly heartbeat: { readonly base: string; }; readonly logstash: { readonly base: string; }; readonly functionbeat: { readonly base: string; }; readonly winlogbeat: { readonly base: string; }; readonly aggs: { readonly composite: string; readonly composite_missing_bucket: string; readonly date_histogram: string; readonly date_range: string; readonly date_format_pattern: string; readonly filter: string; readonly filters: string; readonly geohash_grid: string; readonly histogram: string; readonly ip_range: string; readonly range: string; readonly significant_terms: string; readonly terms: string; readonly avg: string; readonly avg_bucket: string; readonly max_bucket: string; readonly min_bucket: string; readonly sum_bucket: string; readonly cardinality: string; readonly count: string; readonly cumulative_sum: string; readonly derivative: string; readonly geo_bounds: string; readonly geo_centroid: string; readonly max: string; readonly median: string; readonly min: string; readonly moving_avg: string; readonly percentile_ranks: string; readonly serial_diff: string; readonly std_dev: string; readonly sum: string; readonly top_hits: string; }; readonly runtimeFields: { readonly overview: string; readonly mapping: string; }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; readonly painless: string; readonly painlessApi: string; readonly painlessLangSpec: string; readonly painlessSyntax: string; readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; readonly search: { readonly sessions: string; }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; readonly upgradeAssistant: string; readonly elasticsearch: Record; readonly siem: { readonly guide: string; readonly gettingStarted: string; }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; readonly luceneQuerySyntax: string; readonly percolate: string; readonly queryDsl: string; }; readonly date: { readonly dateMath: string; readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; readonly transforms: Record; readonly visualize: Record; readonly apis: Readonly<{ bulkIndexAlias: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; putComponentTemplate: string; painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; timeUnits: string; updateTransform: string; }>; readonly observability: Record; readonly alerting: Record; readonly maps: Record; readonly monitoring: Record; readonly security: Readonly<{ apiKeyServiceSettings: string; clusterPrivileges: string; elasticsearchSettings: string; elasticsearchEnableSecurity: string; indicesPrivileges: string; kibanaTLS: string; kibanaPrivileges: string; mappingRoles: string; mappingRolesFieldRules: string; runAsPrivilege: string; }>; readonly watcher: Record; readonly ccs: Record; readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; }" ], "source": { "path": "src/core/public/doc_links/doc_links_service.ts", - "lineNumber": 411 + "lineNumber": 412 }, "deprecated": false } @@ -8748,7 +8748,9 @@ "type": "string", "tags": [], "label": "endpoint", - "description": [], + "description": [ + "- String descriptor of the endpoint e.g. `cluster.getSettings` or `ping`." + ], "signature": [ "string" ], @@ -8765,7 +8767,9 @@ "type": "Object", "tags": [], "label": "clientParams", - "description": [], + "description": [ + "- A dictionary of parameters that will be passed directly to the Elasticsearch JS client." + ], "signature": [ "Record" ], @@ -8782,7 +8786,9 @@ "type": "Object", "tags": [], "label": "options", - "description": [], + "description": [ + "- Options that affect the way we call the API and process the result." + ], "signature": [ { "pluginId": "core", @@ -10803,7 +10809,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 87 + "lineNumber": 96 }, "deprecated": false, "children": [ @@ -10824,7 +10830,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 100 + "lineNumber": 109 }, "deprecated": false, "children": [ @@ -10840,7 +10846,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 100 + "lineNumber": 109 }, "deprecated": false, "isRequired": true @@ -10857,7 +10863,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 100 + "lineNumber": 109 }, "deprecated": false, "isRequired": true @@ -10876,7 +10882,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 100 + "lineNumber": 109 }, "deprecated": false, "isRequired": false @@ -10901,7 +10907,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 116 + "lineNumber": 125 }, "deprecated": false, "children": [ @@ -10917,7 +10923,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 116 + "lineNumber": 125 }, "deprecated": false, "isRequired": true @@ -10934,7 +10940,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 116 + "lineNumber": 125 }, "deprecated": false, "isRequired": true @@ -10953,7 +10959,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 116 + "lineNumber": 125 }, "deprecated": false, "isRequired": false @@ -10978,7 +10984,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 129 + "lineNumber": 138 }, "deprecated": false, "children": [ @@ -10994,7 +11000,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 129 + "lineNumber": 138 }, "deprecated": false, "isRequired": true @@ -11013,7 +11019,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 129 + "lineNumber": 138 }, "deprecated": false, "isRequired": false @@ -11038,7 +11044,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 145 + "lineNumber": 154 }, "deprecated": false, "children": [ @@ -11054,7 +11060,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 145 + "lineNumber": 154 }, "deprecated": false, "isRequired": true @@ -11073,7 +11079,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 145 + "lineNumber": 154 }, "deprecated": false, "isRequired": false @@ -19544,7 +19550,7 @@ "The os platform" ], "signature": [ - "\"linux\" | \"aix\" | \"android\" | \"darwin\" | \"freebsd\" | \"openbsd\" | \"sunos\" | \"win32\" | \"cygwin\"" + "\"linux\" | \"aix\" | \"android\" | \"darwin\" | \"freebsd\" | \"openbsd\" | \"sunos\" | \"win32\" | \"cygwin\" | \"netbsd\"" ], "source": { "path": "src/core/server/metrics/collectors/types.ts", @@ -22315,7 +22321,7 @@ ], "source": { "path": "node_modules/@kbn/config/target/deprecation/types.d.ts", - "lineNumber": 70 + "lineNumber": 79 }, "deprecated": false, "initialIsOpen": false diff --git a/api_docs/core_application.json b/api_docs/core_application.json index 13b110900ab530..e890b6f92a2ad6 100644 --- a/api_docs/core_application.json +++ b/api_docs/core_application.json @@ -115,7 +115,9 @@ "type": "string", "tags": [], "label": "basePath", - "description": [], + "description": [ + "the URL path scope for the sub history" + ], "signature": [ "string" ], @@ -207,7 +209,9 @@ "type": "CompoundType", "tags": [], "label": "pathOrLocation", - "description": [], + "description": [ + "a string or location descriptor" + ], "signature": [ "string | ", "LocationDescriptorObject", @@ -266,7 +270,9 @@ "type": "CompoundType", "tags": [], "label": "pathOrLocation", - "description": [], + "description": [ + "a string or location descriptor" + ], "signature": [ "string | ", "LocationDescriptorObject", @@ -323,7 +329,9 @@ "type": "number", "tags": [], "label": "n", - "description": [], + "description": [ + "number of positions in the stack to go. Negative numbers indicate number of entries backward, positive\nnumbers for forwards. If passed 0, the current location will be reloaded. If `n` exceeds the number of\nentries available, this is a no-op." + ], "signature": [ "number" ], @@ -449,7 +457,9 @@ "type": "Function", "tags": [], "label": "listener", - "description": [], + "description": [ + "a function that receives location updates." + ], "signature": [ "(location: ", "Location", diff --git a/api_docs/core_saved_objects.json b/api_docs/core_saved_objects.json index f18d9d7cf12772..adf0612a28faf8 100644 --- a/api_docs/core_saved_objects.json +++ b/api_docs/core_saved_objects.json @@ -161,7 +161,9 @@ "type": "Array", "tags": [], "label": "objects", - "description": [], + "description": [ + "- [{ type, id, attributes, references, migrationVersion }]" + ], "signature": [ { "pluginId": "core", @@ -451,7 +453,9 @@ "type": "Array", "tags": [], "label": "objects", - "description": [], + "description": [ + "- an array ids, or an array of objects containing id and optionally type" + ], "signature": [ "{ id: string; type: string; }[]" ], @@ -9362,7 +9366,9 @@ "type": "string", "tags": [], "label": "namespace", - "description": [], + "description": [ + "The namespace ID, which must be either a non-empty string or `undefined`." + ], "signature": [ "string | undefined" ], @@ -9400,7 +9406,9 @@ "type": "string", "tags": [], "label": "namespace", - "description": [], + "description": [ + "The namespace string, which must be non-empty." + ], "signature": [ "string" ], diff --git a/api_docs/data.json b/api_docs/data.json index ab5196934d855b..1ba62ea9946549 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -5610,7 +5610,9 @@ "type": "boolean", "tags": [], "label": "refresh", - "description": [], + "description": [ + "Force refresh of index pattern list" + ], "signature": [ "boolean" ], @@ -5648,7 +5650,9 @@ "type": "boolean", "tags": [], "label": "refresh", - "description": [], + "description": [ + "Force refresh of index pattern list" + ], "signature": [ "boolean" ], @@ -5751,7 +5755,9 @@ "type": "boolean", "tags": [], "label": "refresh", - "description": [], + "description": [ + "Force refresh of index pattern list" + ], "signature": [ "boolean" ], @@ -5789,7 +5795,9 @@ "type": "string", "tags": [], "label": "id", - "description": [], + "description": [ + "optionally clear a single id" + ], "signature": [ "string | undefined" ], @@ -5853,6 +5861,26 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "data", + "id": "def-public.IndexPatternsService.getDefaultId", + "type": "Function", + "tags": [], + "label": "getDefaultId", + "description": [ + "\nGet default index pattern id" + ], + "signature": [ + "() => Promise" + ], + "source": { + "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", + "lineNumber": 206 + }, + "deprecated": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "data", "id": "def-public.IndexPatternsService.setDefault", @@ -5863,30 +5891,30 @@ "\nOptionally set default index pattern, unless force = true" ], "signature": [ - "(id: string, force?: boolean) => Promise" + "(id: string | null, force?: boolean) => Promise" ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 208 + "lineNumber": 216 }, "deprecated": false, "children": [ { "parentPluginId": "data", "id": "def-public.IndexPatternsService.setDefault.$1", - "type": "string", + "type": "CompoundType", "tags": [], "label": "id", "description": [], "signature": [ - "string" + "string | null" ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 208 + "lineNumber": 216 }, "deprecated": false, - "isRequired": true + "isRequired": false }, { "parentPluginId": "data", @@ -5900,7 +5928,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 208 + "lineNumber": 216 }, "deprecated": false, "isRequired": true @@ -5930,7 +5958,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 219 + "lineNumber": 227 }, "deprecated": false, "children": [ @@ -5952,7 +5980,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 219 + "lineNumber": 227 }, "deprecated": false, "isRequired": true @@ -6000,7 +6028,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 235 + "lineNumber": 243 }, "deprecated": false, "children": [ @@ -6030,7 +6058,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 236 + "lineNumber": 244 }, "deprecated": false, "isRequired": true @@ -6054,7 +6082,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 237 + "lineNumber": 245 }, "deprecated": false, "isRequired": false @@ -6086,7 +6114,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 250 + "lineNumber": 258 }, "deprecated": false, "children": [ @@ -6108,7 +6136,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 250 + "lineNumber": 258 }, "deprecated": false, "isRequired": true @@ -6154,7 +6182,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 327 + "lineNumber": 335 }, "deprecated": false, "children": [ @@ -6164,7 +6192,9 @@ "type": "Array", "tags": [], "label": "fields", - "description": [], + "description": [ + ": FieldSpec[]" + ], "signature": [ { "pluginId": "data", @@ -6177,7 +6207,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 327 + "lineNumber": 335 }, "deprecated": false, "isRequired": true @@ -6188,7 +6218,9 @@ "type": "Object", "tags": [], "label": "fieldAttrs", - "description": [], + "description": [ + ": FieldAttrs" + ], "signature": [ { "pluginId": "data", @@ -6201,7 +6233,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 327 + "lineNumber": 335 }, "deprecated": false, "isRequired": false @@ -6242,7 +6274,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 343 + "lineNumber": 351 }, "deprecated": false, "children": [ @@ -6267,7 +6299,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 343 + "lineNumber": 351 }, "deprecated": false, "isRequired": true @@ -6299,7 +6331,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 465 + "lineNumber": 473 }, "deprecated": false, "children": [ @@ -6315,7 +6347,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 465 + "lineNumber": 473 }, "deprecated": false, "isRequired": true @@ -6353,7 +6385,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 484 + "lineNumber": 492 }, "deprecated": false, "children": [ @@ -6375,7 +6407,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 484 + "lineNumber": 492 }, "deprecated": false, "isRequired": true @@ -6392,7 +6424,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 484 + "lineNumber": 492 }, "deprecated": false, "isRequired": true @@ -6432,7 +6464,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "children": [ @@ -6454,7 +6486,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "isRequired": true @@ -6473,7 +6505,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "isRequired": true @@ -6492,7 +6524,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "isRequired": true @@ -6530,7 +6562,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 522 + "lineNumber": 530 }, "deprecated": false, "children": [ @@ -6552,7 +6584,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 522 + "lineNumber": 530 }, "deprecated": false, "isRequired": true @@ -6571,7 +6603,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 522 + "lineNumber": 530 }, "deprecated": false, "isRequired": true @@ -6601,7 +6633,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 550 + "lineNumber": 558 }, "deprecated": false, "children": [ @@ -6623,7 +6655,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 551 + "lineNumber": 559 }, "deprecated": false, "isRequired": true @@ -6640,7 +6672,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 552 + "lineNumber": 560 }, "deprecated": false, "isRequired": true @@ -6657,7 +6689,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 553 + "lineNumber": 561 }, "deprecated": false, "isRequired": true @@ -6679,7 +6711,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 636 + "lineNumber": 644 }, "deprecated": false, "children": [ @@ -6697,7 +6729,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 636 + "lineNumber": 644 }, "deprecated": false, "isRequired": true @@ -7617,7 +7649,7 @@ "plugin": "maps", "link": { "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", - "lineNumber": 266 + "lineNumber": 273 } }, { @@ -8515,7 +8547,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 103 + "lineNumber": 104 }, "deprecated": false, "children": [ @@ -8531,7 +8563,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 103 + "lineNumber": 104 }, "deprecated": false, "isRequired": true @@ -8560,7 +8592,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 110 + "lineNumber": 111 }, "deprecated": false, "children": [ @@ -8576,7 +8608,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 110 + "lineNumber": 111 }, "deprecated": false, "isRequired": true @@ -10812,6 +10844,22 @@ "lineNumber": 23 }, "deprecated": false + }, + { + "parentPluginId": "data", + "id": "def-public.ApplyGlobalFilterActionContext.controlledBy", + "type": "string", + "tags": [], + "label": "controlledBy", + "description": [], + "signature": [ + "string | undefined" + ], + "source": { + "path": "src/plugins/data/public/actions/apply_filter_action.ts", + "lineNumber": 26 + }, + "deprecated": false } ], "initialIsOpen": false @@ -11489,29 +11537,29 @@ { "plugin": "maps", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", - "lineNumber": 10 + "path": "x-pack/plugins/maps/public/classes/fields/es_doc_field.ts", + "lineNumber": 12 } }, { "plugin": "maps", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", - "lineNumber": 360 + "path": "x-pack/plugins/maps/public/classes/fields/es_doc_field.ts", + "lineNumber": 45 } }, { "plugin": "maps", "link": { - "path": "x-pack/plugins/maps/public/classes/fields/es_doc_field.ts", - "lineNumber": 12 + "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", + "lineNumber": 10 } }, { "plugin": "maps", "link": { - "path": "x-pack/plugins/maps/public/classes/fields/es_doc_field.ts", - "lineNumber": 45 + "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", + "lineNumber": 367 } }, { @@ -14884,21 +14932,21 @@ "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts", - "lineNumber": 40 + "lineNumber": 44 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts", - "lineNumber": 55 + "lineNumber": 60 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts", - "lineNumber": 60 + "lineNumber": 65 } }, { @@ -15913,7 +15961,7 @@ "plugin": "maps", "link": { "path": "x-pack/plugins/maps/public/embeddable/types.ts", - "lineNumber": 44 + "lineNumber": 45 } }, { @@ -19699,7 +19747,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 41 + "lineNumber": 42 }, "deprecated": false, "initialIsOpen": false @@ -20185,7 +20233,7 @@ "section": "def-common.IndexPattern", "text": "IndexPattern" }, - " | null>; setDefault: (id: string, force?: boolean) => Promise; getFieldsForWildcard: (options: ", + " | null>; getDefaultId: () => Promise; setDefault: (id: string | null, force?: boolean) => Promise; getFieldsForWildcard: (options: ", { "pluginId": "data", "scope": "common", @@ -20311,7 +20359,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 642 + "lineNumber": 650 }, "deprecated": false, "initialIsOpen": false @@ -21055,7 +21103,7 @@ "description": [], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 52 + "lineNumber": 53 }, "deprecated": false }, @@ -21071,7 +21119,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 52 + "lineNumber": 53 }, "deprecated": false } @@ -21914,7 +21962,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 66 + "lineNumber": 67 }, "deprecated": false } @@ -21936,7 +21984,7 @@ "section": "def-common.Filter", "text": "Filter" }, - ") => { meta: { negate: boolean; alias: string | null; disabled: boolean; controlledBy?: string | undefined; index?: string | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; $state?: ", + ") => { meta: { negate: boolean; alias: string | null; disabled: boolean; controlledBy?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; $state?: ", { "pluginId": "data", "scope": "common", @@ -21981,7 +22029,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 77 + "lineNumber": 78 }, "deprecated": false } @@ -22047,7 +22095,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 94 + "lineNumber": 95 }, "deprecated": false } @@ -26095,7 +26143,7 @@ "section": "def-common.IndexPattern", "text": "IndexPattern" }, - " | null>; setDefault: (id: string, force?: boolean) => Promise; getFieldsForWildcard: (options: ", + " | null>; getDefaultId: () => Promise; setDefault: (id: string | null, force?: boolean) => Promise; getFieldsForWildcard: (options: ", { "pluginId": "data", "scope": "common", @@ -28430,7 +28478,9 @@ "type": "boolean", "tags": [], "label": "refresh", - "description": [], + "description": [ + "Force refresh of index pattern list" + ], "signature": [ "boolean" ], @@ -28468,7 +28518,9 @@ "type": "boolean", "tags": [], "label": "refresh", - "description": [], + "description": [ + "Force refresh of index pattern list" + ], "signature": [ "boolean" ], @@ -28571,7 +28623,9 @@ "type": "boolean", "tags": [], "label": "refresh", - "description": [], + "description": [ + "Force refresh of index pattern list" + ], "signature": [ "boolean" ], @@ -28609,7 +28663,9 @@ "type": "string", "tags": [], "label": "id", - "description": [], + "description": [ + "optionally clear a single id" + ], "signature": [ "string | undefined" ], @@ -28673,6 +28729,26 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "data", + "id": "def-server.IndexPatternsService.getDefaultId", + "type": "Function", + "tags": [], + "label": "getDefaultId", + "description": [ + "\nGet default index pattern id" + ], + "signature": [ + "() => Promise" + ], + "source": { + "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", + "lineNumber": 206 + }, + "deprecated": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "data", "id": "def-server.IndexPatternsService.setDefault", @@ -28683,30 +28759,30 @@ "\nOptionally set default index pattern, unless force = true" ], "signature": [ - "(id: string, force?: boolean) => Promise" + "(id: string | null, force?: boolean) => Promise" ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 208 + "lineNumber": 216 }, "deprecated": false, "children": [ { "parentPluginId": "data", "id": "def-server.IndexPatternsService.setDefault.$1", - "type": "string", + "type": "CompoundType", "tags": [], "label": "id", "description": [], "signature": [ - "string" + "string | null" ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 208 + "lineNumber": 216 }, "deprecated": false, - "isRequired": true + "isRequired": false }, { "parentPluginId": "data", @@ -28720,7 +28796,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 208 + "lineNumber": 216 }, "deprecated": false, "isRequired": true @@ -28750,7 +28826,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 219 + "lineNumber": 227 }, "deprecated": false, "children": [ @@ -28772,7 +28848,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 219 + "lineNumber": 227 }, "deprecated": false, "isRequired": true @@ -28820,7 +28896,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 235 + "lineNumber": 243 }, "deprecated": false, "children": [ @@ -28850,7 +28926,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 236 + "lineNumber": 244 }, "deprecated": false, "isRequired": true @@ -28874,7 +28950,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 237 + "lineNumber": 245 }, "deprecated": false, "isRequired": false @@ -28906,7 +28982,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 250 + "lineNumber": 258 }, "deprecated": false, "children": [ @@ -28928,7 +29004,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 250 + "lineNumber": 258 }, "deprecated": false, "isRequired": true @@ -28974,7 +29050,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 327 + "lineNumber": 335 }, "deprecated": false, "children": [ @@ -28984,7 +29060,9 @@ "type": "Array", "tags": [], "label": "fields", - "description": [], + "description": [ + ": FieldSpec[]" + ], "signature": [ { "pluginId": "data", @@ -28997,7 +29075,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 327 + "lineNumber": 335 }, "deprecated": false, "isRequired": true @@ -29008,7 +29086,9 @@ "type": "Object", "tags": [], "label": "fieldAttrs", - "description": [], + "description": [ + ": FieldAttrs" + ], "signature": [ { "pluginId": "data", @@ -29021,7 +29101,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 327 + "lineNumber": 335 }, "deprecated": false, "isRequired": false @@ -29062,7 +29142,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 343 + "lineNumber": 351 }, "deprecated": false, "children": [ @@ -29087,7 +29167,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 343 + "lineNumber": 351 }, "deprecated": false, "isRequired": true @@ -29119,7 +29199,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 465 + "lineNumber": 473 }, "deprecated": false, "children": [ @@ -29135,7 +29215,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 465 + "lineNumber": 473 }, "deprecated": false, "isRequired": true @@ -29173,7 +29253,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 484 + "lineNumber": 492 }, "deprecated": false, "children": [ @@ -29195,7 +29275,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 484 + "lineNumber": 492 }, "deprecated": false, "isRequired": true @@ -29212,7 +29292,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 484 + "lineNumber": 492 }, "deprecated": false, "isRequired": true @@ -29252,7 +29332,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "children": [ @@ -29274,7 +29354,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "isRequired": true @@ -29293,7 +29373,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "isRequired": true @@ -29312,7 +29392,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "isRequired": true @@ -29350,7 +29430,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 522 + "lineNumber": 530 }, "deprecated": false, "children": [ @@ -29372,7 +29452,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 522 + "lineNumber": 530 }, "deprecated": false, "isRequired": true @@ -29391,7 +29471,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 522 + "lineNumber": 530 }, "deprecated": false, "isRequired": true @@ -29421,7 +29501,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 550 + "lineNumber": 558 }, "deprecated": false, "children": [ @@ -29443,7 +29523,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 551 + "lineNumber": 559 }, "deprecated": false, "isRequired": true @@ -29460,7 +29540,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 552 + "lineNumber": 560 }, "deprecated": false, "isRequired": true @@ -29477,7 +29557,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 553 + "lineNumber": 561 }, "deprecated": false, "isRequired": true @@ -29499,7 +29579,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 636 + "lineNumber": 644 }, "deprecated": false, "children": [ @@ -29517,7 +29597,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 636 + "lineNumber": 644 }, "deprecated": false, "isRequired": true @@ -29619,7 +29699,9 @@ "type": "boolean", "tags": [], "label": "refresh", - "description": [], + "description": [ + "Force refresh of index pattern list" + ], "signature": [ "boolean" ], @@ -29657,7 +29739,9 @@ "type": "boolean", "tags": [], "label": "refresh", - "description": [], + "description": [ + "Force refresh of index pattern list" + ], "signature": [ "boolean" ], @@ -29760,7 +29844,9 @@ "type": "boolean", "tags": [], "label": "refresh", - "description": [], + "description": [ + "Force refresh of index pattern list" + ], "signature": [ "boolean" ], @@ -29798,7 +29884,9 @@ "type": "string", "tags": [], "label": "id", - "description": [], + "description": [ + "optionally clear a single id" + ], "signature": [ "string | undefined" ], @@ -29862,6 +29950,26 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "data", + "id": "def-server.IndexPatternsService.getDefaultId", + "type": "Function", + "tags": [], + "label": "getDefaultId", + "description": [ + "\nGet default index pattern id" + ], + "signature": [ + "() => Promise" + ], + "source": { + "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", + "lineNumber": 206 + }, + "deprecated": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "data", "id": "def-server.IndexPatternsService.setDefault", @@ -29872,30 +29980,30 @@ "\nOptionally set default index pattern, unless force = true" ], "signature": [ - "(id: string, force?: boolean) => Promise" + "(id: string | null, force?: boolean) => Promise" ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 208 + "lineNumber": 216 }, "deprecated": false, "children": [ { "parentPluginId": "data", "id": "def-server.IndexPatternsService.setDefault.$1", - "type": "string", + "type": "CompoundType", "tags": [], "label": "id", "description": [], "signature": [ - "string" + "string | null" ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 208 + "lineNumber": 216 }, "deprecated": false, - "isRequired": true + "isRequired": false }, { "parentPluginId": "data", @@ -29909,7 +30017,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 208 + "lineNumber": 216 }, "deprecated": false, "isRequired": true @@ -29939,7 +30047,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 219 + "lineNumber": 227 }, "deprecated": false, "children": [ @@ -29961,7 +30069,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 219 + "lineNumber": 227 }, "deprecated": false, "isRequired": true @@ -30009,7 +30117,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 235 + "lineNumber": 243 }, "deprecated": false, "children": [ @@ -30039,7 +30147,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 236 + "lineNumber": 244 }, "deprecated": false, "isRequired": true @@ -30063,7 +30171,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 237 + "lineNumber": 245 }, "deprecated": false, "isRequired": false @@ -30095,7 +30203,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 250 + "lineNumber": 258 }, "deprecated": false, "children": [ @@ -30117,7 +30225,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 250 + "lineNumber": 258 }, "deprecated": false, "isRequired": true @@ -30163,7 +30271,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 327 + "lineNumber": 335 }, "deprecated": false, "children": [ @@ -30173,7 +30281,9 @@ "type": "Array", "tags": [], "label": "fields", - "description": [], + "description": [ + ": FieldSpec[]" + ], "signature": [ { "pluginId": "data", @@ -30186,7 +30296,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 327 + "lineNumber": 335 }, "deprecated": false, "isRequired": true @@ -30197,7 +30307,9 @@ "type": "Object", "tags": [], "label": "fieldAttrs", - "description": [], + "description": [ + ": FieldAttrs" + ], "signature": [ { "pluginId": "data", @@ -30210,7 +30322,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 327 + "lineNumber": 335 }, "deprecated": false, "isRequired": false @@ -30251,7 +30363,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 343 + "lineNumber": 351 }, "deprecated": false, "children": [ @@ -30276,7 +30388,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 343 + "lineNumber": 351 }, "deprecated": false, "isRequired": true @@ -30308,7 +30420,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 465 + "lineNumber": 473 }, "deprecated": false, "children": [ @@ -30324,7 +30436,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 465 + "lineNumber": 473 }, "deprecated": false, "isRequired": true @@ -30362,7 +30474,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 484 + "lineNumber": 492 }, "deprecated": false, "children": [ @@ -30384,7 +30496,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 484 + "lineNumber": 492 }, "deprecated": false, "isRequired": true @@ -30401,7 +30513,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 484 + "lineNumber": 492 }, "deprecated": false, "isRequired": true @@ -30441,7 +30553,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "children": [ @@ -30463,7 +30575,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "isRequired": true @@ -30482,7 +30594,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "isRequired": true @@ -30501,7 +30613,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "isRequired": true @@ -30539,7 +30651,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 522 + "lineNumber": 530 }, "deprecated": false, "children": [ @@ -30561,7 +30673,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 522 + "lineNumber": 530 }, "deprecated": false, "isRequired": true @@ -30580,7 +30692,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 522 + "lineNumber": 530 }, "deprecated": false, "isRequired": true @@ -30610,7 +30722,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 550 + "lineNumber": 558 }, "deprecated": false, "children": [ @@ -30632,7 +30744,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 551 + "lineNumber": 559 }, "deprecated": false, "isRequired": true @@ -30649,7 +30761,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 552 + "lineNumber": 560 }, "deprecated": false, "isRequired": true @@ -30666,7 +30778,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 553 + "lineNumber": 561 }, "deprecated": false, "isRequired": true @@ -30688,7 +30800,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 636 + "lineNumber": 644 }, "deprecated": false, "children": [ @@ -30706,7 +30818,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 636 + "lineNumber": 644 }, "deprecated": false, "isRequired": true @@ -33297,29 +33409,29 @@ { "plugin": "maps", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", - "lineNumber": 10 + "path": "x-pack/plugins/maps/public/classes/fields/es_doc_field.ts", + "lineNumber": 12 } }, { "plugin": "maps", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", - "lineNumber": 360 + "path": "x-pack/plugins/maps/public/classes/fields/es_doc_field.ts", + "lineNumber": 45 } }, { "plugin": "maps", "link": { - "path": "x-pack/plugins/maps/public/classes/fields/es_doc_field.ts", - "lineNumber": 12 + "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", + "lineNumber": 10 } }, { "plugin": "maps", "link": { - "path": "x-pack/plugins/maps/public/classes/fields/es_doc_field.ts", - "lineNumber": 45 + "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", + "lineNumber": 367 } }, { @@ -36850,7 +36962,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 41 + "lineNumber": 42 }, "deprecated": false, "initialIsOpen": false @@ -37651,7 +37763,7 @@ "description": [], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 52 + "lineNumber": 53 }, "deprecated": false }, @@ -37667,7 +37779,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 52 + "lineNumber": 53 }, "deprecated": false } @@ -40886,7 +40998,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 52 + "lineNumber": 53 }, "deprecated": false, "children": [ @@ -40902,7 +41014,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 52 + "lineNumber": 53 }, "deprecated": false, "isRequired": true @@ -40919,7 +41031,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 52 + "lineNumber": 53 }, "deprecated": false, "isRequired": false @@ -42077,7 +42189,9 @@ "type": "Object", "tags": [], "label": "filter", - "description": [], + "description": [ + "The filter to clean" + ], "signature": [ { "pluginId": "data", @@ -42293,7 +42407,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 94 + "lineNumber": 95 }, "deprecated": false, "children": [ @@ -42315,7 +42429,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 94 + "lineNumber": 95 }, "deprecated": false, "isRequired": true @@ -42351,7 +42465,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 91 + "lineNumber": 92 }, "deprecated": false, "children": [ @@ -42373,7 +42487,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 91 + "lineNumber": 92 }, "deprecated": false, "isRequired": true @@ -43571,7 +43685,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 103 + "lineNumber": 104 }, "deprecated": false, "children": [ @@ -43587,7 +43701,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 103 + "lineNumber": 104 }, "deprecated": false, "isRequired": true @@ -43667,7 +43781,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 66 + "lineNumber": 67 }, "deprecated": false, "children": [ @@ -43689,7 +43803,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 66 + "lineNumber": 67 }, "deprecated": false, "isRequired": true @@ -43718,7 +43832,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 110 + "lineNumber": 111 }, "deprecated": false, "children": [ @@ -43734,7 +43848,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 110 + "lineNumber": 111 }, "deprecated": false, "isRequired": true @@ -44254,7 +44368,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 97 + "lineNumber": 98 }, "deprecated": false, "children": [ @@ -44276,7 +44390,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 97 + "lineNumber": 98 }, "deprecated": false, "isRequired": true @@ -44469,7 +44583,7 @@ "section": "def-common.Filter", "text": "Filter" }, - ") => { meta: { disabled: boolean; alias: string | null; negate: boolean; controlledBy?: string | undefined; index?: string | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; $state?: ", + ") => { meta: { disabled: boolean; alias: string | null; negate: boolean; controlledBy?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; $state?: ", { "pluginId": "data", "scope": "common", @@ -44481,7 +44595,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 70 + "lineNumber": 71 }, "deprecated": false, "children": [ @@ -44503,7 +44617,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 70 + "lineNumber": 71 }, "deprecated": false, "isRequired": true @@ -44528,7 +44642,7 @@ "section": "def-common.Filter", "text": "Filter" }, - ") => { meta: { negate: boolean; alias: string | null; disabled: boolean; controlledBy?: string | undefined; index?: string | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; $state?: ", + ") => { meta: { negate: boolean; alias: string | null; disabled: boolean; controlledBy?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; $state?: ", { "pluginId": "data", "scope": "common", @@ -44540,7 +44654,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 77 + "lineNumber": 78 }, "deprecated": false, "children": [ @@ -44562,7 +44676,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 77 + "lineNumber": 78 }, "deprecated": false, "isRequired": true @@ -44607,7 +44721,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 84 + "lineNumber": 85 }, "deprecated": false, "children": [ @@ -44629,7 +44743,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 84 + "lineNumber": 85 }, "deprecated": false, "isRequired": true @@ -44665,7 +44779,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 100 + "lineNumber": 101 }, "deprecated": false, "children": [ @@ -44687,7 +44801,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 100 + "lineNumber": 101 }, "deprecated": false, "isRequired": true @@ -45152,7 +45266,7 @@ "description": [], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 47 + "lineNumber": 48 }, "deprecated": false, "children": [ @@ -45165,7 +45279,7 @@ "description": [], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 48 + "lineNumber": 49 }, "deprecated": false }, @@ -45178,7 +45292,7 @@ "description": [], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 49 + "lineNumber": 50 }, "deprecated": false } @@ -45771,7 +45885,7 @@ "label": "ExistsFilterMeta", "description": [], "signature": [ - "{ alias: string | null; disabled: boolean; negate: boolean; controlledBy?: string | undefined; index?: string | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }" + "{ alias: string | null; disabled: boolean; negate: boolean; controlledBy?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }" ], "source": { "path": "src/plugins/data/common/es_query/filters/exists_filter.ts", @@ -45887,7 +46001,7 @@ ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", - "lineNumber": 41 + "lineNumber": 42 }, "deprecated": false, "initialIsOpen": false @@ -45900,7 +46014,7 @@ "label": "FilterMeta", "description": [], "signature": [ - "{ alias: string | null; disabled: boolean; negate: boolean; controlledBy?: string | undefined; index?: string | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }" + "{ alias: string | null; disabled: boolean; negate: boolean; controlledBy?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }" ], "source": { "path": "src/plugins/data/common/es_query/filters/meta_filter.ts", @@ -46178,7 +46292,7 @@ "label": "MissingFilterMeta", "description": [], "signature": [ - "{ alias: string | null; disabled: boolean; negate: boolean; controlledBy?: string | undefined; index?: string | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }" + "{ alias: string | null; disabled: boolean; negate: boolean; controlledBy?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }" ], "source": { "path": "src/plugins/data/common/es_query/filters/missing_filter.ts", @@ -46339,7 +46453,7 @@ "label": "QueryStringFilterMeta", "description": [], "signature": [ - "{ alias: string | null; disabled: boolean; negate: boolean; controlledBy?: string | undefined; index?: string | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }" + "{ alias: string | null; disabled: boolean; negate: boolean; controlledBy?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }" ], "source": { "path": "src/plugins/data/common/es_query/filters/query_string_filter.ts", diff --git a/api_docs/data_field_formats.json b/api_docs/data_field_formats.json index 6fe1b64bd555bb..1807c32a18389d 100644 --- a/api_docs/data_field_formats.json +++ b/api_docs/data_field_formats.json @@ -1687,7 +1687,9 @@ "type": "Enum", "tags": [], "label": "fieldType", - "description": [], + "description": [ + "- the field type" + ], "signature": [ { "pluginId": "data", @@ -1710,7 +1712,9 @@ "type": "Array", "tags": [], "label": "esTypes", - "description": [], + "description": [ + "- Array of ES data types" + ], "signature": [ { "pluginId": "data", @@ -1759,7 +1763,9 @@ "type": "string", "tags": [], "label": "formatId", - "description": [], + "description": [ + "- the format id" + ], "signature": [ "string" ], @@ -1878,7 +1884,9 @@ "type": "Array", "tags": [], "label": "esTypes", - "description": [], + "description": [ + "- Array of ES data types" + ], "signature": [ { "pluginId": "data", @@ -1941,7 +1949,9 @@ "type": "Array", "tags": [], "label": "esTypes", - "description": [], + "description": [ + "- Array of ES data types" + ], "signature": [ { "pluginId": "data", diff --git a/api_docs/data_index_patterns.json b/api_docs/data_index_patterns.json index 6d9230cfb6a877..676ded76cb331b 100644 --- a/api_docs/data_index_patterns.json +++ b/api_docs/data_index_patterns.json @@ -3291,7 +3291,9 @@ "type": "boolean", "tags": [], "label": "refresh", - "description": [], + "description": [ + "Force refresh of index pattern list" + ], "signature": [ "boolean" ], @@ -3329,7 +3331,9 @@ "type": "boolean", "tags": [], "label": "refresh", - "description": [], + "description": [ + "Force refresh of index pattern list" + ], "signature": [ "boolean" ], @@ -3432,7 +3436,9 @@ "type": "boolean", "tags": [], "label": "refresh", - "description": [], + "description": [ + "Force refresh of index pattern list" + ], "signature": [ "boolean" ], @@ -3470,7 +3476,9 @@ "type": "string", "tags": [], "label": "id", - "description": [], + "description": [ + "optionally clear a single id" + ], "signature": [ "string | undefined" ], @@ -3534,6 +3542,26 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "data", + "id": "def-common.IndexPatternsService.getDefaultId", + "type": "Function", + "tags": [], + "label": "getDefaultId", + "description": [ + "\nGet default index pattern id" + ], + "signature": [ + "() => Promise" + ], + "source": { + "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", + "lineNumber": 206 + }, + "deprecated": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "data", "id": "def-common.IndexPatternsService.setDefault", @@ -3544,30 +3572,30 @@ "\nOptionally set default index pattern, unless force = true" ], "signature": [ - "(id: string, force?: boolean) => Promise" + "(id: string | null, force?: boolean) => Promise" ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 208 + "lineNumber": 216 }, "deprecated": false, "children": [ { "parentPluginId": "data", "id": "def-common.IndexPatternsService.setDefault.$1", - "type": "string", + "type": "CompoundType", "tags": [], "label": "id", "description": [], "signature": [ - "string" + "string | null" ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 208 + "lineNumber": 216 }, "deprecated": false, - "isRequired": true + "isRequired": false }, { "parentPluginId": "data", @@ -3581,7 +3609,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 208 + "lineNumber": 216 }, "deprecated": false, "isRequired": true @@ -3611,7 +3639,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 219 + "lineNumber": 227 }, "deprecated": false, "children": [ @@ -3633,7 +3661,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 219 + "lineNumber": 227 }, "deprecated": false, "isRequired": true @@ -3681,7 +3709,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 235 + "lineNumber": 243 }, "deprecated": false, "children": [ @@ -3711,7 +3739,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 236 + "lineNumber": 244 }, "deprecated": false, "isRequired": true @@ -3735,7 +3763,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 237 + "lineNumber": 245 }, "deprecated": false, "isRequired": false @@ -3767,7 +3795,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 250 + "lineNumber": 258 }, "deprecated": false, "children": [ @@ -3789,7 +3817,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 250 + "lineNumber": 258 }, "deprecated": false, "isRequired": true @@ -3835,7 +3863,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 327 + "lineNumber": 335 }, "deprecated": false, "children": [ @@ -3845,7 +3873,9 @@ "type": "Array", "tags": [], "label": "fields", - "description": [], + "description": [ + ": FieldSpec[]" + ], "signature": [ { "pluginId": "data", @@ -3858,7 +3888,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 327 + "lineNumber": 335 }, "deprecated": false, "isRequired": true @@ -3869,7 +3899,9 @@ "type": "Object", "tags": [], "label": "fieldAttrs", - "description": [], + "description": [ + ": FieldAttrs" + ], "signature": [ { "pluginId": "data", @@ -3882,7 +3914,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 327 + "lineNumber": 335 }, "deprecated": false, "isRequired": false @@ -3923,7 +3955,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 343 + "lineNumber": 351 }, "deprecated": false, "children": [ @@ -3948,7 +3980,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 343 + "lineNumber": 351 }, "deprecated": false, "isRequired": true @@ -3980,7 +4012,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 465 + "lineNumber": 473 }, "deprecated": false, "children": [ @@ -3996,7 +4028,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 465 + "lineNumber": 473 }, "deprecated": false, "isRequired": true @@ -4034,7 +4066,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 484 + "lineNumber": 492 }, "deprecated": false, "children": [ @@ -4056,7 +4088,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 484 + "lineNumber": 492 }, "deprecated": false, "isRequired": true @@ -4073,7 +4105,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 484 + "lineNumber": 492 }, "deprecated": false, "isRequired": true @@ -4113,7 +4145,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "children": [ @@ -4135,7 +4167,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "isRequired": true @@ -4154,7 +4186,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "isRequired": true @@ -4173,7 +4205,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 509 + "lineNumber": 517 }, "deprecated": false, "isRequired": true @@ -4211,7 +4243,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 522 + "lineNumber": 530 }, "deprecated": false, "children": [ @@ -4233,7 +4265,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 522 + "lineNumber": 530 }, "deprecated": false, "isRequired": true @@ -4252,7 +4284,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 522 + "lineNumber": 530 }, "deprecated": false, "isRequired": true @@ -4282,7 +4314,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 550 + "lineNumber": 558 }, "deprecated": false, "children": [ @@ -4304,7 +4336,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 551 + "lineNumber": 559 }, "deprecated": false, "isRequired": true @@ -4321,7 +4353,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 552 + "lineNumber": 560 }, "deprecated": false, "isRequired": true @@ -4338,7 +4370,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 553 + "lineNumber": 561 }, "deprecated": false, "isRequired": true @@ -4360,7 +4392,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 636 + "lineNumber": 644 }, "deprecated": false, "children": [ @@ -4378,7 +4410,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 636 + "lineNumber": 644 }, "deprecated": false, "isRequired": true @@ -5516,29 +5548,29 @@ { "plugin": "maps", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", - "lineNumber": 10 + "path": "x-pack/plugins/maps/public/classes/fields/es_doc_field.ts", + "lineNumber": 12 } }, { "plugin": "maps", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", - "lineNumber": 360 + "path": "x-pack/plugins/maps/public/classes/fields/es_doc_field.ts", + "lineNumber": 45 } }, { "plugin": "maps", "link": { - "path": "x-pack/plugins/maps/public/classes/fields/es_doc_field.ts", - "lineNumber": 12 + "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", + "lineNumber": 10 } }, { "plugin": "maps", "link": { - "path": "x-pack/plugins/maps/public/classes/fields/es_doc_field.ts", - "lineNumber": 45 + "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", + "lineNumber": 367 } }, { @@ -8911,21 +8943,21 @@ "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts", - "lineNumber": 40 + "lineNumber": 44 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts", - "lineNumber": 55 + "lineNumber": 60 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts", - "lineNumber": 60 + "lineNumber": 65 } }, { @@ -9940,7 +9972,7 @@ "plugin": "maps", "link": { "path": "x-pack/plugins/maps/public/embeddable/types.ts", - "lineNumber": 44 + "lineNumber": 45 } }, { @@ -12499,7 +12531,7 @@ "section": "def-common.IndexPattern", "text": "IndexPattern" }, - " | null>; setDefault: (id: string, force?: boolean) => Promise; getFieldsForWildcard: (options: ", + " | null>; getDefaultId: () => Promise; setDefault: (id: string | null, force?: boolean) => Promise; getFieldsForWildcard: (options: ", { "pluginId": "data", "scope": "common", @@ -12625,7 +12657,7 @@ ], "source": { "path": "src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts", - "lineNumber": 642 + "lineNumber": 650 }, "deprecated": false, "initialIsOpen": false diff --git a/api_docs/data_query.json b/api_docs/data_query.json index 3e28da259b5e2f..a9c17111369510 100644 --- a/api_docs/data_query.json +++ b/api_docs/data_query.json @@ -1056,7 +1056,9 @@ "type": "Object", "tags": [], "label": "stateContainer", - "description": [], + "description": [ + "to use for syncing" + ], "signature": [ { "pluginId": "kibanaUtils", @@ -1547,7 +1549,9 @@ "type": "Object", "tags": [], "label": "kbnUrlStateStorage", - "description": [], + "description": [ + "to use for syncing" + ], "signature": [ { "pluginId": "kibanaUtils", @@ -2563,7 +2567,9 @@ "type": "CompoundType", "tags": [], "label": "first", - "description": [], + "description": [ + "The first filter or filter array to compare" + ], "signature": [ { "pluginId": "data", @@ -2595,7 +2601,9 @@ "type": "CompoundType", "tags": [], "label": "second", - "description": [], + "description": [ + "The second filter or filter array to compare" + ], "signature": [ { "pluginId": "data", @@ -2627,7 +2635,9 @@ "type": "Object", "tags": [], "label": "comparatorOptions", - "description": [], + "description": [ + "Parameters to use for comparison" + ], "signature": [ { "pluginId": "data", @@ -2706,7 +2716,9 @@ "type": "Array", "tags": [], "label": "existingFilters", - "description": [], + "description": [ + "- The filters to compare to" + ], "signature": [ { "pluginId": "data", @@ -2730,7 +2742,9 @@ "type": "Array", "tags": [], "label": "filters", - "description": [], + "description": [ + "- The filters being added" + ], "signature": [ { "pluginId": "data", @@ -2754,7 +2768,9 @@ "type": "Object", "tags": [], "label": "comparatorOptions", - "description": [], + "description": [ + "- Parameters to use for comparison" + ], "signature": [ { "pluginId": "data", @@ -3132,7 +3148,9 @@ "type": "Array", "tags": [], "label": "filters", - "description": [], + "description": [ + "The filters to remove duplicates from" + ], "signature": [ { "pluginId": "data", @@ -3156,7 +3174,9 @@ "type": "Any", "tags": [], "label": "comparatorOptions", - "description": [], + "description": [ + "- Parameters to use for comparison" + ], "signature": [ "any" ], diff --git a/api_docs/data_search.json b/api_docs/data_search.json index 082553e94dcf42..d72ae79e79bd87 100644 --- a/api_docs/data_search.json +++ b/api_docs/data_search.json @@ -10230,7 +10230,7 @@ "plugin": "maps", "link": { "path": "x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts", - "lineNumber": 266 + "lineNumber": 273 } }, { @@ -10506,7 +10506,7 @@ "section": "def-common.IndexPatternsService", "text": "IndexPatternsService" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"ensureDefaultIndexPattern\" | \"getIds\" | \"getTitles\" | \"getIdsWithTitle\" | \"clearCache\" | \"getCache\" | \"getDefault\" | \"setDefault\" | \"getFieldsForWildcard\" | \"getFieldsForIndexPattern\" | \"refreshFields\" | \"fieldArrayToMap\" | \"savedObjectToSpec\" | \"createAndSave\" | \"createSavedObject\" | \"updateSavedObject\">, dependencies: ", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"ensureDefaultIndexPattern\" | \"getIds\" | \"getTitles\" | \"getIdsWithTitle\" | \"clearCache\" | \"getCache\" | \"getDefault\" | \"getDefaultId\" | \"setDefault\" | \"getFieldsForWildcard\" | \"getFieldsForIndexPattern\" | \"refreshFields\" | \"fieldArrayToMap\" | \"savedObjectToSpec\" | \"createAndSave\" | \"createSavedObject\" | \"updateSavedObject\">, dependencies: ", { "pluginId": "data", "scope": "common", @@ -10562,7 +10562,7 @@ "section": "def-common.IndexPatternsService", "text": "IndexPatternsService" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"ensureDefaultIndexPattern\" | \"getIds\" | \"getTitles\" | \"getIdsWithTitle\" | \"clearCache\" | \"getCache\" | \"getDefault\" | \"setDefault\" | \"getFieldsForWildcard\" | \"getFieldsForIndexPattern\" | \"refreshFields\" | \"fieldArrayToMap\" | \"savedObjectToSpec\" | \"createAndSave\" | \"createSavedObject\" | \"updateSavedObject\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"ensureDefaultIndexPattern\" | \"getIds\" | \"getTitles\" | \"getIdsWithTitle\" | \"clearCache\" | \"getCache\" | \"getDefault\" | \"getDefaultId\" | \"setDefault\" | \"getFieldsForWildcard\" | \"getFieldsForIndexPattern\" | \"refreshFields\" | \"fieldArrayToMap\" | \"savedObjectToSpec\" | \"createAndSave\" | \"createSavedObject\" | \"updateSavedObject\">" ], "source": { "path": "src/plugins/data/common/search/search_source/search_source_service.ts", @@ -11620,7 +11620,7 @@ "section": "def-common.IndexPatternsService", "text": "IndexPatternsService" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"ensureDefaultIndexPattern\" | \"getIds\" | \"getTitles\" | \"getIdsWithTitle\" | \"clearCache\" | \"getCache\" | \"getDefault\" | \"setDefault\" | \"getFieldsForWildcard\" | \"getFieldsForIndexPattern\" | \"refreshFields\" | \"fieldArrayToMap\" | \"savedObjectToSpec\" | \"createAndSave\" | \"createSavedObject\" | \"updateSavedObject\">, searchSourceDependencies: ", + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"ensureDefaultIndexPattern\" | \"getIds\" | \"getTitles\" | \"getIdsWithTitle\" | \"clearCache\" | \"getCache\" | \"getDefault\" | \"getDefaultId\" | \"setDefault\" | \"getFieldsForWildcard\" | \"getFieldsForIndexPattern\" | \"refreshFields\" | \"fieldArrayToMap\" | \"savedObjectToSpec\" | \"createAndSave\" | \"createSavedObject\" | \"updateSavedObject\">, searchSourceDependencies: ", { "pluginId": "data", "scope": "common", @@ -11658,7 +11658,9 @@ "type": "Object", "tags": [], "label": "indexPatterns", - "description": [], + "description": [ + "The index patterns contract of the data plugin" + ], "signature": [ "Pick<", { @@ -11668,7 +11670,7 @@ "section": "def-common.IndexPatternsService", "text": "IndexPatternsService" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"ensureDefaultIndexPattern\" | \"getIds\" | \"getTitles\" | \"getIdsWithTitle\" | \"clearCache\" | \"getCache\" | \"getDefault\" | \"setDefault\" | \"getFieldsForWildcard\" | \"getFieldsForIndexPattern\" | \"refreshFields\" | \"fieldArrayToMap\" | \"savedObjectToSpec\" | \"createAndSave\" | \"createSavedObject\" | \"updateSavedObject\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"ensureDefaultIndexPattern\" | \"getIds\" | \"getTitles\" | \"getIdsWithTitle\" | \"clearCache\" | \"getCache\" | \"getDefault\" | \"getDefaultId\" | \"setDefault\" | \"getFieldsForWildcard\" | \"getFieldsForIndexPattern\" | \"refreshFields\" | \"fieldArrayToMap\" | \"savedObjectToSpec\" | \"createAndSave\" | \"createSavedObject\" | \"updateSavedObject\">" ], "source": { "path": "src/plugins/data/common/search/search_source/create_search_source.ts", diff --git a/api_docs/deprecations.mdx b/api_docs/deprecations.mdx index 9eee6d51d84ab9..99edad62075bce 100644 --- a/api_docs/deprecations.mdx +++ b/api_docs/deprecations.mdx @@ -946,11 +946,11 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location | Remove By | | ---------------|-----------|-----------| | | [types.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L8) | - | -| | [types.ts#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L44) | - | -| | [es_source.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L10) | - | -| | [es_source.ts#L360](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L360) | - | +| | [types.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L45) | - | | | [es_doc_field.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L12) | - | | | [es_doc_field.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L45) | - | +| | [es_source.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L10) | - | +| | [es_source.ts#L367](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L367) | - | | | [index_pattern_util.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/index_pattern_util.ts#L8) | - | | | [index_pattern_util.ts#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/index_pattern_util.ts#L15) | - | | | [index_pattern_util.ts#L49](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/index_pattern_util.ts#L49) | - | @@ -1041,10 +1041,10 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [create_source_editor.tsx#L35](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx#L35) | - | | | [get_docvalue_source_fields.test.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts#L10) | - | | | [get_docvalue_source_fields.test.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts#L12) | - | -| | [es_source.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L10) | - | -| | [es_source.ts#L360](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L360) | - | | | [es_doc_field.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L12) | - | | | [es_doc_field.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L45) | - | +| | [es_source.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L10) | - | +| | [es_source.ts#L367](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L367) | - | | | [index_pattern_util.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/index_pattern_util.ts#L8) | - | | | [index_pattern_util.ts#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/index_pattern_util.ts#L15) | - | | | [index_pattern_util.ts#L49](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/index_pattern_util.ts#L49) | - | @@ -1136,11 +1136,11 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [get_docvalue_source_fields.test.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts#L10) | - | | | [get_docvalue_source_fields.test.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts#L12) | - | | | [types.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L8) | - | -| | [types.ts#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L44) | - | -| | [es_source.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L10) | - | -| | [es_source.ts#L360](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L360) | - | +| | [types.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L45) | - | | | [es_doc_field.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L12) | - | | | [es_doc_field.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L45) | - | +| | [es_source.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L10) | - | +| | [es_source.ts#L367](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L367) | - | | | [index_pattern_util.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/index_pattern_util.ts#L8) | - | | | [index_pattern_util.ts#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/index_pattern_util.ts#L15) | - | | | [index_pattern_util.ts#L49](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/index_pattern_util.ts#L49) | - | @@ -1612,9 +1612,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L41](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/details/types.ts#L41) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L12) | - | | | [index.tsx#L34](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L34) | - | -| | [middleware.ts#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L40) | - | -| | [middleware.ts#L55](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L55) | - | +| | [middleware.ts#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L44) | - | | | [middleware.ts#L60](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L60) | - | +| | [middleware.ts#L65](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L65) | - | | | [types.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L12) | - | | | [types.ts#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L28) | - | | | [index.tsx#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx#L15) | - | @@ -1824,9 +1824,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L41](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/details/types.ts#L41) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L12) | - | | | [index.tsx#L34](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L34) | - | -| | [middleware.ts#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L40) | - | -| | [middleware.ts#L55](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L55) | - | +| | [middleware.ts#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L44) | - | | | [middleware.ts#L60](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L60) | - | +| | [middleware.ts#L65](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L65) | - | | | [types.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L12) | - | | | [types.ts#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L28) | - | | | [index.tsx#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx#L15) | - | diff --git a/api_docs/embeddable.json b/api_docs/embeddable.json index c9fee3570688c7..d560148f35b136 100644 --- a/api_docs/embeddable.json +++ b/api_docs/embeddable.json @@ -3847,7 +3847,9 @@ "type": "string", "tags": [], "label": "appId", - "description": [], + "description": [ + "- The id of the app to fetch the title for" + ], "signature": [ "string" ], @@ -4716,7 +4718,9 @@ "type": "CompoundType", "tags": [], "label": "props", - "description": [], + "description": [ + "- {@link EmbeddableRendererProps}" + ], "signature": [ { "pluginId": "embeddable", diff --git a/api_docs/expressions.json b/api_docs/expressions.json index 6d77161ae85a0f..e981414fe7bda9 100644 --- a/api_docs/expressions.json +++ b/api_docs/expressions.json @@ -4875,7 +4875,9 @@ "type": "Object", "tags": [], "label": "state", - "description": [], + "description": [ + "expression AST to extract references from" + ], "signature": [ { "pluginId": "expressions", @@ -4954,7 +4956,9 @@ "type": "Object", "tags": [], "label": "state", - "description": [], + "description": [ + "expression AST to extract references from" + ], "signature": [ { "pluginId": "expressions", @@ -5017,7 +5021,9 @@ "type": "Object", "tags": [], "label": "state", - "description": [], + "description": [ + "expression AST to update" + ], "signature": [ { "pluginId": "expressions", @@ -5040,7 +5046,9 @@ "type": "Array", "tags": [], "label": "references", - "description": [], + "description": [ + "array of saved object references" + ], "signature": [ "SavedObjectReference", "[]" @@ -5096,7 +5104,9 @@ "type": "Object", "tags": [], "label": "state", - "description": [], + "description": [ + "expression AST to update" + ], "signature": [ { "pluginId": "kibanaUtils", @@ -5119,7 +5129,9 @@ "type": "string", "tags": [], "label": "version", - "description": [], + "description": [ + "defines which migration version to run" + ], "signature": [ "string" ], @@ -25765,7 +25777,9 @@ "type": "Object", "tags": [], "label": "state", - "description": [], + "description": [ + "expression AST to extract references from" + ], "signature": [ { "pluginId": "expressions", @@ -25844,7 +25858,9 @@ "type": "Object", "tags": [], "label": "state", - "description": [], + "description": [ + "expression AST to extract references from" + ], "signature": [ { "pluginId": "expressions", @@ -25907,7 +25923,9 @@ "type": "Object", "tags": [], "label": "state", - "description": [], + "description": [ + "expression AST to update" + ], "signature": [ { "pluginId": "expressions", @@ -25930,7 +25948,9 @@ "type": "Array", "tags": [], "label": "references", - "description": [], + "description": [ + "array of saved object references" + ], "signature": [ "SavedObjectReference", "[]" @@ -25986,7 +26006,9 @@ "type": "Object", "tags": [], "label": "state", - "description": [], + "description": [ + "expression AST to update" + ], "signature": [ { "pluginId": "kibanaUtils", @@ -26009,7 +26031,9 @@ "type": "string", "tags": [], "label": "version", - "description": [], + "description": [ + "defines which migration version to run" + ], "signature": [ "string" ], diff --git a/api_docs/fleet.json b/api_docs/fleet.json index 389a56cccefc5b..043dfcd16c2a79 100644 --- a/api_docs/fleet.json +++ b/api_docs/fleet.json @@ -373,6 +373,30 @@ "lineNumber": 60 }, "deprecated": false + }, + { + "parentPluginId": "fleet", + "id": "def-public.NewPackagePolicy.vars", + "type": "Object", + "tags": [], + "label": "vars", + "description": [], + "signature": [ + "Record | undefined" + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", + "lineNumber": 61 + }, + "deprecated": false } ], "initialIsOpen": false @@ -1435,7 +1459,7 @@ "label": "integration_details_overview", "description": [], "signature": [ - "({ pkgkey }: ", + "({ pkgkey, integration }: ", "DynamicPagePathValues", ") => string" ], @@ -1450,7 +1474,7 @@ "id": "def-public.pagePathGetters.integration_details_overview.$1", "type": "Object", "tags": [], - "label": "{ pkgkey }", + "label": "{ pkgkey, integration }", "description": [], "signature": [ "DynamicPagePathValues" @@ -1473,13 +1497,13 @@ "label": "integration_details_policies", "description": [], "signature": [ - "({ pkgkey }: ", + "({ pkgkey, integration }: ", "DynamicPagePathValues", ") => string" ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 81 + "lineNumber": 82 }, "deprecated": false, "children": [ @@ -1488,14 +1512,14 @@ "id": "def-public.pagePathGetters.integration_details_policies.$1", "type": "Object", "tags": [], - "label": "{ pkgkey }", + "label": "{ pkgkey, integration }", "description": [], "signature": [ "DynamicPagePathValues" ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 81 + "lineNumber": 82 }, "deprecated": false, "isRequired": true @@ -1511,13 +1535,13 @@ "label": "integration_details_settings", "description": [], "signature": [ - "({ pkgkey }: ", + "({ pkgkey, integration }: ", "DynamicPagePathValues", ") => string" ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 82 + "lineNumber": 84 }, "deprecated": false, "children": [ @@ -1526,14 +1550,14 @@ "id": "def-public.pagePathGetters.integration_details_settings.$1", "type": "Object", "tags": [], - "label": "{ pkgkey }", + "label": "{ pkgkey, integration }", "description": [], "signature": [ "DynamicPagePathValues" ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 82 + "lineNumber": 84 }, "deprecated": false, "isRequired": true @@ -1549,13 +1573,13 @@ "label": "integration_details_custom", "description": [], "signature": [ - "({ pkgkey }: ", + "({ pkgkey, integration }: ", "DynamicPagePathValues", ") => string" ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 83 + "lineNumber": 86 }, "deprecated": false, "children": [ @@ -1564,14 +1588,14 @@ "id": "def-public.pagePathGetters.integration_details_custom.$1", "type": "Object", "tags": [], - "label": "{ pkgkey }", + "label": "{ pkgkey, integration }", "description": [], "signature": [ "DynamicPagePathValues" ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 83 + "lineNumber": 86 }, "deprecated": false, "isRequired": true @@ -1593,7 +1617,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 84 + "lineNumber": 88 }, "deprecated": false, "children": [ @@ -1609,7 +1633,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 84 + "lineNumber": 88 }, "deprecated": false, "isRequired": true @@ -1629,7 +1653,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 86 + "lineNumber": 90 }, "deprecated": false, "children": [], @@ -1647,7 +1671,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 87 + "lineNumber": 91 }, "deprecated": false, "children": [], @@ -1667,7 +1691,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 88 + "lineNumber": 92 }, "deprecated": false, "children": [ @@ -1683,7 +1707,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 88 + "lineNumber": 92 }, "deprecated": false, "isRequired": true @@ -1705,7 +1729,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 89 + "lineNumber": 93 }, "deprecated": false, "children": [ @@ -1721,7 +1745,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 89 + "lineNumber": 93 }, "deprecated": false, "isRequired": true @@ -1737,13 +1761,13 @@ "label": "add_integration_to_policy", "description": [], "signature": [ - "({ pkgkey }: ", + "({ pkgkey, integration }: ", "DynamicPagePathValues", ") => string" ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 90 + "lineNumber": 94 }, "deprecated": false, "children": [ @@ -1752,14 +1776,14 @@ "id": "def-public.pagePathGetters.add_integration_to_policy.$1", "type": "Object", "tags": [], - "label": "{ pkgkey }", + "label": "{ pkgkey, integration }", "description": [], "signature": [ "DynamicPagePathValues" ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 90 + "lineNumber": 94 }, "deprecated": false, "isRequired": true @@ -1781,7 +1805,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 91 + "lineNumber": 96 }, "deprecated": false, "children": [ @@ -1797,7 +1821,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 91 + "lineNumber": 96 }, "deprecated": false, "isRequired": true @@ -1817,7 +1841,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 93 + "lineNumber": 98 }, "deprecated": false, "children": [], @@ -1837,7 +1861,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 94 + "lineNumber": 99 }, "deprecated": false, "children": [ @@ -1853,7 +1877,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 94 + "lineNumber": 99 }, "deprecated": false, "isRequired": true @@ -1875,7 +1899,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 95 + "lineNumber": 100 }, "deprecated": false, "children": [ @@ -1891,7 +1915,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 95 + "lineNumber": 100 }, "deprecated": false, "isRequired": true @@ -1911,7 +1935,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 97 + "lineNumber": 102 }, "deprecated": false, "children": [], @@ -1929,7 +1953,7 @@ ], "source": { "path": "x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts", - "lineNumber": 98 + "lineNumber": 103 }, "deprecated": false, "children": [], @@ -4777,7 +4801,7 @@ ], "source": { "path": "x-pack/plugins/fleet/server/services/package_policy.ts", - "lineNumber": 651 + "lineNumber": 655 }, "deprecated": false, "initialIsOpen": false @@ -5376,105 +5400,385 @@ }, { "parentPluginId": "fleet", - "id": "def-common.entries", + "id": "def-common.doesPackageHaveIntegrations", "type": "Function", "tags": [], - "label": "entries", + "label": "doesPackageHaveIntegrations", "description": [], "signature": [ - "(o: T) => [keyof T, T[keyof T]][]" - ], - "source": { - "path": "x-pack/plugins/fleet/common/types/index.ts", - "lineNumber": 35 - }, - "deprecated": false, - "returnComment": [], - "children": [ + "(pkgInfo: ", { - "parentPluginId": "fleet", - "id": "def-common.o", - "type": "Uncategorized", - "tags": [], - "label": "o", - "description": [], - "signature": [ - "T" - ], - "source": { - "path": "x-pack/plugins/fleet/common/types/index.ts", - "lineNumber": 35 - }, - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "fleet", - "id": "def-common.fullAgentPolicyToYaml", - "type": "Function", - "tags": [], - "label": "fullAgentPolicyToYaml", - "description": [], - "signature": [ - "(policy: ", + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installed", + "text": "Installed" + }, + " string" + ", ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.EpmPackageAdditions", + "text": "EpmPackageAdditions" + }, + ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.NotInstalled", + "text": "NotInstalled" + }, + "> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installed", + "text": "Installed" + }, + "> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.NotInstalled", + "text": "NotInstalled" + }, + "> | (Pick<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryPackage", + "text": "RegistryPackage" + }, + ", \"type\" | \"description\" | \"title\" | \"name\" | \"version\" | \"download\" | \"path\" | \"internal\" | \"data_streams\" | \"release\" | \"icons\" | \"policy_templates\"> & { status: \"installed\"; savedObject: ", + "SavedObject", + "<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installation", + "text": "Installation" + }, + ">; } & { integration?: string | undefined; id: string; }) | (Pick<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryPackage", + "text": "RegistryPackage" + }, + ", \"type\" | \"description\" | \"title\" | \"name\" | \"version\" | \"download\" | \"path\" | \"internal\" | \"data_streams\" | \"release\" | \"icons\" | \"policy_templates\"> & { status: \"not_installed\"; } & { integration?: string | undefined; id: string; })) => boolean" ], "source": { - "path": "x-pack/plugins/fleet/common/services/full_agent_policy_to_yaml.ts", - "lineNumber": 28 + "path": "x-pack/plugins/fleet/common/services/packages_with_integrations.ts", + "lineNumber": 9 }, "deprecated": false, "children": [ { "parentPluginId": "fleet", - "id": "def-common.fullAgentPolicyToYaml.$1", - "type": "Object", + "id": "def-common.doesPackageHaveIntegrations.$1", + "type": "CompoundType", "tags": [], - "label": "policy", + "label": "pkgInfo", "description": [], "signature": [ { "pluginId": "fleet", "scope": "common", "docId": "kibFleetPluginApi", - "section": "def-common.FullAgentPolicy", - "text": "FullAgentPolicy" - } - ], - "source": { - "path": "x-pack/plugins/fleet/common/services/full_agent_policy_to_yaml.ts", - "lineNumber": 28 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "fleet", - "id": "def-common.isAgentUpgradeable", - "type": "Function", - "tags": [], - "label": "isAgentUpgradeable", - "description": [], - "signature": [ - "(agent: ", - { - "pluginId": "fleet", - "scope": "common", - "docId": "kibFleetPluginApi", - "section": "def-common.Agent", + "section": "def-common.Installed", + "text": "Installed" + }, + "> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.NotInstalled", + "text": "NotInstalled" + }, + "> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installed", + "text": "Installed" + }, + "> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.NotInstalled", + "text": "NotInstalled" + }, + "> | (Pick<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryPackage", + "text": "RegistryPackage" + }, + ", \"type\" | \"description\" | \"title\" | \"name\" | \"version\" | \"download\" | \"path\" | \"internal\" | \"data_streams\" | \"release\" | \"icons\" | \"policy_templates\"> & { status: \"installed\"; savedObject: ", + "SavedObject", + "<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installation", + "text": "Installation" + }, + ">; } & { integration?: string | undefined; id: string; }) | (Pick<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryPackage", + "text": "RegistryPackage" + }, + ", \"type\" | \"description\" | \"title\" | \"name\" | \"version\" | \"download\" | \"path\" | \"internal\" | \"data_streams\" | \"release\" | \"icons\" | \"policy_templates\"> & { status: \"not_installed\"; } & { integration?: string | undefined; id: string; })" + ], + "source": { + "path": "x-pack/plugins/fleet/common/services/packages_with_integrations.ts", + "lineNumber": 9 + }, + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "fleet", + "id": "def-common.entries", + "type": "Function", + "tags": [], + "label": "entries", + "description": [], + "signature": [ + "(o: T) => [keyof T, T[keyof T]][]" + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/index.ts", + "lineNumber": 35 + }, + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "fleet", + "id": "def-common.o", + "type": "Uncategorized", + "tags": [], + "label": "o", + "description": [], + "signature": [ + "T" + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/index.ts", + "lineNumber": 35 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "fleet", + "id": "def-common.fullAgentPolicyToYaml", + "type": "Function", + "tags": [], + "label": "fullAgentPolicyToYaml", + "description": [], + "signature": [ + "(policy: ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.FullAgentPolicy", + "text": "FullAgentPolicy" + }, + ") => string" + ], + "source": { + "path": "x-pack/plugins/fleet/common/services/full_agent_policy_to_yaml.ts", + "lineNumber": 28 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "fleet", + "id": "def-common.fullAgentPolicyToYaml.$1", + "type": "Object", + "tags": [], + "label": "policy", + "description": [], + "signature": [ + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.FullAgentPolicy", + "text": "FullAgentPolicy" + } + ], + "source": { + "path": "x-pack/plugins/fleet/common/services/full_agent_policy_to_yaml.ts", + "lineNumber": 28 + }, + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "fleet", + "id": "def-common.isAgentUpgradeable", + "type": "Function", + "tags": [], + "label": "isAgentUpgradeable", + "description": [], + "signature": [ + "(agent: ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Agent", "text": "Agent" }, ", kibanaVersion: string) => boolean" @@ -5827,7 +6131,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/services/package_to_package_policy.ts", - "lineNumber": 46 + "lineNumber": 61 }, "deprecated": false, "children": [ @@ -5849,7 +6153,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/services/package_to_package_policy.ts", - "lineNumber": 47 + "lineNumber": 62 }, "deprecated": false, "isRequired": true @@ -6505,7 +6809,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 221 + "lineNumber": 236 }, "deprecated": false, "children": [ @@ -6518,7 +6822,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 222 + "lineNumber": 237 }, "deprecated": false }, @@ -6534,7 +6838,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 223 + "lineNumber": 238 }, "deprecated": false }, @@ -6550,7 +6854,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 224 + "lineNumber": 239 }, "deprecated": false }, @@ -6581,7 +6885,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 225 + "lineNumber": 240 }, "deprecated": false }, @@ -6594,7 +6898,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 226 + "lineNumber": 241 }, "deprecated": false } @@ -6818,7 +7122,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 214 + "lineNumber": 229 }, "deprecated": false, "children": [ @@ -6831,7 +7135,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 215 + "lineNumber": 230 }, "deprecated": false }, @@ -6844,7 +7148,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 216 + "lineNumber": 231 }, "deprecated": false }, @@ -6857,7 +7161,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 217 + "lineNumber": 232 }, "deprecated": false } @@ -7305,7 +7609,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 33 + "lineNumber": 38 }, "deprecated": false, "children": [ @@ -7321,7 +7625,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 34 + "lineNumber": 39 }, "deprecated": false }, @@ -7337,7 +7641,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 35 + "lineNumber": 40 }, "deprecated": false } @@ -7736,7 +8040,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 320 + "lineNumber": 335 }, "deprecated": false, "children": [ @@ -7749,7 +8053,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 321 + "lineNumber": 336 }, "deprecated": false }, @@ -7762,7 +8066,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 322 + "lineNumber": 337 }, "deprecated": false }, @@ -7810,7 +8114,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 323 + "lineNumber": 338 }, "deprecated": false }, @@ -7826,7 +8130,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 324 + "lineNumber": 339 }, "deprecated": false } @@ -9765,7 +10069,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 18 + "lineNumber": 17 }, "deprecated": false, "children": [ @@ -9777,11 +10081,11 @@ "label": "query", "description": [], "signature": [ - "{ experimental?: boolean | undefined; }" + "{ experimental?: boolean | undefined; include_policy_templates?: boolean | undefined; }" ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 19 + "lineNumber": 18 }, "deprecated": false } @@ -10910,18 +11214,10 @@ "pluginId": "fleet", "scope": "common", "docId": "kibFleetPluginApi", - "section": "def-common.Installable", - "text": "Installable" - }, - ">[]" + "[]" ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", @@ -11107,7 +11403,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 391 + "lineNumber": 409 }, "deprecated": false, "children": [ @@ -11120,7 +11416,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 392 + "lineNumber": 410 }, "deprecated": false }, @@ -11136,7 +11432,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 393 + "lineNumber": 411 }, "deprecated": false }, @@ -11152,7 +11448,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 394 + "lineNumber": 412 }, "deprecated": false }, @@ -11168,7 +11464,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 398 + "lineNumber": 416 }, "deprecated": false }, @@ -11184,7 +11480,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 399 + "lineNumber": 417 }, "deprecated": false }, @@ -11200,7 +11496,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 400 + "lineNumber": 418 }, "deprecated": false } @@ -11216,7 +11512,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 384 + "lineNumber": 402 }, "deprecated": false, "children": [ @@ -11232,7 +11528,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 385 + "lineNumber": 403 }, "deprecated": false } @@ -11259,7 +11555,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 339 + "lineNumber": 357 }, "deprecated": false, "children": [ @@ -11282,7 +11578,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 340 + "lineNumber": 358 }, "deprecated": false }, @@ -11305,7 +11601,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 341 + "lineNumber": 359 }, "deprecated": false }, @@ -11328,7 +11624,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 342 + "lineNumber": 360 }, "deprecated": false }, @@ -11344,7 +11640,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 343 + "lineNumber": 361 }, "deprecated": false }, @@ -11357,7 +11653,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 344 + "lineNumber": 362 }, "deprecated": false }, @@ -11370,7 +11666,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 345 + "lineNumber": 363 }, "deprecated": false }, @@ -11386,7 +11682,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 346 + "lineNumber": 364 }, "deprecated": false }, @@ -11399,7 +11695,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 347 + "lineNumber": 365 }, "deprecated": false }, @@ -11412,7 +11708,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 348 + "lineNumber": 366 }, "deprecated": false }, @@ -11428,7 +11724,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 349 + "lineNumber": 367 }, "deprecated": false } @@ -12327,7 +12623,31 @@ "lineNumber": 60 }, "deprecated": false - } + }, + { + "parentPluginId": "fleet", + "id": "def-common.NewPackagePolicy.vars", + "type": "Object", + "tags": [], + "label": "vars", + "description": [], + "signature": [ + "Record | undefined" + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", + "lineNumber": 61 + }, + "deprecated": false + } ], "initialIsOpen": false }, @@ -12592,11 +12912,11 @@ "section": "def-common.NewPackagePolicy", "text": "NewPackagePolicy" }, - ", \"enabled\" | \"description\" | \"name\" | \"package\" | \"namespace\" | \"policy_id\" | \"output_id\">" + ", \"enabled\" | \"description\" | \"name\" | \"package\" | \"namespace\" | \"policy_id\" | \"output_id\" | \"vars\">" ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", - "lineNumber": 67 + "lineNumber": 68 }, "deprecated": false, "children": [ @@ -12609,7 +12929,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", - "lineNumber": 68 + "lineNumber": 69 }, "deprecated": false }, @@ -12632,7 +12952,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", - "lineNumber": 69 + "lineNumber": 70 }, "deprecated": false }, @@ -12648,7 +12968,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", - "lineNumber": 70 + "lineNumber": 71 }, "deprecated": false }, @@ -12661,7 +12981,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", - "lineNumber": 71 + "lineNumber": 72 }, "deprecated": false }, @@ -12674,7 +12994,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", - "lineNumber": 72 + "lineNumber": 73 }, "deprecated": false }, @@ -12687,7 +13007,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", - "lineNumber": 73 + "lineNumber": 74 }, "deprecated": false }, @@ -12700,7 +13020,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", - "lineNumber": 74 + "lineNumber": 75 }, "deprecated": false }, @@ -12713,7 +13033,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", - "lineNumber": 75 + "lineNumber": 76 }, "deprecated": false } @@ -12807,7 +13127,7 @@ "section": "def-common.NewPackagePolicyInput", "text": "NewPackagePolicyInput" }, - ", \"type\" | \"enabled\" | \"config\" | \"keep_enabled\" | \"vars\">" + ", \"type\" | \"enabled\" | \"config\" | \"vars\" | \"keep_enabled\">" ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", @@ -12983,7 +13303,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 60 + "lineNumber": 61 }, "deprecated": false, "children": [ @@ -12996,7 +13316,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 61 + "lineNumber": 62 }, "deprecated": false }, @@ -13012,7 +13332,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 62 + "lineNumber": 63 }, "deprecated": false }, @@ -13028,7 +13348,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 63 + "lineNumber": 64 }, "deprecated": false }, @@ -13044,7 +13364,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 64 + "lineNumber": 65 }, "deprecated": false } @@ -13278,6 +13598,29 @@ }, "deprecated": false }, + { + "parentPluginId": "fleet", + "id": "def-common.PackageSpecManifest.vars", + "type": "Array", + "tags": [], + "label": "vars", + "description": [], + "signature": [ + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryVarsEntry", + "text": "RegistryVarsEntry" + }, + "[] | undefined" + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", + "lineNumber": 25 + }, + "deprecated": false + }, { "parentPluginId": "fleet", "id": "def-common.PackageSpecManifest.owner", @@ -13290,7 +13633,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 25 + "lineNumber": 26 }, "deprecated": false } @@ -13306,7 +13649,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 67 + "lineNumber": 68 }, "deprecated": false, "children": [ @@ -13319,7 +13662,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 68 + "lineNumber": 69 }, "deprecated": false }, @@ -13332,7 +13675,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 69 + "lineNumber": 70 }, "deprecated": false }, @@ -13348,7 +13691,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 70 + "lineNumber": 71 }, "deprecated": false }, @@ -13364,7 +13707,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 71 + "lineNumber": 72 }, "deprecated": false } @@ -13380,7 +13723,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 352 + "lineNumber": 370 }, "deprecated": false, "children": [ @@ -13393,7 +13736,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 353 + "lineNumber": 371 }, "deprecated": false } @@ -13987,7 +14330,7 @@ "section": "def-common.NewPackagePolicy", "text": "NewPackagePolicy" }, - ", \"enabled\" | \"description\" | \"name\" | \"namespace\" | \"policy_id\" | \"output_id\">> & { name: string; package: Partial<", + ", \"enabled\" | \"description\" | \"name\" | \"namespace\" | \"policy_id\" | \"output_id\" | \"vars\">> & { name: string; package: Partial<", { "pluginId": "fleet", "scope": "common", @@ -14243,7 +14586,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 266 + "lineNumber": 281 }, "deprecated": false, "children": [ @@ -14256,7 +14599,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 267 + "lineNumber": 282 }, "deprecated": false }, @@ -14272,7 +14615,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 268 + "lineNumber": 283 }, "deprecated": false }, @@ -14288,7 +14631,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 269 + "lineNumber": 284 }, "deprecated": false }, @@ -14301,7 +14644,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 270 + "lineNumber": 285 }, "deprecated": false }, @@ -14314,7 +14657,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 271 + "lineNumber": 286 }, "deprecated": false }, @@ -14327,7 +14670,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 272 + "lineNumber": 287 }, "deprecated": false }, @@ -14350,7 +14693,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 273 + "lineNumber": 288 }, "deprecated": false }, @@ -14363,7 +14706,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 274 + "lineNumber": 289 }, "deprecated": false }, @@ -14376,7 +14719,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 275 + "lineNumber": 290 }, "deprecated": false }, @@ -14387,9 +14730,12 @@ "tags": [], "label": "[RegistryDataStreamKeys.ingest_pipeline]", "description": [], + "signature": [ + "string | undefined" + ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 276 + "lineNumber": 291 }, "deprecated": false }, @@ -14412,7 +14758,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 277 + "lineNumber": 292 }, "deprecated": false }, @@ -14428,7 +14774,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 278 + "lineNumber": 293 }, "deprecated": false } @@ -14444,7 +14790,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 281 + "lineNumber": 296 }, "deprecated": false, "children": [ @@ -14460,7 +14806,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 282 + "lineNumber": 297 }, "deprecated": false }, @@ -14476,7 +14822,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 283 + "lineNumber": 298 }, "deprecated": false } @@ -14490,25 +14836,29 @@ "tags": [], "label": "RegistryImage", "description": [], + "signature": [ + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryImage", + "text": "RegistryImage" + }, + " extends ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.PackageSpecIcon", + "text": "PackageSpecIcon" + } + ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 121 + "lineNumber": 126 }, "deprecated": false, "children": [ - { - "parentPluginId": "fleet", - "id": "def-common.RegistryImage.src", - "type": "string", - "tags": [], - "label": "src", - "description": [], - "source": { - "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 122 - }, - "deprecated": false - }, { "parentPluginId": "fleet", "id": "def-common.RegistryImage.path", @@ -14518,55 +14868,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 123 - }, - "deprecated": false - }, - { - "parentPluginId": "fleet", - "id": "def-common.RegistryImage.title", - "type": "string", - "tags": [], - "label": "title", - "description": [], - "signature": [ - "string | undefined" - ], - "source": { - "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 124 - }, - "deprecated": false - }, - { - "parentPluginId": "fleet", - "id": "def-common.RegistryImage.size", - "type": "string", - "tags": [], - "label": "size", - "description": [], - "signature": [ - "string | undefined" - ], - "source": { - "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 125 - }, - "deprecated": false - }, - { - "parentPluginId": "fleet", - "id": "def-common.RegistryImage.type", - "type": "string", - "tags": [], - "label": "type", - "description": [], - "signature": [ - "string | undefined" - ], - "source": { - "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 126 + "lineNumber": 127 }, "deprecated": false } @@ -14582,7 +14884,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 154 + "lineNumber": 168 }, "deprecated": false, "children": [ @@ -14595,7 +14897,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 155 + "lineNumber": 169 }, "deprecated": false }, @@ -14608,7 +14910,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 156 + "lineNumber": 170 }, "deprecated": false }, @@ -14621,7 +14923,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 157 + "lineNumber": 171 }, "deprecated": false }, @@ -14637,7 +14939,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 158 + "lineNumber": 172 }, "deprecated": false }, @@ -14653,7 +14955,23 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 159 + "lineNumber": 173 + }, + "deprecated": false + }, + { + "parentPluginId": "fleet", + "id": "def-common.RegistryInput.RegistryInputKeys.input_group", + "type": "CompoundType", + "tags": [], + "label": "[RegistryInputKeys.input_group]", + "description": [], + "signature": [ + "\"metrics\" | \"logs\" | undefined" + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/models/epm.ts", + "lineNumber": 174 }, "deprecated": false }, @@ -14676,7 +14994,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 160 + "lineNumber": 175 }, "deprecated": false } @@ -14692,7 +15010,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 137 + "lineNumber": 143 }, "deprecated": false, "children": [ @@ -14705,7 +15023,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 138 + "lineNumber": 144 }, "deprecated": false }, @@ -14718,7 +15036,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 139 + "lineNumber": 145 }, "deprecated": false }, @@ -14731,7 +15049,85 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 140 + "lineNumber": 146 + }, + "deprecated": false + }, + { + "parentPluginId": "fleet", + "id": "def-common.RegistryPolicyTemplate.RegistryPolicyTemplateKeys.icons", + "type": "Array", + "tags": [], + "label": "[RegistryPolicyTemplateKeys.icons]", + "description": [], + "signature": [ + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryImage", + "text": "RegistryImage" + }, + "[] | undefined" + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/models/epm.ts", + "lineNumber": 147 + }, + "deprecated": false + }, + { + "parentPluginId": "fleet", + "id": "def-common.RegistryPolicyTemplate.RegistryPolicyTemplateKeys.screenshots", + "type": "Array", + "tags": [], + "label": "[RegistryPolicyTemplateKeys.screenshots]", + "description": [], + "signature": [ + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryImage", + "text": "RegistryImage" + }, + "[] | undefined" + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/models/epm.ts", + "lineNumber": 148 + }, + "deprecated": false + }, + { + "parentPluginId": "fleet", + "id": "def-common.RegistryPolicyTemplate.RegistryPolicyTemplateKeys.categories", + "type": "Array", + "tags": [], + "label": "[RegistryPolicyTemplateKeys.categories]", + "description": [], + "signature": [ + "(\"custom\" | \"security\" | \"monitoring\" | \"cloud\" | \"kubernetes\" | \"aws\" | \"azure\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"languages\" | \"message_queue\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"support\" | \"ticketing\" | \"version_control\" | \"web\" | undefined)[] | undefined" + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/models/epm.ts", + "lineNumber": 149 + }, + "deprecated": false + }, + { + "parentPluginId": "fleet", + "id": "def-common.RegistryPolicyTemplate.RegistryPolicyTemplateKeys.data_streams", + "type": "Array", + "tags": [], + "label": "[RegistryPolicyTemplateKeys.data_streams]", + "description": [], + "signature": [ + "string[] | undefined" + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/models/epm.ts", + "lineNumber": 150 }, "deprecated": false }, @@ -14754,7 +15150,23 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 141 + "lineNumber": 151 + }, + "deprecated": false + }, + { + "parentPluginId": "fleet", + "id": "def-common.RegistryPolicyTemplate.RegistryPolicyTemplateKeys.readme", + "type": "string", + "tags": [], + "label": "[RegistryPolicyTemplateKeys.readme]", + "description": [], + "signature": [ + "string | undefined" + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/models/epm.ts", + "lineNumber": 152 }, "deprecated": false }, @@ -14770,7 +15182,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 142 + "lineNumber": 153 }, "deprecated": false } @@ -14786,7 +15198,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 172 + "lineNumber": 187 }, "deprecated": false, "children": [ @@ -14799,7 +15211,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 173 + "lineNumber": 188 }, "deprecated": false }, @@ -14812,7 +15224,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 174 + "lineNumber": 189 }, "deprecated": false }, @@ -14828,7 +15240,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 175 + "lineNumber": 190 }, "deprecated": false }, @@ -14844,7 +15256,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 176 + "lineNumber": 191 }, "deprecated": false }, @@ -14867,7 +15279,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 177 + "lineNumber": 192 }, "deprecated": false }, @@ -14880,7 +15292,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 178 + "lineNumber": 193 }, "deprecated": false } @@ -14896,7 +15308,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 302 + "lineNumber": 317 }, "deprecated": false, "children": [ @@ -14909,7 +15321,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 303 + "lineNumber": 318 }, "deprecated": false }, @@ -14925,7 +15337,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 304 + "lineNumber": 319 }, "deprecated": false }, @@ -14941,7 +15353,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 305 + "lineNumber": 320 }, "deprecated": false }, @@ -14957,7 +15369,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 306 + "lineNumber": 321 }, "deprecated": false }, @@ -14973,7 +15385,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 307 + "lineNumber": 322 }, "deprecated": false }, @@ -14989,7 +15401,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 308 + "lineNumber": 323 }, "deprecated": false }, @@ -15005,7 +15417,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 309 + "lineNumber": 324 }, "deprecated": false }, @@ -15017,11 +15429,11 @@ "label": "[RegistryVarsEntryKeys.default]", "description": [], "signature": [ - "string | string[] | undefined" + "string | boolean | string[] | undefined" ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 310 + "lineNumber": 325 }, "deprecated": false }, @@ -15037,7 +15449,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 311 + "lineNumber": 326 }, "deprecated": false } @@ -15053,7 +15465,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 183 + "lineNumber": 198 }, "deprecated": false, "children": [ @@ -15066,7 +15478,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 184 + "lineNumber": 199 }, "deprecated": false } @@ -15162,7 +15574,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 403 + "lineNumber": 421 }, "deprecated": false, "children": [ @@ -15175,7 +15587,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 404 + "lineNumber": 422 }, "deprecated": false }, @@ -15197,7 +15609,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 405 + "lineNumber": 423 }, "deprecated": false } @@ -15316,7 +15728,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", - "lineNumber": 63 + "lineNumber": 64 }, "deprecated": false, "children": [ @@ -15332,7 +15744,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", - "lineNumber": 64 + "lineNumber": 65 }, "deprecated": false } @@ -15350,7 +15762,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 76 + "lineNumber": 81 }, "deprecated": false, "initialIsOpen": false @@ -15364,7 +15776,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 26 + "lineNumber": 31 }, "deprecated": false, "initialIsOpen": false @@ -15378,7 +15790,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 51 + "lineNumber": 56 }, "deprecated": false, "initialIsOpen": false @@ -15392,7 +15804,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 65 + "lineNumber": 70 }, "deprecated": false, "initialIsOpen": false @@ -15406,7 +15818,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 251 + "lineNumber": 266 }, "deprecated": false, "initialIsOpen": false @@ -15420,7 +15832,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 145 + "lineNumber": 156 }, "deprecated": false, "initialIsOpen": false @@ -15434,7 +15846,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 129 + "lineNumber": 130 }, "deprecated": false, "initialIsOpen": false @@ -15448,7 +15860,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 163 + "lineNumber": 178 }, "deprecated": false, "initialIsOpen": false @@ -15462,7 +15874,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 287 + "lineNumber": 302 }, "deprecated": false, "initialIsOpen": false @@ -15801,7 +16213,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 45 + "lineNumber": 50 }, "deprecated": false, "initialIsOpen": false @@ -16001,7 +16413,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 89 + "lineNumber": 94 }, "deprecated": false, "initialIsOpen": false @@ -16032,7 +16444,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 367 + "lineNumber": 385 }, "deprecated": false, "initialIsOpen": false @@ -16098,7 +16510,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 229 + "lineNumber": 244 }, "deprecated": false, "initialIsOpen": false @@ -16130,7 +16542,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 46 + "lineNumber": 51 }, "deprecated": false, "initialIsOpen": false @@ -16179,7 +16591,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 228 + "lineNumber": 243 }, "deprecated": false, "initialIsOpen": false @@ -16227,7 +16639,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 213 + "lineNumber": 228 }, "deprecated": false, "initialIsOpen": false @@ -16251,7 +16663,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 212 + "lineNumber": 227 }, "deprecated": false, "initialIsOpen": false @@ -16282,7 +16694,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 85 + "lineNumber": 90 }, "deprecated": false, "initialIsOpen": false @@ -16316,7 +16728,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 382 + "lineNumber": 400 }, "deprecated": false, "initialIsOpen": false @@ -16350,7 +16762,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 43 + "lineNumber": 48 }, "deprecated": false, "initialIsOpen": false @@ -16382,7 +16794,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 240 + "lineNumber": 255 }, "deprecated": false, "initialIsOpen": false @@ -16447,7 +16859,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 246 + "lineNumber": 261 }, "deprecated": false, "initialIsOpen": false @@ -16529,7 +16941,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 41 + "lineNumber": 46 }, "deprecated": false, "initialIsOpen": false @@ -16556,7 +16968,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 372 + "lineNumber": 390 }, "deprecated": false, "initialIsOpen": false @@ -16778,7 +17190,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 356 + "lineNumber": 374 }, "deprecated": false, "initialIsOpen": false @@ -16809,7 +17221,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 87 + "lineNumber": 92 }, "deprecated": false, "initialIsOpen": false @@ -16826,7 +17238,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 24 + "lineNumber": 29 }, "deprecated": false, "initialIsOpen": false @@ -16853,7 +17265,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 358 + "lineNumber": 376 }, "deprecated": false, "initialIsOpen": false @@ -16870,7 +17282,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 39 + "lineNumber": 44 }, "deprecated": false, "initialIsOpen": false @@ -16887,7 +17299,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 38 + "lineNumber": 43 }, "deprecated": false, "initialIsOpen": false @@ -16919,7 +17331,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 235 + "lineNumber": 250 }, "deprecated": false, "initialIsOpen": false @@ -16946,7 +17358,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 369 + "lineNumber": 387 }, "deprecated": false, "initialIsOpen": false @@ -17027,7 +17439,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 245 + "lineNumber": 260 }, "deprecated": false, "initialIsOpen": false @@ -17078,7 +17490,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 363 + "lineNumber": 381 }, "deprecated": false, "initialIsOpen": false @@ -17209,7 +17621,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 376 + "lineNumber": 394 }, "deprecated": false, "initialIsOpen": false @@ -17321,7 +17733,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 335 + "lineNumber": 353 }, "deprecated": false, "initialIsOpen": false @@ -17338,22 +17750,14 @@ "pluginId": "fleet", "scope": "common", "docId": "kibFleetPluginApi", - "section": "def-common.Installable", - "text": "Installable" - }, - ">[]" + "[]" ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 331 + "lineNumber": 346 }, "deprecated": false, "initialIsOpen": false @@ -17366,14 +17770,7 @@ "label": "PackageListItem", "description": [], "signature": [ - { - "pluginId": "fleet", - "scope": "common", - "docId": "kibFleetPluginApi", - "section": "def-common.Installed", - "text": "Installed" - }, - "> | ", + ", \"type\" | \"description\" | \"title\" | \"name\" | \"version\" | \"download\" | \"path\" | \"internal\" | \"data_streams\" | \"release\" | \"icons\" | \"policy_templates\"> & { status: \"installed\"; savedObject: ", + "SavedObject", + "<", { "pluginId": "fleet", "scope": "common", "docId": "kibFleetPluginApi", - "section": "def-common.NotInstalled", - "text": "NotInstalled" + "section": "def-common.Installation", + "text": "Installation" }, - "; } & { integration?: string | undefined; id: string; }) | (Pick<", { "pluginId": "fleet", "scope": "common", @@ -17397,11 +17796,11 @@ "section": "def-common.RegistryPackage", "text": "RegistryPackage" }, - ", \"type\" | \"description\" | \"title\" | \"name\" | \"version\" | \"download\" | \"path\" | \"internal\" | \"data_streams\" | \"release\" | \"icons\" | \"policy_templates\">>" + ", \"type\" | \"description\" | \"title\" | \"name\" | \"version\" | \"download\" | \"path\" | \"internal\" | \"data_streams\" | \"release\" | \"icons\" | \"policy_templates\"> & { status: \"not_installed\"; } & { integration?: string | undefined; id: string; })" ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 333 + "lineNumber": 347 }, "deprecated": false, "initialIsOpen": false @@ -17455,11 +17854,19 @@ "section": "def-common.PackagePolicyInput", "text": "PackagePolicyInput" }, - "[]; policy_id: string; output_id: string; revision: number; }" + "[]; policy_id: string; output_id: string; vars?: Record | undefined; revision: number; }" ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_policy.ts", - "lineNumber": 78 + "lineNumber": 79 }, "deprecated": false, "initialIsOpen": false @@ -17509,7 +17916,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 334 + "lineNumber": 352 }, "deprecated": false, "initialIsOpen": false @@ -17526,7 +17933,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 28 + "lineNumber": 29 }, "deprecated": false, "initialIsOpen": false @@ -17543,7 +17950,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", - "lineNumber": 53 + "lineNumber": 54 }, "deprecated": false, "initialIsOpen": false @@ -17667,6 +18074,23 @@ "deprecated": false, "initialIsOpen": false }, + { + "parentPluginId": "fleet", + "id": "def-common.RegistryInputGroup", + "type": "Type", + "tags": [], + "label": "RegistryInputGroup", + "description": [], + "signature": [ + "\"metrics\" | \"logs\"" + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/models/epm.ts", + "lineNumber": 166 + }, + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "fleet", "id": "def-common.RegistryPackage", @@ -17694,7 +18118,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 93 + "lineNumber": 98 }, "deprecated": false, "initialIsOpen": false @@ -17711,7 +18135,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 120 + "lineNumber": 125 }, "deprecated": false, "initialIsOpen": false @@ -17760,7 +18184,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 192 + "lineNumber": 207 }, "deprecated": false, "initialIsOpen": false @@ -17785,7 +18209,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 190 + "lineNumber": 205 }, "deprecated": false, "initialIsOpen": false @@ -17802,7 +18226,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 286 + "lineNumber": 301 }, "deprecated": false, "initialIsOpen": false @@ -17836,7 +18260,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 380 + "lineNumber": 398 }, "deprecated": false, "initialIsOpen": false @@ -17853,7 +18277,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 220 + "lineNumber": 235 }, "deprecated": false, "initialIsOpen": false @@ -17870,7 +18294,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 181 + "lineNumber": 196 }, "deprecated": false, "initialIsOpen": false @@ -17887,7 +18311,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 182 + "lineNumber": 197 }, "deprecated": false, "initialIsOpen": false @@ -17918,7 +18342,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 208 + "lineNumber": 223 }, "deprecated": false, "initialIsOpen": false @@ -17935,7 +18359,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/epm.ts", - "lineNumber": 44 + "lineNumber": 49 }, "deprecated": false, "initialIsOpen": false diff --git a/api_docs/home.json b/api_docs/home.json index 2aa23c1b756dfa..a1039beb8d3a24 100644 --- a/api_docs/home.json +++ b/api_docs/home.json @@ -1070,7 +1070,7 @@ }, ") => void; getSampleDatasets: () => ", "Writable", - "[]; previewImagePath: string; overviewDashboard: string; appLinks: Readonly<{} & { label: string; path: string; icon: string; }>[]; dataIndices: Readonly<{} & { id: string; fields: Record; timeFields: string[]; dataPath: string; currentTimeMarker: string; preserveDayOfWeekTimeOfDay: boolean; }>[]; }>>[]; addSavedObjectsToSampleDataset: (id: string, savedObjects: ", + "[]; previewImagePath: string; overviewDashboard: string; appLinks: Readonly<{} & { label: string; path: string; icon: string; }>[]; defaultIndex: string; dataIndices: Readonly<{} & { id: string; fields: Record; timeFields: string[]; dataPath: string; currentTimeMarker: string; preserveDayOfWeekTimeOfDay: boolean; }>[]; }>>[]; addSavedObjectsToSampleDataset: (id: string, savedObjects: ", "SavedObject", "[]) => void; addAppLinksToSampleDataset: (id: string, appLinks: Readonly<{} & { label: string; path: string; icon: string; }>[]) => void; replacePanelInSampleDatasetDashboard: ({ sampleDataId, dashboardId, oldEmbeddableId, embeddableId, embeddableType, embeddableConfig, }: ", "SampleDatasetDashboardPanel", @@ -1093,7 +1093,7 @@ "signature": [ "() => ", "Writable", - "[]; previewImagePath: string; overviewDashboard: string; appLinks: Readonly<{} & { label: string; path: string; icon: string; }>[]; dataIndices: Readonly<{} & { id: string; fields: Record; timeFields: string[]; dataPath: string; currentTimeMarker: string; preserveDayOfWeekTimeOfDay: boolean; }>[]; }>>" + "[]; previewImagePath: string; overviewDashboard: string; appLinks: Readonly<{} & { label: string; path: string; icon: string; }>[]; defaultIndex: string; dataIndices: Readonly<{} & { id: string; fields: Record; timeFields: string[]; dataPath: string; currentTimeMarker: string; preserveDayOfWeekTimeOfDay: boolean; }>[]; }>>" ], "source": { "path": "src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts", @@ -1426,7 +1426,7 @@ }, ") => void; getSampleDatasets: () => ", "Writable", - "[]; previewImagePath: string; overviewDashboard: string; appLinks: Readonly<{} & { label: string; path: string; icon: string; }>[]; dataIndices: Readonly<{} & { id: string; fields: Record; timeFields: string[]; dataPath: string; currentTimeMarker: string; preserveDayOfWeekTimeOfDay: boolean; }>[]; }>>[]; addSavedObjectsToSampleDataset: (id: string, savedObjects: ", + "[]; previewImagePath: string; overviewDashboard: string; appLinks: Readonly<{} & { label: string; path: string; icon: string; }>[]; defaultIndex: string; dataIndices: Readonly<{} & { id: string; fields: Record; timeFields: string[]; dataPath: string; currentTimeMarker: string; preserveDayOfWeekTimeOfDay: boolean; }>[]; }>>[]; addSavedObjectsToSampleDataset: (id: string, savedObjects: ", "SavedObject", "[]) => void; addAppLinksToSampleDataset: (id: string, appLinks: Readonly<{} & { label: string; path: string; icon: string; }>[]) => void; replacePanelInSampleDatasetDashboard: ({ sampleDataId, dashboardId, oldEmbeddableId, embeddableId, embeddableType, embeddableConfig, }: ", "SampleDatasetDashboardPanel", diff --git a/api_docs/kibana_react.json b/api_docs/kibana_react.json index 9e144147e6919d..98537d71d7b019 100644 --- a/api_docs/kibana_react.json +++ b/api_docs/kibana_react.json @@ -1055,7 +1055,7 @@ "label": "KibanaPageTemplate", "description": [], "signature": [ - "({ template, pageHeader, children, isEmptyState, restrictWidth, bottomBar, bottomBarProps, ...rest }: React.PropsWithChildren<", + "({ template, pageHeader, children, isEmptyState, restrictWidth, bottomBar, bottomBarProps, pageSideBar, solutionNav, ...rest }: React.PropsWithChildren<", { "pluginId": "kibanaReact", "scope": "public", @@ -1067,7 +1067,7 @@ ], "source": { "path": "src/plugins/kibana_react/public/page_template/page_template.tsx", - "lineNumber": 22 + "lineNumber": 37 }, "deprecated": false, "children": [ @@ -1076,7 +1076,7 @@ "id": "def-public.KibanaPageTemplate.$1", "type": "CompoundType", "tags": [], - "label": "{\n template,\n pageHeader,\n children,\n isEmptyState,\n restrictWidth = true,\n bottomBar,\n bottomBarProps,\n ...rest\n}", + "label": "{\n template,\n pageHeader,\n children,\n isEmptyState,\n restrictWidth = true,\n bottomBar,\n bottomBarProps,\n pageSideBar,\n solutionNav,\n ...rest\n}", "description": [], "signature": [ "React.PropsWithChildren<", @@ -1091,7 +1091,7 @@ ], "source": { "path": "src/plugins/kibana_react/public/page_template/page_template.tsx", - "lineNumber": 22 + "lineNumber": 37 }, "deprecated": false, "isRequired": true @@ -1581,7 +1581,9 @@ "type": "CompoundType", "tags": [], "label": "ReactComp", - "description": [], + "description": [ + "A React component." + ], "signature": [ "React.ComponentType" ], @@ -1667,7 +1669,9 @@ "type": "CompoundType", "tags": [], "label": "node", - "description": [], + "description": [ + "to get a mount point for" + ], "signature": [ "React.ReactNode" ], @@ -3936,7 +3940,9 @@ "type": "Type", "tags": [], "label": "KibanaPageTemplateProps", - "description": [], + "description": [ + "\nA thin wrapper around EuiPageTemplate with a few Kibana specific additions" + ], "signature": [ "(Pick<", "EuiPageProps", @@ -3960,7 +3966,9 @@ "EuiPageContentProps", " | undefined; pageContentBodyProps?: ", "EuiPageContentBodyProps", - " | undefined; } & { isEmptyState?: boolean | undefined; }) | (Pick<", + " | undefined; } & { isEmptyState?: boolean | undefined; solutionNav?: ", + "KibanaPageTemplateSolutionNavProps", + " | undefined; }) | (Pick<", "EuiPageProps", ", \"children\" | \"onClick\" | \"onChange\" | \"color\" | \"onKeyDown\" | \"title\" | \"id\" | \"defaultChecked\" | \"defaultValue\" | \"suppressContentEditableWarning\" | \"suppressHydrationWarning\" | \"accessKey\" | \"className\" | \"contentEditable\" | \"contextMenu\" | \"dir\" | \"draggable\" | \"hidden\" | \"lang\" | \"placeholder\" | \"slot\" | \"spellCheck\" | \"style\" | \"tabIndex\" | \"translate\" | \"radioGroup\" | \"role\" | \"about\" | \"datatype\" | \"inlist\" | \"prefix\" | \"property\" | \"resource\" | \"typeof\" | \"vocab\" | \"autoCapitalize\" | \"autoCorrect\" | \"autoSave\" | \"itemProp\" | \"itemScope\" | \"itemType\" | \"itemID\" | \"itemRef\" | \"results\" | \"security\" | \"unselectable\" | \"inputMode\" | \"is\" | \"aria-activedescendant\" | \"aria-atomic\" | \"aria-autocomplete\" | \"aria-busy\" | \"aria-checked\" | \"aria-colcount\" | \"aria-colindex\" | \"aria-colspan\" | \"aria-controls\" | \"aria-current\" | \"aria-describedby\" | \"aria-details\" | \"aria-disabled\" | \"aria-dropeffect\" | \"aria-errormessage\" | \"aria-expanded\" | \"aria-flowto\" | \"aria-grabbed\" | \"aria-haspopup\" | \"aria-hidden\" | \"aria-invalid\" | \"aria-keyshortcuts\" | \"aria-label\" | \"aria-labelledby\" | \"aria-level\" | \"aria-live\" | \"aria-modal\" | \"aria-multiline\" | \"aria-multiselectable\" | \"aria-orientation\" | \"aria-owns\" | \"aria-placeholder\" | \"aria-posinset\" | \"aria-pressed\" | \"aria-readonly\" | \"aria-relevant\" | \"aria-required\" | \"aria-roledescription\" | \"aria-rowcount\" | \"aria-rowindex\" | \"aria-rowspan\" | \"aria-selected\" | \"aria-setsize\" | \"aria-sort\" | \"aria-valuemax\" | \"aria-valuemin\" | \"aria-valuenow\" | \"aria-valuetext\" | \"dangerouslySetInnerHTML\" | \"onCopy\" | \"onCopyCapture\" | \"onCut\" | \"onCutCapture\" | \"onPaste\" | \"onPasteCapture\" | \"onCompositionEnd\" | \"onCompositionEndCapture\" | \"onCompositionStart\" | \"onCompositionStartCapture\" | \"onCompositionUpdate\" | \"onCompositionUpdateCapture\" | \"onFocus\" | \"onFocusCapture\" | \"onBlur\" | \"onBlurCapture\" | \"onChangeCapture\" | \"onBeforeInput\" | \"onBeforeInputCapture\" | \"onInput\" | \"onInputCapture\" | \"onReset\" | \"onResetCapture\" | \"onSubmit\" | \"onSubmitCapture\" | \"onInvalid\" | \"onInvalidCapture\" | \"onLoad\" | \"onLoadCapture\" | \"onError\" | \"onErrorCapture\" | \"onKeyDownCapture\" | \"onKeyPress\" | \"onKeyPressCapture\" | \"onKeyUp\" | \"onKeyUpCapture\" | \"onAbort\" | \"onAbortCapture\" | \"onCanPlay\" | \"onCanPlayCapture\" | \"onCanPlayThrough\" | \"onCanPlayThroughCapture\" | \"onDurationChange\" | \"onDurationChangeCapture\" | \"onEmptied\" | \"onEmptiedCapture\" | \"onEncrypted\" | \"onEncryptedCapture\" | \"onEnded\" | \"onEndedCapture\" | \"onLoadedData\" | \"onLoadedDataCapture\" | \"onLoadedMetadata\" | \"onLoadedMetadataCapture\" | \"onLoadStart\" | \"onLoadStartCapture\" | \"onPause\" | \"onPauseCapture\" | \"onPlay\" | \"onPlayCapture\" | \"onPlaying\" | \"onPlayingCapture\" | \"onProgress\" | \"onProgressCapture\" | \"onRateChange\" | \"onRateChangeCapture\" | \"onSeeked\" | \"onSeekedCapture\" | \"onSeeking\" | \"onSeekingCapture\" | \"onStalled\" | \"onStalledCapture\" | \"onSuspend\" | \"onSuspendCapture\" | \"onTimeUpdate\" | \"onTimeUpdateCapture\" | \"onVolumeChange\" | \"onVolumeChangeCapture\" | \"onWaiting\" | \"onWaitingCapture\" | \"onAuxClick\" | \"onAuxClickCapture\" | \"onClickCapture\" | \"onContextMenu\" | \"onContextMenuCapture\" | \"onDoubleClick\" | \"onDoubleClickCapture\" | \"onDrag\" | \"onDragCapture\" | \"onDragEnd\" | \"onDragEndCapture\" | \"onDragEnter\" | \"onDragEnterCapture\" | \"onDragExit\" | \"onDragExitCapture\" | \"onDragLeave\" | \"onDragLeaveCapture\" | \"onDragOver\" | \"onDragOverCapture\" | \"onDragStart\" | \"onDragStartCapture\" | \"onDrop\" | \"onDropCapture\" | \"onMouseDown\" | \"onMouseDownCapture\" | \"onMouseEnter\" | \"onMouseLeave\" | \"onMouseMove\" | \"onMouseMoveCapture\" | \"onMouseOut\" | \"onMouseOutCapture\" | \"onMouseOver\" | \"onMouseOverCapture\" | \"onMouseUp\" | \"onMouseUpCapture\" | \"onSelect\" | \"onSelectCapture\" | \"onTouchCancel\" | \"onTouchCancelCapture\" | \"onTouchEnd\" | \"onTouchEndCapture\" | \"onTouchMove\" | \"onTouchMoveCapture\" | \"onTouchStart\" | \"onTouchStartCapture\" | \"onPointerDown\" | \"onPointerDownCapture\" | \"onPointerMove\" | \"onPointerMoveCapture\" | \"onPointerUp\" | \"onPointerUpCapture\" | \"onPointerCancel\" | \"onPointerCancelCapture\" | \"onPointerEnter\" | \"onPointerEnterCapture\" | \"onPointerLeave\" | \"onPointerLeaveCapture\" | \"onPointerOver\" | \"onPointerOverCapture\" | \"onPointerOut\" | \"onPointerOutCapture\" | \"onGotPointerCapture\" | \"onGotPointerCaptureCapture\" | \"onLostPointerCapture\" | \"onLostPointerCaptureCapture\" | \"onScroll\" | \"onScrollCapture\" | \"onWheel\" | \"onWheelCapture\" | \"onAnimationStart\" | \"onAnimationStartCapture\" | \"onAnimationEnd\" | \"onAnimationEndCapture\" | \"onAnimationIteration\" | \"onAnimationIterationCapture\" | \"onTransitionEnd\" | \"onTransitionEndCapture\" | \"css\" | \"data-test-subj\" | \"grow\" | \"direction\" | \"restrictWidth\"> & ", "DisambiguateSet", @@ -3990,11 +3998,13 @@ "EuiPageContentProps", " | undefined; pageContentBodyProps?: ", "EuiPageContentBodyProps", - " | undefined; } & { isEmptyState?: boolean | undefined; })" + " | undefined; } & { isEmptyState?: boolean | undefined; solutionNav?: ", + "KibanaPageTemplateSolutionNavProps", + " | undefined; })" ], "source": { "path": "src/plugins/kibana_react/public/page_template/page_template.tsx", - "lineNumber": 12 + "lineNumber": 23 }, "deprecated": false, "initialIsOpen": false diff --git a/api_docs/kibana_utils.json b/api_docs/kibana_utils.json index 173348ea2f2638..66eefa9097bfb1 100644 --- a/api_docs/kibana_utils.json +++ b/api_docs/kibana_utils.json @@ -2551,7 +2551,9 @@ "type": "Object", "tags": [], "label": "storage", - "description": [], + "description": [ + "- Option {@link Storage} to use for storing state. By default window.sessionStorage." + ], "signature": [ "Storage" ], @@ -2616,7 +2618,9 @@ "type": "Function", "tags": [], "label": "accessor", - "description": [], + "description": [ + "Asynchronous start service accessor provided by platform." + ], "signature": [ { "pluginId": "core", @@ -3623,7 +3627,9 @@ "type": "Object", "tags": [], "label": "promise", - "description": [], + "description": [ + "Promise to convert to 3-tuple." + ], "signature": [ "Promise" ], @@ -4687,7 +4693,9 @@ "type": "Uncategorized", "tags": [], "label": "container", - "description": [], + "description": [ + "- {@link StateContainer} which state to track." + ], "signature": [ "Container" ], @@ -4704,7 +4712,9 @@ "type": "Function", "tags": [], "label": "selector", - "description": [], + "description": [ + "- Function used to pick parts of state." + ], "signature": [ "(state: ", { @@ -4729,7 +4739,9 @@ "type": "Function", "tags": [], "label": "comparator", - "description": [], + "description": [ + "- {@link Comparator} function used to memoize previous result, to not\nre-render React component if state did not change. By default uses\n`fast-deep-equal` package." + ], "signature": [ { "pluginId": "kibanaUtils", @@ -4793,7 +4805,9 @@ "type": "Uncategorized", "tags": [], "label": "container", - "description": [], + "description": [ + "- {@link StateContainer} which state to track." + ], "signature": [ "Container" ], @@ -10006,7 +10020,9 @@ "type": "Object", "tags": [], "label": "promise", - "description": [], + "description": [ + "Promise to convert to 3-tuple." + ], "signature": [ "Promise" ], @@ -10069,7 +10085,9 @@ "type": "Uncategorized", "tags": [], "label": "container", - "description": [], + "description": [ + "- {@link StateContainer} which state to track." + ], "signature": [ "Container" ], @@ -10086,7 +10104,9 @@ "type": "Function", "tags": [], "label": "selector", - "description": [], + "description": [ + "- Function used to pick parts of state." + ], "signature": [ "(state: ", { @@ -10111,7 +10131,9 @@ "type": "Function", "tags": [], "label": "comparator", - "description": [], + "description": [ + "- {@link Comparator} function used to memoize previous result, to not\nre-render React component if state did not change. By default uses\n`fast-deep-equal` package." + ], "signature": [ { "pluginId": "kibanaUtils", @@ -10175,7 +10197,9 @@ "type": "Uncategorized", "tags": [], "label": "container", - "description": [], + "description": [ + "- {@link StateContainer} which state to track." + ], "signature": [ "Container" ], diff --git a/api_docs/lens.json b/api_docs/lens.json index f81dbe4217f287..196361f65682b9 100644 --- a/api_docs/lens.json +++ b/api_docs/lens.json @@ -1353,7 +1353,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 473 + "lineNumber": 474 }, "deprecated": false, "children": [ @@ -1369,7 +1369,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 474 + "lineNumber": 475 }, "deprecated": false }, @@ -1391,7 +1391,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 475 + "lineNumber": 476 }, "deprecated": false }, @@ -1407,7 +1407,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 476 + "lineNumber": 477 }, "deprecated": false }, @@ -1423,7 +1423,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 477 + "lineNumber": 478 }, "deprecated": false }, @@ -1440,7 +1440,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 478 + "lineNumber": 479 }, "deprecated": false }, @@ -1457,7 +1457,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 479 + "lineNumber": 480 }, "deprecated": false }, @@ -1480,7 +1480,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 480 + "lineNumber": 481 }, "deprecated": false }, @@ -1496,7 +1496,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 481 + "lineNumber": 482 }, "deprecated": false }, @@ -1512,7 +1512,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 482 + "lineNumber": 483 }, "deprecated": false }, @@ -1528,7 +1528,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 483 + "lineNumber": 484 }, "deprecated": false }, @@ -1551,7 +1551,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 484 + "lineNumber": 485 }, "deprecated": false }, @@ -1574,7 +1574,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 485 + "lineNumber": 486 }, "deprecated": false }, @@ -1597,7 +1597,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 486 + "lineNumber": 487 }, "deprecated": false }, @@ -1613,7 +1613,23 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 487 + "lineNumber": 488 + }, + "deprecated": false + }, + { + "parentPluginId": "lens", + "id": "def-public.XYState.fillOpacity", + "type": "number", + "tags": [], + "label": "fillOpacity", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", + "lineNumber": 489 }, "deprecated": false }, @@ -1629,7 +1645,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 488 + "lineNumber": 490 }, "deprecated": false } @@ -2093,7 +2109,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts", - "lineNumber": 497 + "lineNumber": 498 }, "deprecated": false, "initialIsOpen": false @@ -2280,7 +2296,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/xy_visualization/types.ts", - "lineNumber": 470 + "lineNumber": 471 }, "deprecated": false, "initialIsOpen": false diff --git a/api_docs/licensing.json b/api_docs/licensing.json index 873f40bd301a1c..1451cff869f47a 100644 --- a/api_docs/licensing.json +++ b/api_docs/licensing.json @@ -3088,7 +3088,7 @@ "plugin": "reporting", "link": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 228 + "lineNumber": 226 } }, { diff --git a/api_docs/maps.json b/api_docs/maps.json index a8c582800fc3b9..d418518dcb0d2c 100644 --- a/api_docs/maps.json +++ b/api_docs/maps.json @@ -57,7 +57,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 90 + "lineNumber": 95 }, "deprecated": false, "children": [ @@ -70,7 +70,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 93 + "lineNumber": 98 }, "deprecated": false }, @@ -86,7 +86,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 110 + "lineNumber": 118 }, "deprecated": false, "children": [ @@ -102,7 +102,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 110 + "lineNumber": 118 }, "deprecated": false, "isRequired": true @@ -125,7 +125,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 110 + "lineNumber": 118 }, "deprecated": false, "isRequired": true @@ -165,7 +165,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 110 + "lineNumber": 118 }, "deprecated": false, "isRequired": false @@ -194,7 +194,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 184 + "lineNumber": 200 }, "deprecated": false, "children": [ @@ -216,7 +216,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 185 + "lineNumber": 201 }, "deprecated": false, "isRequired": true @@ -238,7 +238,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 190 + "lineNumber": 206 }, "deprecated": false, "children": [], @@ -258,7 +258,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 198 + "lineNumber": 214 }, "deprecated": false, "children": [], @@ -276,7 +276,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 203 + "lineNumber": 219 }, "deprecated": false, "children": [], @@ -294,7 +294,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 207 + "lineNumber": 223 }, "deprecated": false, "children": [], @@ -314,7 +314,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 211 + "lineNumber": 227 }, "deprecated": false, "children": [ @@ -330,7 +330,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 211 + "lineNumber": 227 }, "deprecated": false, "isRequired": true @@ -352,7 +352,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 215 + "lineNumber": 231 }, "deprecated": false, "children": [ @@ -368,7 +368,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 215 + "lineNumber": 231 }, "deprecated": false, "isRequired": true @@ -395,7 +395,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 219 + "lineNumber": 235 }, "deprecated": false, "children": [], @@ -413,7 +413,51 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 223 + "lineNumber": 239 + }, + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "maps", + "id": "def-public.MapEmbeddable._getFilters", + "type": "Function", + "tags": [], + "label": "_getFilters", + "description": [], + "signature": [ + "() => ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[]" + ], + "source": { + "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", + "lineNumber": 283 + }, + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "maps", + "id": "def-public.MapEmbeddable._getSearchSessionId", + "type": "Function", + "tags": [], + "label": "_getSearchSessionId", + "description": [], + "signature": [ + "() => string | undefined" + ], + "source": { + "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", + "lineNumber": 291 }, "deprecated": false, "children": [], @@ -431,7 +475,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 255 + "lineNumber": 301 }, "deprecated": false, "children": [ @@ -444,7 +488,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 255 + "lineNumber": 301 }, "deprecated": false, "children": [ @@ -457,7 +501,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 255 + "lineNumber": 301 }, "deprecated": false } @@ -486,7 +530,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 277 + "lineNumber": 321 }, "deprecated": false, "children": [ @@ -508,7 +552,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 277 + "lineNumber": 321 }, "deprecated": false, "isRequired": true @@ -528,7 +572,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 287 + "lineNumber": 331 }, "deprecated": false, "children": [ @@ -544,7 +588,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 287 + "lineNumber": 331 }, "deprecated": false, "isRequired": false @@ -566,7 +610,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 305 + "lineNumber": 349 }, "deprecated": false, "children": [ @@ -582,7 +626,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 305 + "lineNumber": 349 }, "deprecated": false, "isRequired": true @@ -604,7 +648,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 331 + "lineNumber": 375 }, "deprecated": false, "children": [ @@ -621,7 +665,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 331 + "lineNumber": 375 }, "deprecated": false, "isRequired": true @@ -649,7 +693,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 348 + "lineNumber": 392 }, "deprecated": false, "children": [ @@ -665,7 +709,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 348 + "lineNumber": 392 }, "deprecated": false, "isRequired": true @@ -682,7 +726,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 348 + "lineNumber": 392 }, "deprecated": false, "isRequired": true @@ -705,7 +749,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 348 + "lineNumber": 392 }, "deprecated": false, "isRequired": false @@ -733,7 +777,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 362 + "lineNumber": 406 }, "deprecated": false, "children": [ @@ -756,7 +800,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 362 + "lineNumber": 406 }, "deprecated": false, "isRequired": true @@ -773,7 +817,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 362 + "lineNumber": 406 }, "deprecated": false, "isRequired": true @@ -801,7 +845,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 374 + "lineNumber": 418 }, "deprecated": false, "children": [], @@ -827,7 +871,43 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 395 + "lineNumber": 439 + }, + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "maps", + "id": "def-public.MapEmbeddable.setMapExtentFilter", + "type": "Function", + "tags": [], + "label": "setMapExtentFilter", + "description": [], + "signature": [ + "() => void" + ], + "source": { + "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", + "lineNumber": 450 + }, + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "maps", + "id": "def-public.MapEmbeddable.clearMapExtentFilter", + "type": "Function", + "tags": [], + "label": "clearMapExtentFilter", + "description": [], + "signature": [ + "() => void" + ], + "source": { + "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", + "lineNumber": 487 }, "deprecated": false, "children": [], @@ -845,7 +925,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 406 + "lineNumber": 501 }, "deprecated": false, "children": [], @@ -863,7 +943,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 422 + "lineNumber": 517 }, "deprecated": false, "children": [], @@ -881,7 +961,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/map_embeddable.tsx", - "lineNumber": 428 + "lineNumber": 523 }, "deprecated": false, "children": [], @@ -1297,7 +1377,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/types.ts", - "lineNumber": 41 + "lineNumber": 42 }, "deprecated": false, "initialIsOpen": false @@ -1329,7 +1409,7 @@ ], "source": { "path": "x-pack/plugins/maps/public/embeddable/types.ts", - "lineNumber": 43 + "lineNumber": 44 }, "deprecated": false, "initialIsOpen": false @@ -1535,7 +1615,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 66 + "lineNumber": 67 }, "deprecated": false, "children": [ @@ -1551,7 +1631,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 66 + "lineNumber": 67 }, "deprecated": false, "isRequired": true @@ -1572,7 +1652,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 63 + "lineNumber": 64 }, "deprecated": false, "children": [ @@ -1588,7 +1668,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 63 + "lineNumber": 64 }, "deprecated": false, "isRequired": true @@ -1609,7 +1689,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 60 + "lineNumber": 61 }, "deprecated": false, "children": [], @@ -1627,7 +1707,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/types.ts", - "lineNumber": 22 + "lineNumber": 23 }, "deprecated": false, "children": [ @@ -1643,7 +1723,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/types.ts", - "lineNumber": 23 + "lineNumber": 24 }, "deprecated": false } @@ -1663,6 +1743,22 @@ }, "deprecated": false, "children": [ + { + "parentPluginId": "maps", + "id": "def-common.CreateDocSourceResp.indexPatternId", + "type": "string", + "tags": [], + "label": "indexPatternId", + "description": [], + "signature": [ + "string | undefined" + ], + "source": { + "path": "x-pack/plugins/maps/common/types.ts", + "lineNumber": 9 + }, + "deprecated": false + }, { "parentPluginId": "maps", "id": "def-common.CreateDocSourceResp.success", @@ -1672,7 +1768,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/types.ts", - "lineNumber": 9 + "lineNumber": 10 }, "deprecated": false }, @@ -1688,7 +1784,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/types.ts", - "lineNumber": 10 + "lineNumber": 11 }, "deprecated": false } @@ -1704,7 +1800,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/types.ts", - "lineNumber": 13 + "lineNumber": 14 }, "deprecated": false, "children": [ @@ -1720,7 +1816,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/types.ts", - "lineNumber": 14 + "lineNumber": 15 }, "deprecated": false }, @@ -1736,7 +1832,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/types.ts", - "lineNumber": 17 + "lineNumber": 18 }, "deprecated": false } @@ -1752,7 +1848,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/types.ts", - "lineNumber": 26 + "lineNumber": 27 }, "deprecated": false, "children": [ @@ -1765,7 +1861,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/types.ts", - "lineNumber": 27 + "lineNumber": 28 }, "deprecated": false }, @@ -1781,7 +1877,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/types.ts", - "lineNumber": 28 + "lineNumber": 29 }, "deprecated": false }, @@ -1797,7 +1893,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/types.ts", - "lineNumber": 29 + "lineNumber": 30 }, "deprecated": false } @@ -1815,7 +1911,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 163 + "lineNumber": 164 }, "deprecated": false, "initialIsOpen": false @@ -1829,7 +1925,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 211 + "lineNumber": 212 }, "deprecated": false, "initialIsOpen": false @@ -1843,7 +1939,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 292 + "lineNumber": 293 }, "deprecated": false, "initialIsOpen": false @@ -1857,7 +1953,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 156 + "lineNumber": 157 }, "deprecated": false, "initialIsOpen": false @@ -1871,7 +1967,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 123 + "lineNumber": 124 }, "deprecated": false, "initialIsOpen": false @@ -1885,7 +1981,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 131 + "lineNumber": 132 }, "deprecated": false, "initialIsOpen": false @@ -1899,7 +1995,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 96 + "lineNumber": 97 }, "deprecated": false, "initialIsOpen": false @@ -1913,7 +2009,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 255 + "lineNumber": 256 }, "deprecated": false, "initialIsOpen": false @@ -1927,7 +2023,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 137 + "lineNumber": 138 }, "deprecated": false, "initialIsOpen": false @@ -1941,7 +2037,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 180 + "lineNumber": 181 }, "deprecated": false, "initialIsOpen": false @@ -1955,7 +2051,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 267 + "lineNumber": 268 }, "deprecated": false, "initialIsOpen": false @@ -1969,7 +2065,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 224 + "lineNumber": 225 }, "deprecated": false, "initialIsOpen": false @@ -1983,7 +2079,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 205 + "lineNumber": 206 }, "deprecated": false, "initialIsOpen": false @@ -1997,7 +2093,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 70 + "lineNumber": 71 }, "deprecated": false, "initialIsOpen": false @@ -2011,7 +2107,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 274 + "lineNumber": 275 }, "deprecated": false, "initialIsOpen": false @@ -2025,7 +2121,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 287 + "lineNumber": 288 }, "deprecated": false, "initialIsOpen": false @@ -2039,7 +2135,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 260 + "lineNumber": 261 }, "deprecated": false, "initialIsOpen": false @@ -2053,7 +2149,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 174 + "lineNumber": 175 }, "deprecated": false, "initialIsOpen": false @@ -2067,7 +2163,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 248 + "lineNumber": 249 }, "deprecated": false, "initialIsOpen": false @@ -2081,7 +2177,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 79 + "lineNumber": 80 }, "deprecated": false, "initialIsOpen": false @@ -2095,7 +2191,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 200 + "lineNumber": 201 }, "deprecated": false, "initialIsOpen": false @@ -2109,7 +2205,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 219 + "lineNumber": 220 }, "deprecated": false, "initialIsOpen": false @@ -2123,7 +2219,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 280 + "lineNumber": 281 }, "deprecated": false, "initialIsOpen": false @@ -2137,7 +2233,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 233 + "lineNumber": 234 }, "deprecated": false, "initialIsOpen": false @@ -2156,7 +2252,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 162 + "lineNumber": 163 }, "deprecated": false, "initialIsOpen": false @@ -2170,7 +2266,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 44 + "lineNumber": 45 }, "deprecated": false, "initialIsOpen": false @@ -2252,7 +2348,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 216 + "lineNumber": 217 }, "deprecated": false, "initialIsOpen": false @@ -2266,7 +2362,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 194 + "lineNumber": 195 }, "deprecated": false, "initialIsOpen": false @@ -2283,7 +2379,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 198 + "lineNumber": 199 }, "deprecated": false, "initialIsOpen": false @@ -2300,7 +2396,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 112 + "lineNumber": 113 }, "deprecated": false, "initialIsOpen": false @@ -2317,7 +2413,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 231 + "lineNumber": 232 }, "deprecated": false, "initialIsOpen": false @@ -2334,7 +2430,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 116 + "lineNumber": 117 }, "deprecated": false, "initialIsOpen": false @@ -2351,7 +2447,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 115 + "lineNumber": 116 }, "deprecated": false, "initialIsOpen": false @@ -2368,7 +2464,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 114 + "lineNumber": 115 }, "deprecated": false, "initialIsOpen": false @@ -2385,7 +2481,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 192 + "lineNumber": 193 }, "deprecated": false, "initialIsOpen": false @@ -2402,7 +2498,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 296 + "lineNumber": 297 }, "deprecated": false, "initialIsOpen": false @@ -2657,7 +2753,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 307 + "lineNumber": 308 }, "deprecated": false, "initialIsOpen": false @@ -2674,7 +2770,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 308 + "lineNumber": 309 }, "deprecated": false, "initialIsOpen": false @@ -2691,7 +2787,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 306 + "lineNumber": 307 }, "deprecated": false, "initialIsOpen": false @@ -2708,7 +2804,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 129 + "lineNumber": 130 }, "deprecated": false, "initialIsOpen": false @@ -2725,7 +2821,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 118 + "lineNumber": 119 }, "deprecated": false, "initialIsOpen": false @@ -2742,7 +2838,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 119 + "lineNumber": 120 }, "deprecated": false, "initialIsOpen": false @@ -2767,7 +2863,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 300 + "lineNumber": 301 }, "deprecated": false, "initialIsOpen": false @@ -2798,7 +2894,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 103 + "lineNumber": 104 }, "deprecated": false, "initialIsOpen": false @@ -2815,7 +2911,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 189 + "lineNumber": 190 }, "deprecated": false, "initialIsOpen": false @@ -2832,7 +2928,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 188 + "lineNumber": 189 }, "deprecated": false, "initialIsOpen": false @@ -2851,6 +2947,20 @@ "deprecated": false, "initialIsOpen": false }, + { + "parentPluginId": "maps", + "id": "def-common.INDEX_FEATURE_PATH", + "type": "string", + "tags": [], + "label": "INDEX_FEATURE_PATH", + "description": [], + "source": { + "path": "x-pack/plugins/maps/common/constants.ts", + "lineNumber": 44 + }, + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "maps", "id": "def-common.INDEX_META_DATA_CREATED_BY", @@ -2863,7 +2973,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 302 + "lineNumber": 303 }, "deprecated": false, "initialIsOpen": false @@ -2925,7 +3035,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 100 + "lineNumber": 101 }, "deprecated": false, "initialIsOpen": false @@ -2942,7 +3052,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 55 + "lineNumber": 56 }, "deprecated": false, "initialIsOpen": false @@ -2959,7 +3069,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 52 + "lineNumber": 53 }, "deprecated": false, "initialIsOpen": false @@ -2976,7 +3086,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 51 + "lineNumber": 52 }, "deprecated": false, "initialIsOpen": false @@ -2993,7 +3103,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 149 + "lineNumber": 150 }, "deprecated": false, "initialIsOpen": false @@ -3010,7 +3120,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 148 + "lineNumber": 149 }, "deprecated": false, "initialIsOpen": false @@ -3075,7 +3185,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 304 + "lineNumber": 305 }, "deprecated": false, "initialIsOpen": false @@ -3092,7 +3202,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 110 + "lineNumber": 111 }, "deprecated": false, "initialIsOpen": false @@ -3109,7 +3219,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 121 + "lineNumber": 122 }, "deprecated": false, "initialIsOpen": false @@ -3126,7 +3236,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 102 + "lineNumber": 103 }, "deprecated": false, "initialIsOpen": false @@ -3143,7 +3253,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 109 + "lineNumber": 110 }, "deprecated": false, "initialIsOpen": false @@ -3160,7 +3270,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 47 + "lineNumber": 48 }, "deprecated": false, "initialIsOpen": false @@ -3177,7 +3287,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 46 + "lineNumber": 47 }, "deprecated": false, "initialIsOpen": false @@ -3194,7 +3304,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 48 + "lineNumber": 49 }, "deprecated": false, "initialIsOpen": false @@ -3211,7 +3321,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 57 + "lineNumber": 58 }, "deprecated": false, "initialIsOpen": false @@ -3228,7 +3338,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 217 + "lineNumber": 218 }, "deprecated": false, "initialIsOpen": false @@ -3245,7 +3355,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 147 + "lineNumber": 148 }, "deprecated": false, "initialIsOpen": false @@ -3262,7 +3372,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 298 + "lineNumber": 299 }, "deprecated": false, "initialIsOpen": false @@ -3276,7 +3386,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 107 + "lineNumber": 108 }, "deprecated": false, "initialIsOpen": false @@ -3293,7 +3403,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 104 + "lineNumber": 105 }, "deprecated": false, "initialIsOpen": false @@ -3307,7 +3417,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 106 + "lineNumber": 107 }, "deprecated": false, "initialIsOpen": false @@ -3321,7 +3431,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 105 + "lineNumber": 106 }, "deprecated": false, "initialIsOpen": false @@ -3338,7 +3448,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 265 + "lineNumber": 266 }, "deprecated": false, "initialIsOpen": false @@ -3355,7 +3465,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 187 + "lineNumber": 188 }, "deprecated": false, "initialIsOpen": false @@ -3372,7 +3482,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 191 + "lineNumber": 192 }, "deprecated": false, "initialIsOpen": false @@ -3389,7 +3499,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 113 + "lineNumber": 114 }, "deprecated": false, "initialIsOpen": false @@ -3405,7 +3515,7 @@ "description": [], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 151 + "lineNumber": 152 }, "deprecated": false, "children": [ @@ -3421,7 +3531,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 152 + "lineNumber": 153 }, "deprecated": false }, @@ -3437,7 +3547,7 @@ ], "source": { "path": "x-pack/plugins/maps/common/constants.ts", - "lineNumber": 153 + "lineNumber": 154 }, "deprecated": false } diff --git a/api_docs/observability.json b/api_docs/observability.json index a80e8e44fe665e..fc645dd7e2e6b8 100644 --- a/api_docs/observability.json +++ b/api_docs/observability.json @@ -170,7 +170,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/components/shared/index.tsx", - "lineNumber": 34 + "lineNumber": 36 }, "deprecated": false, "children": [ @@ -186,7 +186,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/components/shared/index.tsx", - "lineNumber": 34 + "lineNumber": 36 }, "deprecated": false, "isRequired": true @@ -283,7 +283,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/components/shared/index.tsx", - "lineNumber": 14 + "lineNumber": 16 }, "deprecated": false, "children": [ @@ -299,7 +299,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/components/shared/index.tsx", - "lineNumber": 14 + "lineNumber": 16 }, "deprecated": false, "isRequired": true @@ -322,7 +322,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/components/shared/index.tsx", - "lineNumber": 24 + "lineNumber": 26 }, "deprecated": false, "children": [ @@ -338,7 +338,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/components/shared/index.tsx", - "lineNumber": 24 + "lineNumber": 26 }, "deprecated": false, "isRequired": true @@ -1050,7 +1050,7 @@ "label": "unsafe", "description": [], "signature": [ - "{ alertingExperience: { enabled: boolean; }; }" + "{ alertingExperience: { enabled: boolean; }; cases: { enabled: boolean; }; }" ], "source": { "path": "x-pack/plugins/observability/public/index.ts", @@ -2040,7 +2040,7 @@ "description": [], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 40 + "lineNumber": 42 }, "deprecated": false, "children": [ @@ -2062,7 +2062,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 41 + "lineNumber": 43 }, "deprecated": false }, @@ -2084,7 +2084,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 42 + "lineNumber": 44 }, "deprecated": false }, @@ -2107,7 +2107,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 43 + "lineNumber": 45 }, "deprecated": false } @@ -2123,7 +2123,7 @@ "description": [], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 46 + "lineNumber": 48 }, "deprecated": false, "children": [ @@ -2146,7 +2146,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 47 + "lineNumber": 49 }, "deprecated": false }, @@ -2168,7 +2168,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 48 + "lineNumber": 50 }, "deprecated": false }, @@ -2190,7 +2190,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 49 + "lineNumber": 51 }, "deprecated": false }, @@ -2212,7 +2212,7 @@ ], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 50 + "lineNumber": 52 }, "deprecated": false } @@ -2838,7 +2838,7 @@ "METRIC_TYPE" ], "source": { - "path": "node_modules/@kbn/analytics/target/types/metrics/index.d.ts", + "path": "node_modules/@kbn/analytics/target_types/metrics/index.d.ts", "lineNumber": 10 }, "deprecated": false, @@ -3142,11 +3142,15 @@ "section": "def-public.AlertTypeModel", "text": "AlertTypeModel" }, - " & { format: Formatter; }) => void; getFormatter: (typeId: string) => Formatter | undefined; }; isAlertingExperienceEnabled: () => boolean; }" + " & { format: Formatter; }) => void; getFormatter: (typeId: string) => Formatter | undefined; }; isAlertingExperienceEnabled: () => boolean; navigation: { registerSections: (sections$: ", + "Observable", + "<", + "NavigationSection", + "[]>) => void; }; }" ], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 38 + "lineNumber": 40 }, "deprecated": false, "lifecycle": "setup", @@ -3160,11 +3164,13 @@ "label": "ObservabilityPublicStart", "description": [], "signature": [ - "void" + "{ navigation: { PageTemplate: (pageTemplateProps: ", + "WrappedPageTemplateProps", + ") => JSX.Element; }; }" ], "source": { "path": "x-pack/plugins/observability/public/plugin.ts", - "lineNumber": 53 + "lineNumber": 55 }, "deprecated": false, "lifecycle": "start", @@ -3431,7 +3437,7 @@ ], "source": { "path": "x-pack/plugins/observability/server/utils/queries.ts", - "lineNumber": 25 + "lineNumber": 35 }, "deprecated": false, "children": [ @@ -3447,7 +3453,7 @@ ], "source": { "path": "x-pack/plugins/observability/server/utils/queries.ts", - "lineNumber": 25 + "lineNumber": 35 }, "deprecated": false, "isRequired": false @@ -3470,7 +3476,7 @@ ], "source": { "path": "x-pack/plugins/observability/server/utils/queries.ts", - "lineNumber": 11 + "lineNumber": 21 }, "deprecated": false, "children": [ @@ -3486,7 +3492,7 @@ ], "source": { "path": "x-pack/plugins/observability/server/utils/queries.ts", - "lineNumber": 11 + "lineNumber": 21 }, "deprecated": false, "isRequired": false @@ -3503,7 +3509,7 @@ ], "source": { "path": "x-pack/plugins/observability/server/utils/queries.ts", - "lineNumber": 11 + "lineNumber": 21 }, "deprecated": false, "isRequired": false @@ -3520,7 +3526,7 @@ ], "source": { "path": "x-pack/plugins/observability/server/utils/queries.ts", - "lineNumber": 11 + "lineNumber": 21 }, "deprecated": false, "isRequired": true @@ -3861,7 +3867,15 @@ "Type", "; end: ", "Type", - "; }>, ", + "; status: ", + "UnionC", + "<[", + "LiteralC", + "<\"all\">, ", + "LiteralC", + "<\"open\">, ", + "LiteralC", + "<\"closed\">]>; }>, ", "PartialC", "<{ kuery: ", "StringC", @@ -3928,11 +3942,11 @@ "label": "ObservabilityConfig", "description": [], "signature": [ - "{ readonly enabled: boolean; readonly annotations: Readonly<{} & { enabled: boolean; index: string; }>; readonly unsafe: Readonly<{} & { alertingExperience: Readonly<{} & { enabled: boolean; }>; }>; }" + "{ readonly enabled: boolean; readonly annotations: Readonly<{} & { enabled: boolean; index: string; }>; readonly unsafe: Readonly<{} & { cases: Readonly<{} & { enabled: boolean; }>; alertingExperience: Readonly<{} & { enabled: boolean; }>; }>; }" ], "source": { "path": "x-pack/plugins/observability/server/index.ts", - "lineNumber": 34 + "lineNumber": 35 }, "deprecated": false, "initialIsOpen": false @@ -3974,7 +3988,15 @@ "Type", "; end: ", "Type", - "; }>, ", + "; status: ", + "UnionC", + "<[", + "LiteralC", + "<\"all\">, ", + "LiteralC", + "<\"open\">, ", + "LiteralC", + "<\"closed\">]>; }>, ", "PartialC", "<{ kuery: ", "StringC", diff --git a/api_docs/presentation_util.json b/api_docs/presentation_util.json index ee045dfd830fbe..bde8373e4450e0 100644 --- a/api_docs/presentation_util.json +++ b/api_docs/presentation_util.json @@ -528,7 +528,9 @@ "type": "CompoundType", "tags": [], "label": "Component", - "description": [], + "description": [ + "A component deferred by `React.lazy`" + ], "signature": [ "React.ComponentType

" ], @@ -545,7 +547,9 @@ "type": "CompoundType", "tags": [], "label": "fallback", - "description": [], + "description": [ + "A fallback component to render while things load; default is `EuiLoadingSpinner`" + ], "signature": [ "React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)> | null" ], diff --git a/api_docs/reporting.json b/api_docs/reporting.json index e8cea57b24902c..b2529f510bc2ed 100644 --- a/api_docs/reporting.json +++ b/api_docs/reporting.json @@ -911,10 +911,10 @@ "children": [ { "parentPluginId": "reporting", - "id": "def-server.ReportingCore.getStartContract", + "id": "def-server.ReportingCore.getContract", "type": "Function", "tags": [], - "label": "getStartContract", + "label": "getContract", "description": [], "signature": [ "() => ", @@ -1010,7 +1010,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 92 + "lineNumber": 90 }, "deprecated": false, "children": [ @@ -1026,7 +1026,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 92 + "lineNumber": 90 }, "deprecated": false, "isRequired": true @@ -1048,7 +1048,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 106 + "lineNumber": 104 }, "deprecated": false, "children": [ @@ -1064,7 +1064,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 106 + "lineNumber": 104 }, "deprecated": false, "isRequired": true @@ -1084,7 +1084,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 119 + "lineNumber": 117 }, "deprecated": false, "children": [], @@ -1102,7 +1102,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 130 + "lineNumber": 128 }, "deprecated": false, "children": [], @@ -1120,7 +1120,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 137 + "lineNumber": 135 }, "deprecated": false, "children": [], @@ -1146,7 +1146,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 144 + "lineNumber": 142 }, "deprecated": false, "children": [ @@ -1168,7 +1168,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 144 + "lineNumber": 142 }, "deprecated": false, "isRequired": true @@ -1190,7 +1190,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 153 + "lineNumber": 151 }, "deprecated": false, "children": [], @@ -1215,7 +1215,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 187 + "lineNumber": 185 }, "deprecated": false, "children": [], @@ -1233,7 +1233,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 199 + "lineNumber": 197 }, "deprecated": false, "children": [], @@ -1253,7 +1253,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 206 + "lineNumber": 204 }, "deprecated": false, "children": [], @@ -1272,7 +1272,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 214 + "lineNumber": 212 }, "deprecated": false, "children": [], @@ -1302,7 +1302,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 218 + "lineNumber": 216 }, "deprecated": false, "children": [ @@ -1321,7 +1321,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 218 + "lineNumber": 216 }, "deprecated": false, "isRequired": true @@ -1343,7 +1343,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 222 + "lineNumber": 220 }, "deprecated": false, "children": [], @@ -1363,7 +1363,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 226 + "lineNumber": 224 }, "deprecated": false, "children": [], @@ -1383,7 +1383,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 236 + "lineNumber": 234 }, "deprecated": false, "children": [], @@ -1401,7 +1401,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 242 + "lineNumber": 240 }, "deprecated": false, "children": [], @@ -1420,7 +1420,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 250 + "lineNumber": 248 }, "deprecated": false, "children": [], @@ -1454,7 +1454,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 262 + "lineNumber": 260 }, "deprecated": false, "children": [ @@ -1478,7 +1478,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 262 + "lineNumber": 260 }, "deprecated": false, "isRequired": true @@ -1508,7 +1508,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 268 + "lineNumber": 266 }, "deprecated": false, "children": [ @@ -1531,7 +1531,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 268 + "lineNumber": 266 }, "deprecated": false, "isRequired": true @@ -1548,7 +1548,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 268 + "lineNumber": 266 }, "deprecated": false, "isRequired": true @@ -1578,7 +1578,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 282 + "lineNumber": 280 }, "deprecated": false, "children": [ @@ -1594,7 +1594,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 282 + "lineNumber": 280 }, "deprecated": false, "isRequired": true @@ -1611,7 +1611,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 282 + "lineNumber": 280 }, "deprecated": false, "isRequired": false @@ -1628,7 +1628,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 282 + "lineNumber": 280 }, "deprecated": false, "isRequired": true @@ -1666,7 +1666,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 302 + "lineNumber": 300 }, "deprecated": false, "children": [ @@ -1689,7 +1689,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 302 + "lineNumber": 300 }, "deprecated": false, "isRequired": true @@ -1706,7 +1706,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 302 + "lineNumber": 300 }, "deprecated": false, "isRequired": true @@ -1734,7 +1734,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 312 + "lineNumber": 310 }, "deprecated": false, "children": [], @@ -1760,7 +1760,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 317 + "lineNumber": 315 }, "deprecated": false, "children": [], @@ -1778,7 +1778,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 322 + "lineNumber": 320 }, "deprecated": false, "children": [ @@ -1794,7 +1794,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 322 + "lineNumber": 320 }, "deprecated": false, "isRequired": true @@ -1814,7 +1814,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 326 + "lineNumber": 324 }, "deprecated": false, "children": [ @@ -1830,7 +1830,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 326 + "lineNumber": 324 }, "deprecated": false, "isRequired": true @@ -1850,7 +1850,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 330 + "lineNumber": 328 }, "deprecated": false, "children": [], @@ -1918,7 +1918,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/plugin.ts", - "lineNumber": 25 + "lineNumber": 26 }, "deprecated": false, "children": [ @@ -1934,7 +1934,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/plugin.ts", - "lineNumber": 30 + "lineNumber": 31 }, "deprecated": false, "children": [ @@ -1959,7 +1959,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/plugin.ts", - "lineNumber": 30 + "lineNumber": 31 }, "deprecated": false, "isRequired": true @@ -2002,7 +2002,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/plugin.ts", - "lineNumber": 34 + "lineNumber": 35 }, "deprecated": false, "children": [ @@ -2025,7 +2025,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/plugin.ts", - "lineNumber": 34 + "lineNumber": 35 }, "deprecated": false, "isRequired": true @@ -2048,7 +2048,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/plugin.ts", - "lineNumber": 34 + "lineNumber": 35 }, "deprecated": false, "isRequired": true @@ -2091,7 +2091,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/plugin.ts", - "lineNumber": 87 + "lineNumber": 88 }, "deprecated": false, "children": [ @@ -2113,7 +2113,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/plugin.ts", - "lineNumber": 87 + "lineNumber": 88 }, "deprecated": false, "isRequired": true @@ -2136,7 +2136,7 @@ ], "source": { "path": "x-pack/plugins/reporting/server/plugin.ts", - "lineNumber": 87 + "lineNumber": 88 }, "deprecated": false, "isRequired": true diff --git a/api_docs/rule_registry.json b/api_docs/rule_registry.json index 63a50f8fbd42f5..cd1f4994c1da61 100644 --- a/api_docs/rule_registry.json +++ b/api_docs/rule_registry.json @@ -695,8 +695,8 @@ "{ readonly enabled: boolean; readonly index: string; readonly write: Readonly<{} & { enabled: boolean; }>; }" ], "source": { - "path": "x-pack/plugins/rule_registry/server/index.ts", - "lineNumber": 28 + "path": "x-pack/plugins/rule_registry/server/config.ts", + "lineNumber": 20 }, "deprecated": false, "initialIsOpen": false @@ -706,18 +706,49 @@ "setup": { "parentPluginId": "ruleRegistry", "id": "def-server.RuleRegistryPluginSetupContract", - "type": "Type", + "type": "Interface", "tags": [], "label": "RuleRegistryPluginSetupContract", "description": [], - "signature": [ - "RuleDataPluginService" - ], "source": { "path": "x-pack/plugins/rule_registry/server/plugin.ts", - "lineNumber": 12 + "lineNumber": 22 }, "deprecated": false, + "children": [ + { + "parentPluginId": "ruleRegistry", + "id": "def-server.RuleRegistryPluginSetupContract.ruleDataService", + "type": "Object", + "tags": [], + "label": "ruleDataService", + "description": [], + "signature": [ + "RuleDataPluginService" + ], + "source": { + "path": "x-pack/plugins/rule_registry/server/plugin.ts", + "lineNumber": 23 + }, + "deprecated": false + }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.RuleRegistryPluginSetupContract.eventLogService", + "type": "Object", + "tags": [], + "label": "eventLogService", + "description": [], + "signature": [ + "IEventLogService" + ], + "source": { + "path": "x-pack/plugins/rule_registry/server/plugin.ts", + "lineNumber": 24 + }, + "deprecated": false + } + ], "lifecycle": "setup", "initialIsOpen": true }, @@ -733,7 +764,7 @@ ], "source": { "path": "x-pack/plugins/rule_registry/server/plugin.ts", - "lineNumber": 13 + "lineNumber": 27 }, "deprecated": false, "lifecycle": "start", diff --git a/api_docs/spaces.json b/api_docs/spaces.json index 5225e8cebbeb5a..2f2d79c2379d58 100644 --- a/api_docs/spaces.json +++ b/api_docs/spaces.json @@ -1210,7 +1210,7 @@ "plugin": "reporting", "link": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 271 + "lineNumber": 269 } }, { @@ -2030,21 +2030,21 @@ "plugin": "reporting", "link": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 269 + "lineNumber": 267 } }, { "plugin": "reporting", "link": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 291 + "lineNumber": 289 } }, { "plugin": "reporting", "link": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 303 + "lineNumber": 301 } }, { diff --git a/api_docs/triggers_actions_ui.json b/api_docs/triggers_actions_ui.json index cade40cf39c738..de7a13b3080fe0 100644 --- a/api_docs/triggers_actions_ui.json +++ b/api_docs/triggers_actions_ui.json @@ -45,7 +45,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 77 + "lineNumber": 80 }, "deprecated": false, "children": [ @@ -61,7 +61,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 88 + "lineNumber": 91 }, "deprecated": false, "children": [], @@ -94,7 +94,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 93 + "lineNumber": 96 }, "deprecated": false, "children": [ @@ -117,7 +117,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 93 + "lineNumber": 96 }, "deprecated": false, "isRequired": true @@ -134,7 +134,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 93 + "lineNumber": 96 }, "deprecated": false, "isRequired": true @@ -161,7 +161,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 169 + "lineNumber": 172 }, "deprecated": false, "children": [], @@ -179,7 +179,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 203 + "lineNumber": 206 }, "deprecated": false, "children": [], @@ -228,44 +228,6 @@ ], "initialIsOpen": false }, - { - "parentPluginId": "triggersActionsUi", - "id": "def-public.AlertAdd", - "type": "Function", - "tags": [], - "label": "AlertAdd", - "description": [], - "signature": [ - "(props: ", - "AlertAddProps", - ">) => JSX.Element" - ], - "source": { - "path": "x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx", - "lineNumber": 11 - }, - "deprecated": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "triggersActionsUi", - "id": "def-public.props", - "type": "Uncategorized", - "tags": [], - "label": "props", - "description": [], - "signature": [ - "T" - ], - "source": { - "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/suspended_component_with_props.tsx", - "lineNumber": 16 - }, - "deprecated": false - } - ], - "initialIsOpen": false - }, { "parentPluginId": "triggersActionsUi", "id": "def-public.AlertConditions", @@ -346,44 +308,6 @@ ], "initialIsOpen": false }, - { - "parentPluginId": "triggersActionsUi", - "id": "def-public.AlertEdit", - "type": "Function", - "tags": [], - "label": "AlertEdit", - "description": [], - "signature": [ - "(props: ", - "AlertEditProps", - ">) => JSX.Element" - ], - "source": { - "path": "x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx", - "lineNumber": 17 - }, - "deprecated": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "triggersActionsUi", - "id": "def-public.props", - "type": "Uncategorized", - "tags": [], - "label": "props", - "description": [], - "signature": [ - "T" - ], - "source": { - "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/suspended_component_with_props.tsx", - "lineNumber": 16 - }, - "deprecated": false - } - ], - "initialIsOpen": false - }, { "parentPluginId": "triggersActionsUi", "id": "def-public.ConnectorAddFlyout", @@ -518,7 +442,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx", - "lineNumber": 63 + "lineNumber": 62 }, "deprecated": false, "children": [], @@ -545,7 +469,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx", - "lineNumber": 68 + "lineNumber": 67 }, "deprecated": false, "children": [], @@ -752,7 +676,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx", - "lineNumber": 64 + "lineNumber": 63 }, "deprecated": false, "children": [], @@ -779,7 +703,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx", - "lineNumber": 99 + "lineNumber": 98 }, "deprecated": false, "children": [], @@ -1459,7 +1383,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 234 + "lineNumber": 235 }, "deprecated": false, "children": [ @@ -1472,7 +1396,7 @@ "description": [], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 235 + "lineNumber": 236 }, "deprecated": false }, @@ -1485,7 +1409,7 @@ "description": [], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 236 + "lineNumber": 237 }, "deprecated": false }, @@ -1498,7 +1422,7 @@ "description": [], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 237 + "lineNumber": 238 }, "deprecated": false }, @@ -1522,7 +1446,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 238 + "lineNumber": 239 }, "deprecated": false }, @@ -1545,7 +1469,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 239 + "lineNumber": 240 }, "deprecated": false, "returnComment": [], @@ -1562,7 +1486,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 239 + "lineNumber": 240 }, "deprecated": false } @@ -1588,7 +1512,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 240 + "lineNumber": 241 }, "deprecated": false, "returnComment": [], @@ -1620,7 +1544,7 @@ "description": [], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 243 + "lineNumber": 244 }, "deprecated": false }, @@ -1636,7 +1560,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 244 + "lineNumber": 245 }, "deprecated": false } @@ -1662,7 +1586,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 212 + "lineNumber": 213 }, "deprecated": false, "children": [ @@ -1678,7 +1602,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 217 + "lineNumber": 218 }, "deprecated": false }, @@ -1691,7 +1615,7 @@ "description": [], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 218 + "lineNumber": 219 }, "deprecated": false }, @@ -1704,7 +1628,7 @@ "description": [], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 219 + "lineNumber": 220 }, "deprecated": false }, @@ -1720,7 +1644,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 220 + "lineNumber": 221 }, "deprecated": false }, @@ -1736,7 +1660,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 221 + "lineNumber": 222 }, "deprecated": false, "returnComment": [], @@ -1753,7 +1677,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 221 + "lineNumber": 222 }, "deprecated": false }, @@ -1769,7 +1693,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 221 + "lineNumber": 222 }, "deprecated": false } @@ -1795,7 +1719,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 222 + "lineNumber": 223 }, "deprecated": false, "returnComment": [], @@ -1812,7 +1736,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 223 + "lineNumber": 224 }, "deprecated": false }, @@ -1836,7 +1760,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 224 + "lineNumber": 225 }, "deprecated": false } @@ -1860,7 +1784,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 226 + "lineNumber": 227 }, "deprecated": false }, @@ -1873,7 +1797,7 @@ "description": [], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 227 + "lineNumber": 228 }, "deprecated": false }, @@ -1896,7 +1820,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 228 + "lineNumber": 229 }, "deprecated": false }, @@ -1912,7 +1836,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 229 + "lineNumber": 230 }, "deprecated": false }, @@ -1934,7 +1858,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 230 + "lineNumber": 231 }, "deprecated": false }, @@ -1956,7 +1880,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 231 + "lineNumber": 232 }, "deprecated": false } @@ -2098,7 +2022,7 @@ "description": [], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 247 + "lineNumber": 248 }, "deprecated": false, "children": [ @@ -2114,7 +2038,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 248 + "lineNumber": 249 }, "deprecated": false } @@ -2483,7 +2407,7 @@ "description": [], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 127 + "lineNumber": 128 }, "deprecated": false, "children": [ @@ -2499,7 +2423,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 128 + "lineNumber": 129 }, "deprecated": false } @@ -2531,7 +2455,7 @@ "description": [], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 79 + "lineNumber": 80 }, "deprecated": false, "initialIsOpen": false @@ -2581,7 +2505,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 161 + "lineNumber": 162 }, "deprecated": false, "initialIsOpen": false @@ -2637,7 +2561,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 64 + "lineNumber": 65 }, "deprecated": false, "initialIsOpen": false @@ -2654,7 +2578,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 179 + "lineNumber": 180 }, "deprecated": false, "initialIsOpen": false @@ -2695,7 +2619,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 40 + "lineNumber": 41 }, "deprecated": false, "initialIsOpen": false @@ -2753,7 +2677,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", - "lineNumber": 68 + "lineNumber": 69 }, "deprecated": false, "initialIsOpen": false @@ -3782,7 +3706,7 @@ "description": [], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 41 + "lineNumber": 44 }, "deprecated": false, "children": [ @@ -3801,7 +3725,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 42 + "lineNumber": 45 }, "deprecated": false }, @@ -3826,7 +3750,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 43 + "lineNumber": 46 }, "deprecated": false } @@ -3843,7 +3767,7 @@ "description": [], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 46 + "lineNumber": 49 }, "deprecated": false, "children": [ @@ -3862,7 +3786,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 47 + "lineNumber": 50 }, "deprecated": false }, @@ -3887,7 +3811,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 48 + "lineNumber": 51 }, "deprecated": false }, @@ -3907,7 +3831,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 49 + "lineNumber": 52 }, "deprecated": false, "returnComment": [], @@ -3948,7 +3872,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 50 + "lineNumber": 53 }, "deprecated": false } @@ -3970,7 +3894,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 52 + "lineNumber": 55 }, "deprecated": false, "returnComment": [], @@ -4005,7 +3929,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 53 + "lineNumber": 56 }, "deprecated": false } @@ -4027,7 +3951,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 55 + "lineNumber": 58 }, "deprecated": false, "returnComment": [], @@ -4060,7 +3984,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 56 + "lineNumber": 59 }, "deprecated": false } @@ -4082,7 +4006,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 58 + "lineNumber": 61 }, "deprecated": false, "returnComment": [], @@ -4115,7 +4039,7 @@ ], "source": { "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", - "lineNumber": 59 + "lineNumber": 62 }, "deprecated": false } diff --git a/api_docs/visualizations.json b/api_docs/visualizations.json index bea890f00e0ed2..0df428a158a3da 100644 --- a/api_docs/visualizations.json +++ b/api_docs/visualizations.json @@ -85,7 +85,15 @@ "label": "getSupportedTriggers", "description": [], "signature": [ - "(() => string[]) | undefined" + "((params?: ", + { + "pluginId": "visualizations", + "scope": "common", + "docId": "kibVisualizationsPluginApi", + "section": "def-common.VisParams", + "text": "VisParams" + }, + " | undefined) => string[]) | undefined" ], "source": { "path": "src/plugins/visualizations/public/vis_types/base_vis_type.ts", @@ -3327,7 +3335,15 @@ "\nIf given, it will return the supported triggers for this vis." ], "signature": [ - "(() => string[]) | undefined" + "((params?: ", + { + "pluginId": "visualizations", + "scope": "common", + "docId": "kibVisualizationsPluginApi", + "section": "def-common.VisParams", + "text": "VisParams" + }, + " | undefined) => string[]) | undefined" ], "source": { "path": "src/plugins/visualizations/public/vis_types/types.ts", diff --git a/dev_docs/tutorials/expressions.mdx b/dev_docs/tutorials/expressions.mdx new file mode 100644 index 00000000000000..f0fc1dc595cfa4 --- /dev/null +++ b/dev_docs/tutorials/expressions.mdx @@ -0,0 +1,129 @@ +--- +id: kibDevTutorialExpressions +slug: /kibana-dev-docs/tutorials/expressions +title: Kibana Expressions Service +summary: Kibana Expressions Service +date: 2021-06-01 +tags: ['kibana', 'onboarding', 'dev', 'architecture'] +--- + +## Expressions service + +Expression service exposes a registry of reusable functions primary used for fetching and transposing data and a registry of renderer functions that can render data into a DOM element. +Adding functions is easy and so is reusing them. An expression is a chain of functions with provided arguments, which given a single input translates to a single output. +Each expression is representable by a human friendly string which a user can type. + +### creating expressions + +Here is a very simple expression string: + + essql 'select column1, column2 from myindex' | mapColumn name=column3 fn='{ column1 + 3 }' | table + + +It consists of 3 functions: + + - essql which runs given sql query against elasticsearch and returns the results + - `mapColumn`, which computes a new column from existing ones; + - `table`, which prepares the data for rendering in a tabular format. + +The same expression could also be constructed in the code: + +```ts +import { buildExpression, buildExpressionFunction } from 'src/plugins/expressions'; + +const expression = buildExpression([ + buildExpressionFunction('essql', [ q: 'select column1, column2 from myindex' ]), + buildExpressionFunction('mapColumn', [ name: 'column3', expression: 'column1 + 3' ]), + buildExpressionFunction('table'), +] +``` + +Note: Consumers need to be aware which plugin registers specific functions with expressions function registry and import correct type definitions from there. + + + The `expressions` service is available on both server and client, with similar APIs. + + +### Running expressions + +Expression service exposes `execute` method which allows you to execute an expression: + +```ts +const executionContract = expressions.execute(expression, input); +const result = await executionContract.getData(); +``` + + + Check the full spec of execute function [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md) + + +In addition, on the browser side, there are two additional ways to run expressions and render the results. + +#### React expression renderer component + +This is the easiest way to get expressions rendered inside your application. + +```ts + +``` + + + Check the full spec of ReactExpressionRenderer component props [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md) + + +#### Expression loader + +If you are not using React, you can use the loader expression service provides to achieve the same: + +```ts +const handler = loader(domElement, expression, params); +``` + + + Check the full spec of expression loader params [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) + + +### Creating new expression functions + +Creating a new expression function is easy, just call `registerFunction` method on expressions service setup contract with your function definition: + +```ts +const functionDefinition = { + name: 'clog', + args: {}, + help: 'Outputs the context to the console', + fn: (input: unknown) => { + // eslint-disable-next-line no-console + console.log(input); + return input; + }, +}; + +expressions.registerFunction(functionDefinition); +``` + + + Check the full interface of ExpressionFuntionDefinition [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md) + + +### Creating new expression renderers + +Adding new renderers is just as easy as adding functions: + +```ts +const rendererDefinition = { + name: 'debug', + help: 'Outputs the context to the dom element', + render: (domElement, input, handlers) => { + // eslint-disable-next-line no-console + domElement.innerText = JSON.strinfigy(input); + handlers.done(); + }, +}; + +expressions.registerRenderer(rendererDefinition); +``` + + + Check the full interface of ExpressionRendererDefinition [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderdefinition.md) + diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 4e8bbf76eaacb0..dbfbe90ec9263e 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -80,6 +80,7 @@ yarn kbn watch-bazel - @kbn/eslint-plugin-eslint - @kbn/expect - @kbn/i18n +- @kbn/io-ts-utils - @kbn/legacy-logging - @kbn/logging - @kbn/mapbox-gl diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.deprecationtype.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.deprecationtype.md new file mode 100644 index 00000000000000..3a76bc60ee6306 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.deprecationtype.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [deprecationType](./kibana-plugin-core-server.deprecationsdetails.deprecationtype.md) + +## DeprecationsDetails.deprecationType property + +(optional) Used to identify between different deprecation types. Example use case: in Upgrade Assistant, we may want to allow the user to sort by deprecation type or show each type in a separate tab. + +Feel free to add new types if necessary. Predefined types are necessary to reduce having similar definitions with different keywords across kibana deprecations. + +Signature: + +```typescript +deprecationType?: 'config' | 'feature'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md index bb77e4247711f0..6e46ce0b8611f7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md @@ -15,6 +15,7 @@ export interface DeprecationsDetails | Property | Type | Description | | --- | --- | --- | | [correctiveActions](./kibana-plugin-core-server.deprecationsdetails.correctiveactions.md) | {
api?: {
path: string;
method: 'POST' | 'PUT';
body?: {
[key: string]: any;
};
};
manualSteps?: string[];
} | | +| [deprecationType](./kibana-plugin-core-server.deprecationsdetails.deprecationtype.md) | 'config' | 'feature' | (optional) Used to identify between different deprecation types. Example use case: in Upgrade Assistant, we may want to allow the user to sort by deprecation type or show each type in a separate tab.Feel free to add new types if necessary. Predefined types are necessary to reduce having similar definitions with different keywords across kibana deprecations. | | [documentationUrl](./kibana-plugin-core-server.deprecationsdetails.documentationurl.md) | string | | | [level](./kibana-plugin-core-server.deprecationsdetails.level.md) | 'warning' | 'critical' | 'fetch_error' | levels: - warning: will not break deployment upon upgrade - critical: needs to be addressed before upgrade. - fetch\_error: Deprecations service failed to grab the deprecation details for the domain. | | [message](./kibana-plugin-core-server.deprecationsdetails.message.md) | string | | diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc index e47858f58cd1a6..b908bd03b09927 100644 --- a/docs/user/alerting/rule-management.asciidoc +++ b/docs/user/alerting/rule-management.asciidoc @@ -57,15 +57,6 @@ These operations can also be performed in bulk by multi-selecting rules and clic [role="screenshot"] image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, enable/disable, and delete in bulk] -[float] -[[importing-and-exporting-rules]] -=== Importing and exporting rules - -To import and export rules, use the <>. -After the succesful import the proper banner will be displayed: -[role="screenshot"] -image::images/rules-imported-banner.png[Rules import banner, width=50%] - [float] === Required permissions diff --git a/docs/user/dashboard/images/lens_missing_values_strategy.png b/docs/user/dashboard/images/lens_missing_values_strategy.png new file mode 100644 index 00000000000000..d77c230b533f58 Binary files /dev/null and b/docs/user/dashboard/images/lens_missing_values_strategy.png differ diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 613432908df3d3..c5718b2a089bfc 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -4,8 +4,19 @@ To create visualizations with *Lens*, you drag and drop data fields onto the visualization builder, then *Lens* uses heuristics to apply each field. -[role="screenshot"] -image:dashboard/images/lens.png[Lens] +++++ + + +
+++++ [float] [[lens-required-choices]] @@ -179,6 +190,8 @@ A subset of *Lens* visualizations support value labels. [role="screenshot"] image::images/lens_value_labels_xychart_toggle.png[Lens Bar chart value labels menu] +NOTE: In bar charts, you are unable to move the label positions. + * *Pie*, *Donut*, and *Treemap* + [role="screenshot"] @@ -232,6 +245,9 @@ refer to <>. Sorting dimensions in visualizations is unsupported in *Lens*. +You can sort the dimensions for a single column in data tables: click the column header, then select the sorting criteria you want to use. +If you use the dimension as `Columns`, then all the columns that belong to the same dimension are sorted in the table. + [float] [[is-it-possible-to-use-saved-serches-in-lens]] ===== How do I visualize saved searches? @@ -243,3 +259,47 @@ Visualizing saved searches in unsupported in *Lens*. ===== How do I change the number of suggestions? Configuring the *Suggestions* that *Lens* automatically populates is unsupported. + +[float] +[[is-it-possible-to-use-different-indexpatterns-in-lens]] +===== Can I visualize multiple index patterns in a single visualization? + +You can create *Bar*, *Line* and *Area* charts from multiple index patterns. + +Each *Layer* in a visualization is associated with an index pattern and mutiple *Layers* can be combined together within the same visualization. Each *Layer* also has a chart switcher button in order to select the best type of visualization for the specific dataset. +You can also change the index pattern for a single *Layer*. + +[float] +[[why-my-field-x-is-missing-from-the-fields-list]] +===== Why is my field X missing from the fields list? + +*Lens* does not support the visualization of full-text fields, therefore it is not showing them in the data summary. + +[float] +[[how-to-handle-gaps-in-time-series-visualizations]] +===== How do I handle gaps in time series visualizations? + +*Lens* provides a set of features to handle missing values for *Area* and *Line* charts, which is useful for sparse data in time series data. + +To select a different way to represent missing values, open the *Visual options* menu, then select how to handle missing values. The default is to hide the missing values. ++ +[role="screenshot"] +image::images/lens_missing_values_strategy.png[Lens Missing values strategies menu] + +[float] +[[is-it-possible-to-change-the-scale-of-Y-axis]] +===== Is it possible to statically define the scale of the y-axis in a visualization? + +The ability to start the y-axis from another value than 0, or use a logarithmic scale, is unsupported in *Lens*. + +[float] +[[is-it-possible-to-have-pagination-for-datatable]] +===== Is it possible to have pagination in a data table? + +Pagination in a data table is unsupported in *Lens*. However, the <> supports pagination. + +[float] +[[is-it-possible-to-have-more-than-one-Y-axis-scale]] +===== Is it possible to have more than one y-axis scale in visualizations? + +*Lens* lets you pick, for each Y dimension, up to two distinct axis: *left* and *right*. Each axis can have a different scale. diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index 8a0dd5e4e2a2ba..38435708aaf997 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -20,6 +20,20 @@ Manage your indices and ingest pipelines, monitor the health of your Elastic Stack cluster, and control which users have access to which features. +++++ + + +
+++++ + *{kib} is for administrators, analysts, and business users.* As an admin, your role is to manage the Elastic Stack, from creating your deployment to getting {es} data into {kib}, and then @@ -43,9 +57,6 @@ If you’re not ready to use your own data, you can add a sample data set. The home page provides access to the *Enterprise Search*, *Observability*, and *Security* solutions, and everything you need to visualize and analyze your data. -[role="screenshot"] -image::images/home-page.png[Kibana home page] - 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*. diff --git a/package.json b/package.json index 1369b1d105aa45..e5b9ca1ef98cc6 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module", "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n/npm_module", "@kbn/interpreter": "link:packages/kbn-interpreter", - "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", + "@kbn/io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils/npm_module", "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging/npm_module", "@kbn/logging": "link:bazel-bin/packages/kbn-logging/npm_module", "@kbn/monaco": "link:bazel-bin/packages/kbn-monaco/npm_module", @@ -161,6 +161,7 @@ "@mapbox/mapbox-gl-draw": "1.3.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mapbox/vector-tile": "1.3.1", + "@reduxjs/toolkit": "^1.5.1", "@scant/router": "^0.1.1", "@slack/webhook": "^5.0.4", "@turf/along": "6.0.1", @@ -173,6 +174,7 @@ "@turf/distance": "6.0.1", "@turf/helpers": "6.0.1", "@turf/length": "^6.0.2", + "@types/redux-logger": "^3.0.8", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.4.0", @@ -227,6 +229,7 @@ "expiry-js": "0.1.7", "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.1", + "fflate": "^0.6.9", "file-saver": "^1.3.8", "file-type": "^10.9.0", "focus-trap-react": "^3.1.1", @@ -365,6 +368,7 @@ "redux": "^4.0.5", "redux-actions": "^2.6.5", "redux-devtools-extension": "^2.13.8", + "redux-logger": "^3.0.6", "redux-observable": "^1.2.0", "redux-saga": "^1.1.3", "redux-thunk": "^2.3.0", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index c885666f7a916e..de3498da1a6976 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -22,6 +22,7 @@ filegroup( "//packages/kbn-eslint-plugin-eslint:build", "//packages/kbn-expect:build", "//packages/kbn-i18n:build", + "//packages/kbn-io-ts-utils:build", "//packages/kbn-legacy-logging:build", "//packages/kbn-logging:build", "//packages/kbn-mapbox-gl:build", diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts index bcbb9cf3e7bbe0..c714165a0922c0 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts @@ -19,7 +19,7 @@ import { import { AnchorLink, ApiDeclaration, TypeKind } from '../types'; import { buildApiDecsForParameters } from './build_parameter_decs'; import { getSignature } from './get_signature'; -import { getJSDocReturnTagComment } from './js_doc_utils'; +import { getJSDocReturnTagComment, getJSDocs } from './js_doc_utils'; import { buildBasicApiDeclaration } from './build_basic_api_declaration'; /** @@ -66,7 +66,8 @@ export function getArrowFunctionDec( anchorLink, currentPluginId, log, - captureReferences + captureReferences, + getJSDocs(node) ), // need to override the signature - use the initializer, not the node. signature: getSignature(initializer, plugins, log), diff --git a/packages/kbn-docs-utils/src/api_docs/tests/api_doc_suite.test.ts b/packages/kbn-docs-utils/src/api_docs/tests/api_doc_suite.test.ts index cdf1e5b718ccaf..ff71b0efc79d1c 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/api_doc_suite.test.ts +++ b/packages/kbn-docs-utils/src/api_docs/tests/api_doc_suite.test.ts @@ -46,6 +46,8 @@ function fnIsCorrect(fn: ApiDeclaration | undefined) { expect(p1!.isRequired).toBe(true); expect(p1!.signature?.length).toBe(1); expect(linkCount(p1!.signature!)).toBe(0); + expect(p1?.description).toBeDefined(); + expect(p1?.description?.length).toBe(1); const p2 = fn?.children!.find((c) => c.label === 'b'); expect(p2).toBeDefined(); @@ -53,12 +55,15 @@ function fnIsCorrect(fn: ApiDeclaration | undefined) { expect(p2!.type).toBe(TypeKind.NumberKind); expect(p2!.signature?.length).toBe(1); expect(linkCount(p2!.signature!)).toBe(0); + expect(p2?.description?.length).toBe(1); const p3 = fn?.children!.find((c) => c.label === 'c'); expect(p3).toBeDefined(); expect(p3!.isRequired).toBe(true); expect(p3!.type).toBe(TypeKind.ArrayKind); expect(linkCount(p3!.signature!)).toBe(1); + expect(p3?.description).toBeDefined(); + expect(p3?.description?.length).toBe(1); const p4 = fn?.children!.find((c) => c.label === 'd'); expect(p4).toBeDefined(); @@ -66,6 +71,7 @@ function fnIsCorrect(fn: ApiDeclaration | undefined) { expect(p4!.type).toBe(TypeKind.CompoundTypeKind); expect(p4!.signature?.length).toBe(1); expect(linkCount(p4!.signature!)).toBe(1); + expect(p4?.description?.length).toBe(1); const p5 = fn?.children!.find((c) => c.label === 'e'); expect(p5).toBeDefined(); @@ -73,6 +79,7 @@ function fnIsCorrect(fn: ApiDeclaration | undefined) { expect(p5!.type).toBe(TypeKind.StringKind); expect(p5!.signature?.length).toBe(1); expect(linkCount(p5!.signature!)).toBe(0); + expect(p5?.description?.length).toBe(1); } beforeAll(() => { diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json index deb40d875b4428..7d8a90c3aad77a 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json @@ -165,7 +165,9 @@ "type": "CompoundType", "tags": [], "label": "a", - "description": [], + "description": [ + "im a string" + ], "signature": [ { "pluginId": "pluginA", @@ -292,7 +294,9 @@ "type": "string", "tags": [], "label": "a", - "description": [], + "description": [ + "The letter A" + ], "signature": [ "string" ], @@ -309,7 +313,9 @@ "type": "number", "tags": [], "label": "b", - "description": [], + "description": [ + "Feed me to the function" + ], "signature": [ "number | undefined" ], @@ -326,7 +332,9 @@ "type": "Array", "tags": [], "label": "c", - "description": [], + "description": [ + "So many params" + ], "signature": [ { "pluginId": "pluginA", @@ -350,7 +358,9 @@ "type": "CompoundType", "tags": [], "label": "d", - "description": [], + "description": [ + "a great param" + ], "signature": [ { "pluginId": "pluginA", @@ -373,7 +383,9 @@ "type": "string", "tags": [], "label": "e", - "description": [], + "description": [ + "Another comment" + ], "signature": [ "string | undefined" ], @@ -586,7 +598,7 @@ "section": "def-public.ImAType", "text": "ImAType" }, - ", e: string | undefined) => ", + ", e?: string | undefined) => ", { "pluginId": "pluginA", "scope": "public", diff --git a/packages/kbn-io-ts-utils/BUILD.bazel b/packages/kbn-io-ts-utils/BUILD.bazel new file mode 100644 index 00000000000000..6b26173fe8f369 --- /dev/null +++ b/packages/kbn-io-ts-utils/BUILD.bazel @@ -0,0 +1,85 @@ +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-io-ts-utils" +PKG_REQUIRE_NAME = "@kbn/io-ts-utils" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +SRC_DEPS = [ + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = DEPS + [":tsc"], + 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-io-ts-utils/package.json b/packages/kbn-io-ts-utils/package.json index 4d6f02d3f85a62..9d22277f27c014 100644 --- a/packages/kbn-io-ts-utils/package.json +++ b/packages/kbn-io-ts-utils/package.json @@ -4,10 +4,5 @@ "types": "./target/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true, - "scripts": { - "build": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" - } + "private": true } diff --git a/packages/kbn-io-ts-utils/tsconfig.json b/packages/kbn-io-ts-utils/tsconfig.json index 6c67518e210734..7b8f2552754992 100644 --- a/packages/kbn-io-ts-utils/tsconfig.json +++ b/packages/kbn-io-ts-utils/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "stripInternal": false, "declaration": true, diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index c28fd835919602..6ccf6269751b1e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -3,7 +3,7 @@ pageLoadAssetSize: alerting: 106936 apm: 64385 apmOss: 18996 - bfetch: 41874 + bfetch: 51874 canvas: 1066647 charts: 195358 cloud: 21076 diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/index.ts index 2c71afd9e066e2..3d3c41aed5a72a 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/index.ts @@ -31,6 +31,7 @@ export * from './read_exception_list_item_schema'; export * from './read_exception_list_schema'; export * from './read_list_item_schema'; export * from './read_list_schema'; +export * from './summary_exception_list_schema'; export * from './update_endpoint_list_item_schema'; export * from './update_exception_list_item_schema'; export * from './update_exception_list_item_validation'; diff --git a/src/plugins/vis_type_timeseries/common/get_last_value.js b/packages/kbn-securitysolution-io-ts-list-types/src/request/summary_exception_list_schema/index.mock.ts similarity index 55% rename from src/plugins/vis_type_timeseries/common/get_last_value.js rename to packages/kbn-securitysolution-io-ts-list-types/src/request/summary_exception_list_schema/index.mock.ts index 80adf7098f24dd..384f093d0884aa 100644 --- a/src/plugins/vis_type_timeseries/common/get_last_value.js +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/summary_exception_list_schema/index.mock.ts @@ -6,16 +6,12 @@ * Side Public License, v 1. */ -import { isArray, last } from 'lodash'; +import { ID, LIST_ID, NAMESPACE_TYPE } from '../../constants/index.mock'; -export const DEFAULT_VALUE = '-'; +import { SummaryExceptionListSchema } from '.'; -const extractValue = (data) => (data && data[1]) ?? null; - -export const getLastValue = (data) => { - if (!isArray(data)) { - return data ?? DEFAULT_VALUE; - } - - return extractValue(last(data)) ?? DEFAULT_VALUE; -}; +export const getSummaryExceptionListSchemaMock = (): SummaryExceptionListSchema => ({ + id: ID, + list_id: LIST_ID, + namespace_type: NAMESPACE_TYPE, +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/summary_exception_list_schema/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/summary_exception_list_schema/index.test.ts new file mode 100644 index 00000000000000..ade015b0d26bfa --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/summary_exception_list_schema/index.test.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { left } from 'fp-ts/lib/Either'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +import { getSummaryExceptionListSchemaMock } from './index.mock'; +import { SummaryExceptionListSchema, summaryExceptionListSchema } from '.'; + +describe('summary_exception_list_schema', () => { + test('it should validate a typical exception list request', () => { + const payload = getSummaryExceptionListSchemaMock(); + const decoded = summaryExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "id"', () => { + const payload = getSummaryExceptionListSchemaMock(); + delete payload.id; + const decoded = summaryExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "list_id"', () => { + const payload = getSummaryExceptionListSchemaMock(); + delete payload.list_id; + const decoded = summaryExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "namespace_type" but default to "single"', () => { + const payload = getSummaryExceptionListSchemaMock(); + delete payload.namespace_type; + const decoded = summaryExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getSummaryExceptionListSchemaMock()); + }); + + test('it should accept an undefined for "id", "list_id", "namespace_type" but default "namespace_type" to "single"', () => { + const payload = getSummaryExceptionListSchemaMock(); + delete payload.id; + delete payload.namespace_type; + delete payload.list_id; + const output = getSummaryExceptionListSchemaMock(); + delete output.id; + delete output.list_id; + const decoded = summaryExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(output); + }); + + test('it should accept an undefined for "id", "list_id"', () => { + const payload = getSummaryExceptionListSchemaMock(); + delete payload.id; + delete payload.list_id; + const decoded = summaryExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "id", "namespace_type" but default "namespace_type" to "single"', () => { + const payload = getSummaryExceptionListSchemaMock(); + delete payload.id; + delete payload.namespace_type; + const output = getSummaryExceptionListSchemaMock(); + delete output.id; + const decoded = summaryExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(output); + }); + + test('it should accept an undefined for "list_id", "namespace_type" but default "namespace_type" to "single"', () => { + const payload = getSummaryExceptionListSchemaMock(); + delete payload.namespace_type; + delete payload.list_id; + const output = getSummaryExceptionListSchemaMock(); + delete output.list_id; + const decoded = summaryExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(output); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: SummaryExceptionListSchema & { + extraKey?: string; + } = getSummaryExceptionListSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = summaryExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/summary_exception_list_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/summary_exception_list_schema/index.ts new file mode 100644 index 00000000000000..990091882df7bd --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/summary_exception_list_schema/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { NamespaceType } from '../../common/default_namespace'; +import { RequiredKeepUndefined } from '../../common/required_keep_undefined'; +import { id } from '../../common/id'; +import { list_id } from '../../common/list_id'; +import { namespace_type } from '../../common/namespace_type'; + +export const summaryExceptionListSchema = t.exact( + t.partial({ + id, + list_id, + namespace_type, // defaults to 'single' if not set during decode + }) +); + +export type SummaryExceptionListSchema = t.OutputOf; + +// This type is used after a decode since some things are defaults after a decode. +export type SummaryExceptionListSchemaDecoded = Omit< + RequiredKeepUndefined>, + 'namespace_type' +> & { + namespace_type: NamespaceType; +}; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_summary_schema/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_summary_schema/index.mock.ts new file mode 100644 index 00000000000000..6b7ce27d071948 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_summary_schema/index.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ExceptionListSummarySchema } from '.'; + +export const getListSummaryResponseMock = (): ExceptionListSummarySchema => ({ + windows: 0, + linux: 1, + macos: 2, + total: 3, +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_summary_schema/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_summary_schema/index.test.ts new file mode 100644 index 00000000000000..ea086f427451d6 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_summary_schema/index.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +import { getListSummaryResponseMock } from './index.mock'; +import { ExceptionListSummarySchema, exceptionListSummarySchema } from '.'; + +describe('list_summary_schema', () => { + test('it should validate a typical list summary response', () => { + const payload = getListSummaryResponseMock(); + const decoded = exceptionListSummarySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept an undefined for "windows"', () => { + const payload = getListSummaryResponseMock(); + // @ts-expect-error + delete payload.windows; + const decoded = exceptionListSummarySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "windows"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "linux"', () => { + const payload = getListSummaryResponseMock(); + // @ts-expect-error + delete payload.linux; + const decoded = exceptionListSummarySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "linux"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "macos"', () => { + const payload = getListSummaryResponseMock(); + // @ts-expect-error + delete payload.macos; + const decoded = exceptionListSummarySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "macos"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "total"', () => { + const payload = getListSummaryResponseMock(); + // @ts-expect-error + delete payload.total; + const decoded = exceptionListSummarySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "total"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ExceptionListSummarySchema & { + extraKey?: string; + } = getListSummaryResponseMock(); + payload.extraKey = 'some new value'; + const decoded = exceptionListSummarySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_summary_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_summary_schema/index.ts new file mode 100644 index 00000000000000..4c0cc8301dbf7e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_summary_schema/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; +import * as t from 'io-ts'; + +export const exceptionListSummarySchema = t.exact( + t.type({ + windows: PositiveInteger, + linux: PositiveInteger, + macos: PositiveInteger, + total: PositiveInteger, + }) +); + +export type ExceptionListSummarySchema = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/response/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/response/index.ts index 005e753ccf1b39..dc29bdf16ab48a 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/response/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/response/index.ts @@ -16,5 +16,6 @@ export * from './found_list_item_schema'; export * from './found_list_schema'; export * from './list_item_schema'; export * from './list_schema'; +export * from './exception_list_summary_schema'; export * from './list_item_index_exist_schema'; export * from './search_list_item_schema'; diff --git a/packages/kbn-server-route-repository/package.json b/packages/kbn-server-route-repository/package.json index ce1ca02d0c4f6b..4ae625d83a7003 100644 --- a/packages/kbn-server-route-repository/package.json +++ b/packages/kbn-server-route-repository/package.json @@ -9,8 +9,5 @@ "build": "../../node_modules/.bin/tsc", "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" - }, - "dependencies": { - "@kbn/io-ts-utils": "link:../kbn-io-ts-utils" } } diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index 4029ce28faf5bf..d3755ed7c5f293 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -44,6 +44,8 @@ export const Theme = require('./theme.ts'); export const Lodash = require('lodash'); export const LodashFp = require('lodash/fp'); +export const Fflate = require('fflate/esm/browser'); + // runtime deps which don't need to be copied across all bundles export const TsLib = require('tslib'); export const KbnAnalytics = require('@kbn/analytics'); diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 62ddb09d25add7..877bf3df6c039d 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -52,6 +52,7 @@ exports.externals = { '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.Theme.euiDarkVars', lodash: '__kbnSharedDeps__.Lodash', 'lodash/fp': '__kbnSharedDeps__.LodashFp', + fflate: '__kbnSharedDeps__.Fflate', /** * runtime deps which don't need to be copied across all bundles diff --git a/src/core/server/deprecations/deprecations_factory.mock.ts b/src/core/server/deprecations/deprecations_factory.mock.ts new file mode 100644 index 00000000000000..91ae4e6fa9af93 --- /dev/null +++ b/src/core/server/deprecations/deprecations_factory.mock.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 { PublicMethodsOf } from '@kbn/utility-types'; +import type { DeprecationsFactory } from './deprecations_factory'; +type DeprecationsFactoryContract = PublicMethodsOf; + +const createDeprecationsFactoryMock = () => { + const mocked: jest.Mocked = { + getRegistry: jest.fn(), + getDeprecations: jest.fn(), + getAllDeprecations: jest.fn(), + }; + + mocked.getDeprecations.mockResolvedValue([]); + mocked.getAllDeprecations.mockResolvedValue([]); + return mocked as jest.Mocked; +}; + +export const mockDeprecationsFactory = { + create: createDeprecationsFactoryMock, +}; diff --git a/src/core/server/deprecations/deprecations_factory.test.ts b/src/core/server/deprecations/deprecations_factory.test.ts index 469451b0020c02..187f3880f9998a 100644 --- a/src/core/server/deprecations/deprecations_factory.test.ts +++ b/src/core/server/deprecations/deprecations_factory.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { GetDeprecationsContext } from './types'; +import type { GetDeprecationsContext } from './types'; import { DeprecationsFactory } from './deprecations_factory'; import { loggerMock } from '../logging/logger.mock'; diff --git a/src/core/server/deprecations/deprecations_registry.mock.ts b/src/core/server/deprecations/deprecations_registry.mock.ts new file mode 100644 index 00000000000000..bb178c3935cdcf --- /dev/null +++ b/src/core/server/deprecations/deprecations_registry.mock.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { DeprecationsRegistry } from './deprecations_registry'; +import type { GetDeprecationsContext } from './types'; +import { elasticsearchClientMock } from '../elasticsearch/client/mocks'; +import { savedObjectsClientMock } from '../saved_objects/service/saved_objects_client.mock'; +type DeprecationsRegistryContract = PublicMethodsOf; + +const createDeprecationsRegistryMock = () => { + const mocked: jest.Mocked = { + registerDeprecations: jest.fn(), + getDeprecations: jest.fn(), + }; + + return mocked as jest.Mocked; +}; + +const createGetDeprecationsContextMock = () => { + const mocked: jest.Mocked = { + esClient: elasticsearchClientMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + + return mocked; +}; + +export const mockDeprecationsRegistry = { + create: createDeprecationsRegistryMock, + createGetDeprecationsContext: createGetDeprecationsContextMock, +}; diff --git a/src/core/server/deprecations/deprecations_registry.test.ts b/src/core/server/deprecations/deprecations_registry.test.ts index 507677a5318618..82b09beaa5123e 100644 --- a/src/core/server/deprecations/deprecations_registry.test.ts +++ b/src/core/server/deprecations/deprecations_registry.test.ts @@ -7,7 +7,7 @@ */ /* eslint-disable dot-notation */ -import { RegisterDeprecationsConfig, GetDeprecationsContext } from './types'; +import type { RegisterDeprecationsConfig, GetDeprecationsContext } from './types'; import { DeprecationsRegistry } from './deprecations_registry'; describe('DeprecationsRegistry', () => { diff --git a/src/core/server/deprecations/deprecations_registry.ts b/src/core/server/deprecations/deprecations_registry.ts index f92d807514b821..cc05473923ac88 100644 --- a/src/core/server/deprecations/deprecations_registry.ts +++ b/src/core/server/deprecations/deprecations_registry.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -import { DeprecationsDetails, RegisterDeprecationsConfig, GetDeprecationsContext } from './types'; +import type { + DeprecationsDetails, + RegisterDeprecationsConfig, + GetDeprecationsContext, +} from './types'; export class DeprecationsRegistry { private readonly deprecationContexts: RegisterDeprecationsConfig[] = []; diff --git a/src/core/server/deprecations/deprecations_service.test.ts b/src/core/server/deprecations/deprecations_service.test.ts new file mode 100644 index 00000000000000..d1ed7a83402cb3 --- /dev/null +++ b/src/core/server/deprecations/deprecations_service.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +/* eslint-disable dot-notation */ +import { DeprecationsService } from './deprecations_service'; +import { httpServiceMock } from '../http/http_service.mock'; +import { mockRouter } from '../http/router/router.mock'; +import { mockCoreContext } from '../core_context.mock'; +import { mockDeprecationsFactory } from './deprecations_factory.mock'; +import { mockDeprecationsRegistry } from './deprecations_registry.mock'; + +describe('DeprecationsService', () => { + const coreContext = mockCoreContext.create(); + beforeEach(() => jest.clearAllMocks()); + + describe('#setup', () => { + const http = httpServiceMock.createInternalSetupContract(); + const router = mockRouter.create(); + http.createRouter.mockReturnValue(router); + const deprecationsCoreSetupDeps = { http }; + + it('registers routes', () => { + const deprecationsService = new DeprecationsService(coreContext); + deprecationsService.setup(deprecationsCoreSetupDeps); + // Registers correct base api path + expect(http.createRouter).toBeCalledWith('/api/deprecations'); + // registers get route '/' + expect(router.get).toHaveBeenCalledTimes(1); + expect(router.get).toHaveBeenCalledWith({ path: '/', validate: false }, expect.any(Function)); + }); + + it('calls registerConfigDeprecationsInfo', () => { + const deprecationsService = new DeprecationsService(coreContext); + const mockRegisterConfigDeprecationsInfo = jest.fn(); + deprecationsService['registerConfigDeprecationsInfo'] = mockRegisterConfigDeprecationsInfo; + deprecationsService.setup(deprecationsCoreSetupDeps); + expect(mockRegisterConfigDeprecationsInfo).toBeCalledTimes(1); + }); + }); + + describe('#registerConfigDeprecationsInfo', () => { + const deprecationsFactory = mockDeprecationsFactory.create(); + const deprecationsRegistry = mockDeprecationsRegistry.create(); + const getDeprecationsContext = mockDeprecationsRegistry.createGetDeprecationsContext(); + + it('registers config deprecations', () => { + const deprecationsService = new DeprecationsService(coreContext); + coreContext.configService.getHandledDeprecatedConfigs.mockReturnValue([ + [ + 'testDomain', + [ + { + message: 'testMessage', + documentationUrl: 'testDocUrl', + correctiveActions: { + manualSteps: [ + 'Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.', + 'Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role.', + ], + }, + }, + ], + ], + ]); + + deprecationsFactory.getRegistry.mockReturnValue(deprecationsRegistry); + deprecationsService['registerConfigDeprecationsInfo'](deprecationsFactory); + + expect(coreContext.configService.getHandledDeprecatedConfigs).toBeCalledTimes(1); + expect(deprecationsFactory.getRegistry).toBeCalledTimes(1); + expect(deprecationsFactory.getRegistry).toBeCalledWith('testDomain'); + expect(deprecationsRegistry.registerDeprecations).toBeCalledTimes(1); + const configDeprecations = deprecationsRegistry.registerDeprecations.mock.calls[0][0].getDeprecations( + getDeprecationsContext + ); + expect(configDeprecations).toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.", + "Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role.", + ], + }, + "deprecationType": "config", + "documentationUrl": "testDocUrl", + "level": "critical", + "message": "testMessage", + }, + ] + `); + }); + }); +}); diff --git a/src/core/server/deprecations/deprecations_service.ts b/src/core/server/deprecations/deprecations_service.ts index 8eca1ba5790c55..205dd964468c1a 100644 --- a/src/core/server/deprecations/deprecations_service.ts +++ b/src/core/server/deprecations/deprecations_service.ts @@ -11,8 +11,6 @@ import { RegisterDeprecationsConfig } from './types'; import { registerRoutes } from './routes'; import { CoreContext } from '../core_context'; -import { CoreUsageDataSetup } from '../core_usage_data'; -import { InternalElasticsearchServiceSetup } from '../elasticsearch'; import { CoreService } from '../../types'; import { InternalHttpServiceSetup } from '../http'; import { Logger } from '../logging'; @@ -112,8 +110,6 @@ export interface InternalDeprecationsServiceSetup { /** @internal */ export interface DeprecationsSetupDeps { http: InternalHttpServiceSetup; - elasticsearch: InternalElasticsearchServiceSetup; - coreUsageData: CoreUsageDataSetup; } /** @internal */ @@ -156,6 +152,7 @@ export class DeprecationsService implements CoreService { return { level: 'critical', + deprecationType: 'config', message, correctiveActions: correctiveActions ?? {}, documentationUrl, diff --git a/src/core/server/deprecations/types.ts b/src/core/server/deprecations/types.ts index 31734b51b46bd2..50c947591fdf4a 100644 --- a/src/core/server/deprecations/types.ts +++ b/src/core/server/deprecations/types.ts @@ -25,6 +25,16 @@ export interface DeprecationsDetails { * - fetch_error: Deprecations service failed to grab the deprecation details for the domain. */ level: 'warning' | 'critical' | 'fetch_error'; + /** + * (optional) Used to identify between different deprecation types. + * Example use case: in Upgrade Assistant, we may want to allow the user to sort by + * deprecation type or show each type in a separate tab. + * + * Feel free to add new types if necessary. + * Predefined types are necessary to reduce having similar definitions with different keywords + * across kibana deprecations. + */ + deprecationType?: 'config' | 'feature'; /* (optional) link to the documentation for more details on the deprecation. */ documentationUrl?: string; /* corrective action needed to fix this deprecation. */ diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts index df74a4e1282e43..05da335d708840 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts @@ -37,7 +37,7 @@ describe('actions', () => { describe('fetchIndices', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.fetchIndices(client, ['my_index']); + const task = Actions.fetchIndices({ client, indices: ['my_index'] }); try { await task(); } catch (e) { @@ -49,7 +49,7 @@ describe('actions', () => { describe('setWriteBlock', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.setWriteBlock(client, 'my_index'); + const task = Actions.setWriteBlock({ client, index: 'my_index' }); try { await task(); } catch (e) { @@ -58,7 +58,10 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); it('re-throws non retry-able errors', async () => { - const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + const task = Actions.setWriteBlock({ + client: clientWithNonRetryableError, + index: 'my_index', + }); await task(); expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); }); @@ -66,7 +69,11 @@ describe('actions', () => { describe('cloneIndex', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.cloneIndex(client, 'my_source_index', 'my_target_index'); + const task = Actions.cloneIndex({ + client, + source: 'my_source_index', + target: 'my_target_index', + }); try { await task(); } catch (e) { @@ -75,7 +82,10 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); it('re-throws non retry-able errors', async () => { - const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + const task = Actions.setWriteBlock({ + client: clientWithNonRetryableError, + index: 'my_index', + }); await task(); expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); }); @@ -95,7 +105,7 @@ describe('actions', () => { describe('openPit', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.openPit(client, 'my_index'); + const task = Actions.openPit({ client, index: 'my_index' }); try { await task(); } catch (e) { @@ -107,7 +117,12 @@ describe('actions', () => { describe('readWithPit', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.readWithPit(client, 'pitId', { match_all: {} }, 10_000); + const task = Actions.readWithPit({ + client, + pitId: 'pitId', + query: { match_all: {} }, + batchSize: 10_000, + }); try { await task(); } catch (e) { @@ -119,7 +134,7 @@ describe('actions', () => { describe('closePit', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.closePit(client, 'pitId'); + const task = Actions.closePit({ client, pitId: 'pitId' }); try { await task(); } catch (e) { @@ -131,14 +146,14 @@ describe('actions', () => { describe('reindex', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.reindex( + const task = Actions.reindex({ client, - 'my_source_index', - 'my_target_index', - Option.none, - false, - {} - ); + sourceIndex: 'my_source_index', + targetIndex: 'my_target_index', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: {}, + }); try { await task(); } catch (e) { @@ -150,7 +165,7 @@ describe('actions', () => { describe('waitForReindexTask', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.waitForReindexTask(client, 'my task id', '60s'); + const task = Actions.waitForReindexTask({ client, taskId: 'my task id', timeout: '60s' }); try { await task(); } catch (e) { @@ -160,7 +175,10 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); it('re-throws non retry-able errors', async () => { - const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + const task = Actions.setWriteBlock({ + client: clientWithNonRetryableError, + index: 'my_index', + }); await task(); expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); }); @@ -168,7 +186,11 @@ describe('actions', () => { describe('waitForPickupUpdatedMappingsTask', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.waitForPickupUpdatedMappingsTask(client, 'my task id', '60s'); + const task = Actions.waitForPickupUpdatedMappingsTask({ + client, + taskId: 'my task id', + timeout: '60s', + }); try { await task(); } catch (e) { @@ -178,7 +200,10 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); it('re-throws non retry-able errors', async () => { - const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + const task = Actions.setWriteBlock({ + client: clientWithNonRetryableError, + index: 'my_index', + }); await task(); expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); }); @@ -186,7 +211,7 @@ describe('actions', () => { describe('updateAliases', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.updateAliases(client, []); + const task = Actions.updateAliases({ client, aliasActions: [] }); try { await task(); } catch (e) { @@ -196,7 +221,10 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); it('re-throws non retry-able errors', async () => { - const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + const task = Actions.setWriteBlock({ + client: clientWithNonRetryableError, + index: 'my_index', + }); await task(); expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); }); @@ -204,7 +232,11 @@ describe('actions', () => { describe('createIndex', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.createIndex(client, 'new_index', { properties: {} }); + const task = Actions.createIndex({ + client, + indexName: 'new_index', + mappings: { properties: {} }, + }); try { await task(); } catch (e) { @@ -214,7 +246,10 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); it('re-throws non retry-able errors', async () => { - const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + const task = Actions.setWriteBlock({ + client: clientWithNonRetryableError, + index: 'my_index', + }); await task(); expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); }); @@ -222,7 +257,11 @@ describe('actions', () => { describe('updateAndPickupMappings', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.updateAndPickupMappings(client, 'new_index', { properties: {} }); + const task = Actions.updateAndPickupMappings({ + client, + index: 'new_index', + mappings: { properties: {} }, + }); try { await task(); } catch (e) { @@ -276,7 +315,12 @@ describe('actions', () => { describe('bulkOverwriteTransformedDocuments', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.bulkOverwriteTransformedDocuments(client, 'new_index', [], 'wait_for'); + const task = Actions.bulkOverwriteTransformedDocuments({ + client, + index: 'new_index', + transformedDocs: [], + refresh: 'wait_for', + }); try { await task(); } catch (e) { @@ -289,7 +333,7 @@ describe('actions', () => { describe('refreshIndex', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.refreshIndex(client, 'target_index'); + const task = Actions.refreshIndex({ client, targetIndex: 'target_index' }); try { await task(); } catch (e) { diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index c2e0476960c3b3..905d64947298ec 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -68,20 +68,26 @@ export type FetchIndexResponse = Record< { aliases: Record; mappings: IndexMapping; settings: unknown } >; +/** @internal */ +export interface FetchIndicesParams { + client: ElasticsearchClient; + indices: string[]; +} + /** * Fetches information about the given indices including aliases, mappings and * settings. */ -export const fetchIndices = ( - client: ElasticsearchClient, - indicesToFetch: string[] -): TaskEither.TaskEither => +export const fetchIndices = ({ + client, + indices, +}: FetchIndicesParams): TaskEither.TaskEither => // @ts-expect-error @elastic/elasticsearch IndexState.alias and IndexState.mappings should be required () => { return client.indices .get( { - index: indicesToFetch, + index: indices, ignore_unavailable: true, // Don't return an error for missing indices. Note this *will* include closed indices, the docs are misleading https://github.com/elastic/elasticsearch/issues/63607 }, { ignore: [404], maxRetries: 0 } @@ -96,6 +102,12 @@ export interface IndexNotFound { type: 'index_not_found_exception'; index: string; } + +/** @internal */ +export interface SetWriteBlockParams { + client: ElasticsearchClient; + index: string; +} /** * Sets a write block in place for the given index. If the response includes * `acknowledged: true` all in-progress writes have drained and no further @@ -105,10 +117,10 @@ export interface IndexNotFound { * include `shards_acknowledged: true` but once the block is in place, * subsequent calls return `shards_acknowledged: false` */ -export const setWriteBlock = ( - client: ElasticsearchClient, - index: string -): TaskEither.TaskEither< +export const setWriteBlock = ({ + client, + index, +}: SetWriteBlockParams): TaskEither.TaskEither< IndexNotFound | RetryableEsClientError, 'set_write_block_succeeded' > => () => { @@ -145,13 +157,21 @@ export const setWriteBlock = ( ); }; +/** @internal */ +export interface RemoveWriteBlockParams { + client: ElasticsearchClient; + index: string; +} /** * Removes a write block from an index */ -export const removeWriteBlock = ( - client: ElasticsearchClient, - index: string -): TaskEither.TaskEither => () => { +export const removeWriteBlock = ({ + client, + index, +}: RemoveWriteBlockParams): TaskEither.TaskEither< + RetryableEsClientError, + 'remove_write_block_succeeded' +> => () => { return client.indices .putSettings<{ acknowledged: boolean; @@ -182,6 +202,12 @@ export const removeWriteBlock = ( .catch(catchRetryableEsClientErrors); }; +/** @internal */ +export interface WaitForIndexStatusYellowParams { + client: ElasticsearchClient; + index: string; + timeout?: string; +} /** * A yellow index status means the index's primary shard is allocated and the * index is ready for searching/indexing documents, but ES wasn't able to @@ -193,11 +219,11 @@ export const removeWriteBlock = ( * yellow at any point in the future. So ultimately data-redundancy is up to * users to maintain. */ -export const waitForIndexStatusYellow = ( - client: ElasticsearchClient, - index: string, - timeout = DEFAULT_TIMEOUT -): TaskEither.TaskEither => () => { +export const waitForIndexStatusYellow = ({ + client, + index, + timeout = DEFAULT_TIMEOUT, +}: WaitForIndexStatusYellowParams): TaskEither.TaskEither => () => { return client.cluster .health({ index, wait_for_status: 'yellow', timeout }) .then(() => { @@ -208,6 +234,14 @@ export const waitForIndexStatusYellow = ( export type CloneIndexResponse = AcknowledgeResponse; +/** @internal */ +export interface CloneIndexParams { + client: ElasticsearchClient; + source: string; + target: string; + /** only used for testing */ + timeout?: string; +} /** * Makes a clone of the source index into the target. * @@ -218,13 +252,15 @@ export type CloneIndexResponse = AcknowledgeResponse; * - the first call will wait up to 120s for the cluster state and all shards * to be updated. */ -export const cloneIndex = ( - client: ElasticsearchClient, - source: string, - target: string, - /** only used for testing */ - timeout = DEFAULT_TIMEOUT -): TaskEither.TaskEither => { +export const cloneIndex = ({ + client, + source, + target, + timeout = DEFAULT_TIMEOUT, +}: CloneIndexParams): TaskEither.TaskEither< + RetryableEsClientError | IndexNotFound, + CloneIndexResponse +> => { const cloneTask: TaskEither.TaskEither< RetryableEsClientError | IndexNotFound, AcknowledgeResponse @@ -302,7 +338,7 @@ export const cloneIndex = ( } else { // Otherwise, wait until the target index has a 'green' status. return pipe( - waitForIndexStatusYellow(client, target, timeout), + waitForIndexStatusYellow({ client, index: target, timeout }), TaskEither.map((value) => { /** When the index status is 'green' we know that all shards were started */ return { acknowledged: true, shardsAcknowledged: true }; @@ -352,16 +388,22 @@ const catchWaitForTaskCompletionTimeout = ( } }; +/** @internal */ +export interface WaitForTaskParams { + client: ElasticsearchClient; + taskId: string; + timeout: string; +} /** * Blocks for up to 60s or until a task completes. * * TODO: delete completed tasks */ -const waitForTask = ( - client: ElasticsearchClient, - taskId: string, - timeout: string -): TaskEither.TaskEither< +const waitForTask = ({ + client, + taskId, + timeout, +}: WaitForTaskParams): TaskEither.TaskEither< RetryableEsClientError | WaitForTaskCompletionTimeout, WaitForTaskResponse > => () => { @@ -433,16 +475,21 @@ export interface OpenPitResponse { pitId: string; } +/** @internal */ +export interface OpenPitParams { + client: ElasticsearchClient; + index: string; +} // how long ES should keep PIT alive const pitKeepAlive = '10m'; /* * Creates a lightweight view of data when the request has been initiated. * See https://www.elastic.co/guide/en/elasticsearch/reference/current/point-in-time-api.html * */ -export const openPit = ( - client: ElasticsearchClient, - index: string -): TaskEither.TaskEither => () => { +export const openPit = ({ + client, + index, +}: OpenPitParams): TaskEither.TaskEither => () => { return client .openPointInTime({ index, @@ -459,17 +506,28 @@ export interface ReadWithPit { readonly totalHits: number | undefined; } +/** @internal */ + +export interface ReadWithPitParams { + client: ElasticsearchClient; + pitId: string; + query: estypes.QueryContainer; + batchSize: number; + searchAfter?: number[]; + seqNoPrimaryTerm?: boolean; +} + /* * Requests documents from the index using PIT mechanism. * */ -export const readWithPit = ( - client: ElasticsearchClient, - pitId: string, - query: estypes.QueryContainer, - batchSize: number, - searchAfter?: number[], - seqNoPrimaryTerm?: boolean -): TaskEither.TaskEither => () => { +export const readWithPit = ({ + client, + pitId, + query, + batchSize, + searchAfter, + seqNoPrimaryTerm, +}: ReadWithPitParams): TaskEither.TaskEither => () => { return client .search({ seq_no_primary_term: seqNoPrimaryTerm, @@ -516,14 +574,19 @@ export const readWithPit = ( .catch(catchRetryableEsClientErrors); }; +/** @internal */ +export interface ClosePitParams { + client: ElasticsearchClient; + pitId: string; +} /* * Closes PIT. * See https://www.elastic.co/guide/en/elasticsearch/reference/current/point-in-time-api.html * */ -export const closePit = ( - client: ElasticsearchClient, - pitId: string -): TaskEither.TaskEither => () => { +export const closePit = ({ + client, + pitId, +}: ClosePitParams): TaskEither.TaskEither => () => { return client .closePointInTime({ body: { id: pitId }, @@ -537,27 +600,42 @@ export const closePit = ( .catch(catchRetryableEsClientErrors); }; +/** @internal */ +export interface TransformDocsParams { + transformRawDocs: TransformRawDocs; + outdatedDocuments: SavedObjectsRawDoc[]; +} /* * Transform outdated docs * */ -export const transformDocs = ( - transformRawDocs: TransformRawDocs, - outdatedDocuments: SavedObjectsRawDoc[] -): TaskEither.TaskEither => - transformRawDocs(outdatedDocuments); +export const transformDocs = ({ + transformRawDocs, + outdatedDocuments, +}: TransformDocsParams): TaskEither.TaskEither< + DocumentsTransformFailed, + DocumentsTransformSuccess +> => transformRawDocs(outdatedDocuments); /** @internal */ export interface ReindexResponse { taskId: string; } +/** @internal */ +export interface RefreshIndexParams { + client: ElasticsearchClient; + targetIndex: string; +} /** * Wait for Elasticsearch to reindex all the changes. */ -export const refreshIndex = ( - client: ElasticsearchClient, - targetIndex: string -): TaskEither.TaskEither => () => { +export const refreshIndex = ({ + client, + targetIndex, +}: RefreshIndexParams): TaskEither.TaskEither< + RetryableEsClientError, + { refreshed: boolean } +> => () => { return client.indices .refresh({ index: targetIndex, @@ -567,6 +645,19 @@ export const refreshIndex = ( }) .catch(catchRetryableEsClientErrors); }; +/** @internal */ +export interface ReindexParams { + client: ElasticsearchClient; + sourceIndex: string; + targetIndex: string; + reindexScript: Option.Option; + requireAlias: boolean; + /* When reindexing we use a source query to exclude saved objects types which + * are no longer used. These saved objects will still be kept in the outdated + * index for backup purposes, but won't be available in the upgraded index. + */ + unusedTypesQuery: estypes.QueryContainer; +} /** * Reindex documents from the `sourceIndex` into the `targetIndex`. Returns a * task ID which can be tracked for progress. @@ -575,18 +666,14 @@ export const refreshIndex = ( * this in parallel. By using `op_type: 'create', conflicts: 'proceed'` there * will be only one write per reindexed document. */ -export const reindex = ( - client: ElasticsearchClient, - sourceIndex: string, - targetIndex: string, - reindexScript: Option.Option, - requireAlias: boolean, - /* When reindexing we use a source query to exclude saved objects types which - * are no longer used. These saved objects will still be kept in the outdated - * index for backup purposes, but won't be available in the upgraded index. - */ - unusedTypesQuery: estypes.QueryContainer -): TaskEither.TaskEither => () => { +export const reindex = ({ + client, + sourceIndex, + targetIndex, + reindexScript, + requireAlias, + unusedTypesQuery, +}: ReindexParams): TaskEither.TaskEither => () => { return client .reindex({ // Require targetIndex to be an alias. Prevents a new index from being @@ -688,11 +775,18 @@ export const waitForReindexTask = flow( ) ); -export const verifyReindex = ( - client: ElasticsearchClient, - sourceIndex: string, - targetIndex: string -): TaskEither.TaskEither< +/** @internal */ +export interface VerifyReindexParams { + client: ElasticsearchClient; + sourceIndex: string; + targetIndex: string; +} + +export const verifyReindex = ({ + client, + sourceIndex, + targetIndex, +}: VerifyReindexParams): TaskEither.TaskEither< RetryableEsClientError | { type: 'verify_reindex_failed' }, 'verify_reindex_succeeded' > => () => { @@ -762,13 +856,18 @@ export type AliasAction = | { remove: { index: string; alias: string; must_exist: boolean } } | { add: { index: string; alias: string } }; +/** @internal */ +export interface UpdateAliasesParams { + client: ElasticsearchClient; + aliasActions: AliasAction[]; +} /** * Calls the Update index alias API `_alias` with the provided alias actions. */ -export const updateAliases = ( - client: ElasticsearchClient, - aliasActions: AliasAction[] -): TaskEither.TaskEither< +export const updateAliases = ({ + client, + aliasActions, +}: UpdateAliasesParams): TaskEither.TaskEither< IndexNotFound | AliasNotFound | RemoveIndexNotAConcreteIndex | RetryableEsClientError, 'update_aliases_succeeded' > => () => { @@ -836,6 +935,14 @@ function aliasArrayToRecord(aliases: string[]): Record { } return result; } + +/** @internal */ +export interface CreateIndexParams { + client: ElasticsearchClient; + indexName: string; + mappings: IndexMapping; + aliases?: string[]; +} /** * Creates an index with the given mappings * @@ -846,12 +953,12 @@ function aliasArrayToRecord(aliases: string[]): Record { * - the first call will wait up to 120s for the cluster state and all shards * to be updated. */ -export const createIndex = ( - client: ElasticsearchClient, - indexName: string, - mappings: IndexMapping, - aliases: string[] = [] -): TaskEither.TaskEither => { +export const createIndex = ({ + client, + indexName, + mappings, + aliases = [], +}: CreateIndexParams): TaskEither.TaskEither => { const createIndexTask: TaskEither.TaskEither< RetryableEsClientError, AcknowledgeResponse @@ -930,7 +1037,7 @@ export const createIndex = ( } else { // Otherwise, wait until the target index has a 'yellow' status. return pipe( - waitForIndexStatusYellow(client, indexName, DEFAULT_TIMEOUT), + waitForIndexStatusYellow({ client, index: indexName, timeout: DEFAULT_TIMEOUT }), TaskEither.map(() => { /** When the index status is 'yellow' we know that all shards were started */ return 'create_index_succeeded'; @@ -946,15 +1053,24 @@ export interface UpdateAndPickupMappingsResponse { taskId: string; } +/** @internal */ +export interface UpdateAndPickupMappingsParams { + client: ElasticsearchClient; + index: string; + mappings: IndexMapping; +} /** * Updates an index's mappings and runs an pickupUpdatedMappings task so that the mapping * changes are "picked up". Returns a taskId to track progress. */ -export const updateAndPickupMappings = ( - client: ElasticsearchClient, - index: string, - mappings: IndexMapping -): TaskEither.TaskEither => { +export const updateAndPickupMappings = ({ + client, + index, + mappings, +}: UpdateAndPickupMappingsParams): TaskEither.TaskEither< + RetryableEsClientError, + UpdateAndPickupMappingsResponse +> => { const putMappingTask: TaskEither.TaskEither< RetryableEsClientError, 'update_mappings_succeeded' @@ -1053,16 +1169,26 @@ export const searchForOutdatedDocuments = ( .catch(catchRetryableEsClientErrors); }; +/** @internal */ +export interface BulkOverwriteTransformedDocumentsParams { + client: ElasticsearchClient; + index: string; + transformedDocs: SavedObjectsRawDoc[]; + refresh?: estypes.Refresh; +} /** * Write the up-to-date transformed documents to the index, overwriting any * documents that are still on their outdated version. */ -export const bulkOverwriteTransformedDocuments = ( - client: ElasticsearchClient, - index: string, - transformedDocs: SavedObjectsRawDoc[], - refresh: estypes.Refresh -): TaskEither.TaskEither => () => { +export const bulkOverwriteTransformedDocuments = ({ + client, + index, + transformedDocs, + refresh = false, +}: BulkOverwriteTransformedDocumentsParams): TaskEither.TaskEither< + RetryableEsClientError, + 'bulk_index_succeeded' +> => () => { return client .bulk({ // Because we only add aliases in the MARK_VERSION_INDEX_READY step we diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index d0158a4c68f246..67a2685caf3d61 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -67,9 +67,13 @@ describe('migration actions', () => { client = start.elasticsearch.client.asInternalUser; // Create test fixture data: - await createIndex(client, 'existing_index_with_docs', { - dynamic: true, - properties: {}, + await createIndex({ + client, + indexName: 'existing_index_with_docs', + mappings: { + dynamic: true, + properties: {}, + }, })(); const sourceDocs = ([ { _source: { title: 'doc 1' } }, @@ -78,25 +82,30 @@ describe('migration actions', () => { { _source: { title: 'saved object 4', type: 'another_unused_type' } }, { _source: { title: 'f-agent-event 5', type: 'f_agent_event' } }, ] as unknown) as SavedObjectsRawDoc[]; - await bulkOverwriteTransformedDocuments( + await bulkOverwriteTransformedDocuments({ + client, + index: 'existing_index_with_docs', + transformedDocs: sourceDocs, + refresh: 'wait_for', + })(); + + await createIndex({ client, indexName: 'existing_index_2', mappings: { properties: {} } })(); + await createIndex({ client, - 'existing_index_with_docs', - sourceDocs, - 'wait_for' - )(); - - await createIndex(client, 'existing_index_2', { properties: {} })(); - await createIndex(client, 'existing_index_with_write_block', { properties: {} })(); - await bulkOverwriteTransformedDocuments( + indexName: 'existing_index_with_write_block', + mappings: { properties: {} }, + })(); + await bulkOverwriteTransformedDocuments({ client, - 'existing_index_with_write_block', - sourceDocs, - 'wait_for' - )(); - await setWriteBlock(client, 'existing_index_with_write_block')(); - await updateAliases(client, [ - { add: { index: 'existing_index_2', alias: 'existing_index_2_alias' } }, - ])(); + index: 'existing_index_with_write_block', + transformedDocs: sourceDocs, + refresh: 'wait_for', + })(); + await setWriteBlock({ client, index: 'existing_index_with_write_block' })(); + await updateAliases({ + client, + aliasActions: [{ add: { index: 'existing_index_2', alias: 'existing_index_2_alias' } }], + })(); }); afterAll(async () => { @@ -107,7 +116,7 @@ describe('migration actions', () => { describe('fetchIndices', () => { it('resolves right empty record if no indices were found', async () => { expect.assertions(1); - const task = fetchIndices(client, ['no_such_index']); + const task = fetchIndices({ client, indices: ['no_such_index'] }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -117,10 +126,10 @@ describe('migration actions', () => { }); it('resolves right record with found indices', async () => { expect.assertions(1); - const res = (await fetchIndices(client, [ - 'no_such_index', - 'existing_index_with_docs', - ])()) as Either.Right; + const res = (await fetchIndices({ + client, + indices: ['no_such_index', 'existing_index_with_docs'], + })()) as Either.Right; expect(res.right).toEqual( expect.objectContaining({ @@ -136,11 +145,15 @@ describe('migration actions', () => { describe('setWriteBlock', () => { beforeAll(async () => { - await createIndex(client, 'new_index_without_write_block', { properties: {} })(); + await createIndex({ + client, + indexName: 'new_index_without_write_block', + mappings: { properties: {} }, + })(); }); it('resolves right when setting the write block succeeds', async () => { expect.assertions(1); - const task = setWriteBlock(client, 'new_index_without_write_block'); + const task = setWriteBlock({ client, index: 'new_index_without_write_block' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -150,7 +163,7 @@ describe('migration actions', () => { }); it('resolves right when setting a write block on an index that already has one', async () => { expect.assertions(1); - const task = setWriteBlock(client, 'existing_index_with_write_block'); + const task = setWriteBlock({ client, index: 'existing_index_with_write_block' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -160,7 +173,7 @@ describe('migration actions', () => { }); it('once resolved, prevents further writes to the index', async () => { expect.assertions(1); - const task = setWriteBlock(client, 'new_index_without_write_block'); + const task = setWriteBlock({ client, index: 'new_index_without_write_block' }); await task(); const sourceDocs = ([ { _source: { title: 'doc 1' } }, @@ -169,17 +182,17 @@ describe('migration actions', () => { { _source: { title: 'doc 4' } }, ] as unknown) as SavedObjectsRawDoc[]; await expect( - bulkOverwriteTransformedDocuments( + bulkOverwriteTransformedDocuments({ client, - 'new_index_without_write_block', - sourceDocs, - 'wait_for' - )() + index: 'new_index_without_write_block', + transformedDocs: sourceDocs, + refresh: 'wait_for', + })() ).rejects.toMatchObject(expect.anything()); }); it('resolves left index_not_found_exception when the index does not exist', async () => { expect.assertions(1); - const task = setWriteBlock(client, 'no_such_index'); + const task = setWriteBlock({ client, index: 'no_such_index' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -194,13 +207,21 @@ describe('migration actions', () => { describe('removeWriteBlock', () => { beforeAll(async () => { - await createIndex(client, 'existing_index_without_write_block_2', { properties: {} })(); - await createIndex(client, 'existing_index_with_write_block_2', { properties: {} })(); - await setWriteBlock(client, 'existing_index_with_write_block_2')(); + await createIndex({ + client, + indexName: 'existing_index_without_write_block_2', + mappings: { properties: {} }, + })(); + await createIndex({ + client, + indexName: 'existing_index_with_write_block_2', + mappings: { properties: {} }, + })(); + await setWriteBlock({ client, index: 'existing_index_with_write_block_2' })(); }); it('resolves right if successful when an index already has a write block', async () => { expect.assertions(1); - const task = removeWriteBlock(client, 'existing_index_with_write_block_2'); + const task = removeWriteBlock({ client, index: 'existing_index_with_write_block_2' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -210,7 +231,7 @@ describe('migration actions', () => { }); it('resolves right if successful when an index does not have a write block', async () => { expect.assertions(1); - const task = removeWriteBlock(client, 'existing_index_without_write_block_2'); + const task = removeWriteBlock({ client, index: 'existing_index_without_write_block_2' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -220,7 +241,7 @@ describe('migration actions', () => { }); it('rejects if there is a non-retryable error', async () => { expect.assertions(1); - const task = removeWriteBlock(client, 'no_such_index'); + const task = removeWriteBlock({ client, index: 'no_such_index' }); await expect(task()).rejects.toMatchInlineSnapshot( `[ResponseError: index_not_found_exception]` ); @@ -251,7 +272,10 @@ describe('migration actions', () => { ); // Start tracking the index status - const indexStatusPromise = waitForIndexStatusYellow(client, 'red_then_yellow_index')(); + const indexStatusPromise = waitForIndexStatusYellow({ + client, + index: 'red_then_yellow_index', + })(); const redStatusResponse = await client.cluster.health({ index: 'red_then_yellow_index' }); expect(redStatusResponse.body.status).toBe('red'); @@ -281,7 +305,11 @@ describe('migration actions', () => { } }); it('resolves right if cloning into a new target index', async () => { - const task = cloneIndex(client, 'existing_index_with_write_block', 'clone_target_1'); + const task = cloneIndex({ + client, + source: 'existing_index_with_write_block', + target: 'clone_target_1', + }); expect.assertions(1); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -314,11 +342,11 @@ describe('migration actions', () => { .catch((e) => {}); // Call clone even though the index already exists - const cloneIndexPromise = cloneIndex( + const cloneIndexPromise = cloneIndex({ client, - 'existing_index_with_write_block', - 'clone_red_then_yellow_index' - )(); + source: 'existing_index_with_write_block', + target: 'clone_red_then_yellow_index', + })(); let indexYellow = false; setTimeout(() => { @@ -348,7 +376,7 @@ describe('migration actions', () => { }); it('resolves left index_not_found_exception if the source index does not exist', async () => { expect.assertions(1); - const task = cloneIndex(client, 'no_such_index', 'clone_target_3'); + const task = cloneIndex({ client, source: 'no_such_index', target: 'clone_target_3' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -378,12 +406,12 @@ describe('migration actions', () => { .catch((e) => {}); // Call clone even though the index already exists - const cloneIndexPromise = cloneIndex( + const cloneIndexPromise = cloneIndex({ client, - 'existing_index_with_write_block', - 'clone_red_index', - '0s' - )(); + source: 'existing_index_with_write_block', + target: 'clone_red_index', + timeout: '0s', + })(); await cloneIndexPromise.then((res) => { expect(res).toMatchInlineSnapshot(` @@ -404,15 +432,15 @@ describe('migration actions', () => { // together with waitForReindexTask describe('reindex & waitForReindexTask', () => { it('resolves right when reindex succeeds without reindex script', async () => { - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -436,21 +464,21 @@ describe('migration actions', () => { `); }); it('resolves right and excludes all documents not matching the unusedTypesQuery', async () => { - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_excluded_docs', - Option.none, - false, - { + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_excluded_docs', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { bool: { must_not: ['f_agent_event', 'another_unused_type'].map((type) => ({ term: { type }, })), }, - } - )()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + }, + })()) as Either.Right; + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -473,15 +501,15 @@ describe('migration actions', () => { }); it('resolves right when reindex succeeds with reindex script', async () => { expect.assertions(2); - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_2', - Option.some(`ctx._source.title = ctx._source.title + '_updated'`), - false, - { match_all: {} } - )()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_2', + reindexScript: Option.some(`ctx._source.title = ctx._source.title + '_updated'`), + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -506,15 +534,15 @@ describe('migration actions', () => { it('resolves right, ignores version conflicts and does not update existing docs when reindex multiple times', async () => { expect.assertions(3); // Reindex with a script - let res = (await reindex( + let res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_3', - Option.some(`ctx._source.title = ctx._source.title + '_updated'`), - false, - { match_all: {} } - )()) as Either.Right; - let task = waitForReindexTask(client, res.right.taskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_3', + reindexScript: Option.some(`ctx._source.title = ctx._source.title + '_updated'`), + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + let task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -523,15 +551,15 @@ describe('migration actions', () => { `); // reindex without a script - res = (await reindex( + res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_3', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; - task = waitForReindexTask(client, res.right.taskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_3', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -559,7 +587,7 @@ describe('migration actions', () => { expect.assertions(2); // Simulate a reindex that only adds some of the documents from the // source index into the target index - await createIndex(client, 'reindex_target_4', { properties: {} })(); + await createIndex({ client, indexName: 'reindex_target_4', mappings: { properties: {} } })(); const sourceDocs = ((await searchForOutdatedDocuments(client, { batchSize: 1000, targetIndex: 'existing_index_with_docs', @@ -570,18 +598,23 @@ describe('migration actions', () => { _id, _source, })); - await bulkOverwriteTransformedDocuments(client, 'reindex_target_4', sourceDocs, 'wait_for')(); + await bulkOverwriteTransformedDocuments({ + client, + index: 'reindex_target_4', + transformedDocs: sourceDocs, + refresh: 'wait_for', + })(); // Now do a real reindex - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_4', - Option.some(`ctx._source.title = ctx._source.title + '_updated'`), - false, - { match_all: {} } - )()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_4', + reindexScript: Option.some(`ctx._source.title = ctx._source.title + '_updated'`), + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -614,24 +647,28 @@ describe('migration actions', () => { // and should ignore this error. // Create an index with incompatible mappings - await createIndex(client, 'reindex_target_5', { - dynamic: 'strict', - properties: { - /** no title field */ + await createIndex({ + client, + indexName: 'reindex_target_5', + mappings: { + dynamic: 'strict', + properties: { + /** no title field */ + }, }, })(); const { right: { taskId: reindexTaskId }, - } = (await reindex( + } = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_5', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; - const task = waitForReindexTask(client, reindexTaskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_5', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + const task = waitForReindexTask({ client, taskId: reindexTaskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -651,22 +688,26 @@ describe('migration actions', () => { // and should ignore this error. // Create an index with incompatible mappings - await createIndex(client, 'reindex_target_6', { - dynamic: false, - properties: { title: { type: 'integer' } }, // integer is incompatible with string title + await createIndex({ + client, + indexName: 'reindex_target_6', + mappings: { + dynamic: false, + properties: { title: { type: 'integer' } }, // integer is incompatible with string title + }, })(); const { right: { taskId: reindexTaskId }, - } = (await reindex( + } = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_6', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; - const task = waitForReindexTask(client, reindexTaskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_6', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + const task = waitForReindexTask({ client, taskId: reindexTaskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -679,10 +720,17 @@ describe('migration actions', () => { }); it('resolves left index_not_found_exception if source index does not exist', async () => { expect.assertions(1); - const res = (await reindex(client, 'no_such_index', 'reindex_target', Option.none, false, { - match_all: {}, + const res = (await reindex({ + client, + sourceIndex: 'no_such_index', + targetIndex: 'reindex_target', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { + match_all: {}, + }, })()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -695,16 +743,16 @@ describe('migration actions', () => { }); it('resolves left target_index_had_write_block if all failures are due to a write block', async () => { expect.assertions(1); - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'existing_index_with_write_block', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; + sourceIndex: 'existing_index_with_docs', + targetIndex: 'existing_index_with_write_block', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -717,16 +765,16 @@ describe('migration actions', () => { }); it('resolves left if requireAlias=true and the target is not an alias', async () => { expect.assertions(1); - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'existing_index_with_write_block', - Option.none, - true, - { match_all: {} } - )()) as Either.Right; + sourceIndex: 'existing_index_with_docs', + targetIndex: 'existing_index_with_write_block', + reindexScript: Option.none, + requireAlias: true, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -739,16 +787,16 @@ describe('migration actions', () => { `); }); it('resolves left wait_for_task_completion_timeout when the task does not finish within the timeout', async () => { - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '0s'); + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '0s' }); await expect(task()).resolves.toMatchObject({ _tag: 'Left', @@ -766,17 +814,21 @@ describe('migration actions', () => { describe('verifyReindex', () => { it('resolves right if source and target indices have the same amount of documents', async () => { expect.assertions(1); - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_7', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; - await waitForReindexTask(client, res.right.taskId, '10s')(); - - const task = verifyReindex(client, 'existing_index_with_docs', 'reindex_target_7'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_7', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + await waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' })(); + + const task = verifyReindex({ + client, + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_7', + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -786,7 +838,11 @@ describe('migration actions', () => { }); it('resolves left if source and target indices have different amount of documents', async () => { expect.assertions(1); - const task = verifyReindex(client, 'existing_index_with_docs', 'existing_index_2'); + const task = verifyReindex({ + client, + sourceIndex: 'existing_index_with_docs', + targetIndex: 'existing_index_2', + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -798,19 +854,27 @@ describe('migration actions', () => { }); it('rejects if source or target index does not exist', async () => { expect.assertions(2); - let task = verifyReindex(client, 'no_such_index', 'existing_index_2'); + let task = verifyReindex({ + client, + sourceIndex: 'no_such_index', + targetIndex: 'existing_index_2', + }); await expect(task()).rejects.toMatchInlineSnapshot( `[ResponseError: index_not_found_exception]` ); - task = verifyReindex(client, 'existing_index_2', 'no_such_index'); + task = verifyReindex({ + client, + sourceIndex: 'existing_index_2', + targetIndex: 'no_such_index', + }); await expect(task()).rejects.toThrow('index_not_found_exception'); }); }); describe('openPit', () => { it('opens PointInTime for an index', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; expect(pitResponse.right.pitId).toEqual(expect.any(String)); @@ -824,52 +888,52 @@ describe('migration actions', () => { await expect(searchResponse.body.hits.hits.length).toBeGreaterThan(0); }); it('rejects if index does not exist', async () => { - const openPitTask = openPit(client, 'no_such_index'); + const openPitTask = openPit({ client, index: 'no_such_index' }); await expect(openPitTask()).rejects.toThrow('index_not_found_exception'); }); }); describe('readWithPit', () => { it('requests documents from an index using given PIT', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - pitResponse.right.pitId, - { match_all: {} }, - 1000, - undefined - ); + pitId: pitResponse.right.pitId, + query: { match_all: {} }, + batchSize: 1000, + searchAfter: undefined, + }); const docsResponse = (await readWithPitTask()) as Either.Right; await expect(docsResponse.right.outdatedDocuments.length).toBe(5); }); it('requests the batchSize of documents from an index', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - pitResponse.right.pitId, - { match_all: {} }, - 3, - undefined - ); + pitId: pitResponse.right.pitId, + query: { match_all: {} }, + batchSize: 3, + searchAfter: undefined, + }); const docsResponse = (await readWithPitTask()) as Either.Right; await expect(docsResponse.right.outdatedDocuments.length).toBe(3); }); it('it excludes documents not matching the provided "query"', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - pitResponse.right.pitId, - { + pitId: pitResponse.right.pitId, + query: { bool: { must_not: [ { @@ -885,9 +949,9 @@ describe('migration actions', () => { ], }, }, - 1000, - undefined - ); + batchSize: 1000, + searchAfter: undefined, + }); const docsResponse = (await readWithPitTask()) as Either.Right; @@ -902,18 +966,18 @@ describe('migration actions', () => { }); it('only returns documents that match the provided "query"', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - pitResponse.right.pitId, - { + pitId: pitResponse.right.pitId, + query: { match: { title: { query: 'doc' } }, }, - 1000, - undefined - ); + batchSize: 1000, + searchAfter: undefined, + }); const docsResponse = (await readWithPitTask()) as Either.Right; @@ -928,19 +992,19 @@ describe('migration actions', () => { }); it('returns docs with _seq_no and _primary_term when specified', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - pitResponse.right.pitId, - { + pitId: pitResponse.right.pitId, + query: { match: { title: { query: 'doc' } }, }, - 1000, - undefined, - true - ); + batchSize: 1000, + searchAfter: undefined, + seqNoPrimaryTerm: true, + }); const docsResponse = (await readWithPitTask()) as Either.Right; @@ -955,18 +1019,18 @@ describe('migration actions', () => { }); it('does not return docs with _seq_no and _primary_term if not specified', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - pitResponse.right.pitId, - { + pitId: pitResponse.right.pitId, + query: { match: { title: { query: 'doc' } }, }, - 1000, - undefined - ); + batchSize: 1000, + searchAfter: undefined, + }); const docsResponse = (await readWithPitTask()) as Either.Right; @@ -981,24 +1045,24 @@ describe('migration actions', () => { }); it('rejects if PIT does not exist', async () => { - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - 'no_such_pit', - { match_all: {} }, - 1000, - undefined - ); + pitId: 'no_such_pit', + query: { match_all: {} }, + batchSize: 1000, + searchAfter: undefined, + }); await expect(readWithPitTask()).rejects.toThrow('illegal_argument_exception'); }); }); describe('closePit', () => { it('closes PointInTime', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; const pitId = pitResponse.right.pitId; - await closePit(client, pitId)(); + await closePit({ client, pitId })(); const searchTask = client.search({ body: { @@ -1010,7 +1074,7 @@ describe('migration actions', () => { }); it('rejects if PIT does not exist', async () => { - const closePitTask = closePit(client, 'no_such_pit'); + const closePitTask = closePit({ client, pitId: 'no_such_pit' }); await expect(closePitTask()).rejects.toThrow('illegal_argument_exception'); }); }); @@ -1034,7 +1098,10 @@ describe('migration actions', () => { return Either.right({ processedDocs }); }; } - const transformTask = transformDocs(innerTransformRawDocs, originalDocs); + const transformTask = transformDocs({ + transformRawDocs: innerTransformRawDocs, + outdatedDocuments: originalDocs, + }); const resultsWithProcessDocs = ((await transformTask()) as Either.Right) .right.processedDocs; @@ -1051,7 +1118,11 @@ describe('migration actions', () => { 'existing_index_with_write_block' )()) as Either.Right; - const task = waitForPickupUpdatedMappingsTask(client, res.right.taskId, '10s'); + const task = waitForPickupUpdatedMappingsTask({ + client, + taskId: res.right.taskId, + timeout: '10s', + }); // We can't do a snapshot match because the response includes an index // id which ES assigns dynamically @@ -1065,7 +1136,11 @@ describe('migration actions', () => { 'no_such_index' )()) as Either.Right; - const task = waitForPickupUpdatedMappingsTask(client, res.right.taskId, '10s'); + const task = waitForPickupUpdatedMappingsTask({ + client, + taskId: res.right.taskId, + timeout: '10s', + }); await expect(task()).rejects.toMatchInlineSnapshot(` [Error: pickupUpdatedMappings task failed with the following error: @@ -1078,7 +1153,11 @@ describe('migration actions', () => { 'existing_index_with_docs' )()) as Either.Right; - const task = waitForPickupUpdatedMappingsTask(client, res.right.taskId, '0s'); + const task = waitForPickupUpdatedMappingsTask({ + client, + taskId: res.right.taskId, + timeout: '0s', + }); await expect(task()).resolves.toMatchObject({ _tag: 'Left', @@ -1097,7 +1176,11 @@ describe('migration actions', () => { 'existing_index_with_docs' )()) as Either.Right; - const task = waitForPickupUpdatedMappingsTask(client, res.right.taskId, '10s'); + const task = waitForPickupUpdatedMappingsTask({ + client, + taskId: res.right.taskId, + timeout: '10s', + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -1111,9 +1194,13 @@ describe('migration actions', () => { describe('updateAndPickupMappings', () => { it('resolves right when mappings were updated and picked up', async () => { // Create an index without any mappings and insert documents into it - await createIndex(client, 'existing_index_without_mappings', { - dynamic: false, - properties: {}, + await createIndex({ + client, + indexName: 'existing_index_without_mappings', + mappings: { + dynamic: false, + properties: {}, + }, })(); const sourceDocs = ([ { _source: { title: 'doc 1' } }, @@ -1121,12 +1208,12 @@ describe('migration actions', () => { { _source: { title: 'doc 3' } }, { _source: { title: 'doc 4' } }, ] as unknown) as SavedObjectsRawDoc[]; - await bulkOverwriteTransformedDocuments( + await bulkOverwriteTransformedDocuments({ client, - 'existing_index_without_mappings', - sourceDocs, - 'wait_for' - )(); + index: 'existing_index_without_mappings', + transformedDocs: sourceDocs, + refresh: 'wait_for', + })(); // Assert that we can't search over the unmapped fields of the document const originalSearchResults = ((await searchForOutdatedDocuments(client, { @@ -1139,14 +1226,18 @@ describe('migration actions', () => { expect(originalSearchResults.length).toBe(0); // Update and pickup mappings so that the title field is searchable - const res = await updateAndPickupMappings(client, 'existing_index_without_mappings', { - properties: { - title: { type: 'text' }, + const res = await updateAndPickupMappings({ + client, + index: 'existing_index_without_mappings', + mappings: { + properties: { + title: { type: 'text' }, + }, }, })(); expect(Either.isRight(res)).toBe(true); const taskId = (res as Either.Right).right.taskId; - await waitForPickupUpdatedMappingsTask(client, taskId, '60s')(); + await waitForPickupUpdatedMappingsTask({ client, taskId, timeout: '60s' })(); // Repeat the search expecting to be able to find the existing documents const pickedUpSearchResults = ((await searchForOutdatedDocuments(client, { @@ -1163,15 +1254,18 @@ describe('migration actions', () => { describe('updateAliases', () => { describe('remove', () => { it('resolves left index_not_found_exception when the index does not exist', async () => { - const task = updateAliases(client, [ - { - remove: { - alias: 'no_such_alias', - index: 'no_such_index', - must_exist: false, + const task = updateAliases({ + client, + aliasActions: [ + { + remove: { + alias: 'no_such_alias', + index: 'no_such_index', + must_exist: false, + }, }, - }, - ]); + ], + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -1184,15 +1278,18 @@ describe('migration actions', () => { }); describe('with must_exist=false', () => { it('resolves left alias_not_found_exception when alias does not exist', async () => { - const task = updateAliases(client, [ - { - remove: { - alias: 'no_such_alias', - index: 'existing_index_with_docs', - must_exist: false, + const task = updateAliases({ + client, + aliasActions: [ + { + remove: { + alias: 'no_such_alias', + index: 'existing_index_with_docs', + must_exist: false, + }, }, - }, - ]); + ], + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -1205,15 +1302,18 @@ describe('migration actions', () => { }); describe('with must_exist=true', () => { it('resolves left alias_not_found_exception when alias does not exist on specified index', async () => { - const task = updateAliases(client, [ - { - remove: { - alias: 'existing_index_2_alias', - index: 'existing_index_with_docs', - must_exist: true, + const task = updateAliases({ + client, + aliasActions: [ + { + remove: { + alias: 'existing_index_2_alias', + index: 'existing_index_with_docs', + must_exist: true, + }, }, - }, - ]); + ], + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -1224,15 +1324,18 @@ describe('migration actions', () => { `); }); it('resolves left alias_not_found_exception when alias does not exist', async () => { - const task = updateAliases(client, [ - { - remove: { - alias: 'no_such_alias', - index: 'existing_index_with_docs', - must_exist: true, + const task = updateAliases({ + client, + aliasActions: [ + { + remove: { + alias: 'no_such_alias', + index: 'existing_index_with_docs', + must_exist: true, + }, }, - }, - ]); + ], + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -1246,13 +1349,16 @@ describe('migration actions', () => { }); describe('remove_index', () => { it('left index_not_found_exception if index does not exist', async () => { - const task = updateAliases(client, [ - { - remove_index: { - index: 'no_such_index', + const task = updateAliases({ + client, + aliasActions: [ + { + remove_index: { + index: 'no_such_index', + }, }, - }, - ]); + ], + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -1264,13 +1370,16 @@ describe('migration actions', () => { `); }); it('left remove_index_not_a_concrete_index when remove_index targets an alias', async () => { - const task = updateAliases(client, [ - { - remove_index: { - index: 'existing_index_2_alias', + const task = updateAliases({ + client, + aliasActions: [ + { + remove_index: { + index: 'existing_index_2_alias', + }, }, - }, - ]); + ], + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -1312,7 +1421,11 @@ describe('migration actions', () => { }); // Call createIndex even though the index already exists - const createIndexPromise = createIndex(client, 'red_then_yellow_index', undefined as any)(); + const createIndexPromise = createIndex({ + client, + indexName: 'red_then_yellow_index', + mappings: undefined as any, + })(); let indexYellow = false; setTimeout(() => { @@ -1341,7 +1454,7 @@ describe('migration actions', () => { // Creating an index with the same name as an existing alias to induce // failure await expect( - createIndex(client, 'existing_index_2_alias', undefined as any)() + createIndex({ client, indexName: 'existing_index_2_alias', mappings: undefined as any })() ).rejects.toMatchInlineSnapshot(`[ResponseError: invalid_index_name_exception]`); }); }); @@ -1353,12 +1466,12 @@ describe('migration actions', () => { { _source: { title: 'doc 6' } }, { _source: { title: 'doc 7' } }, ] as unknown) as SavedObjectsRawDoc[]; - const task = bulkOverwriteTransformedDocuments( + const task = bulkOverwriteTransformedDocuments({ client, - 'existing_index_with_docs', - newDocs, - 'wait_for' - ); + index: 'existing_index_with_docs', + transformedDocs: newDocs, + refresh: 'wait_for', + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -1374,12 +1487,15 @@ describe('migration actions', () => { outdatedDocumentsQuery: undefined, })()) as Either.Right).right.outdatedDocuments; - const task = bulkOverwriteTransformedDocuments( + const task = bulkOverwriteTransformedDocuments({ client, - 'existing_index_with_docs', - [...existingDocs, ({ _source: { title: 'doc 8' } } as unknown) as SavedObjectsRawDoc], - 'wait_for' - ); + index: 'existing_index_with_docs', + transformedDocs: [ + ...existingDocs, + ({ _source: { title: 'doc 8' } } as unknown) as SavedObjectsRawDoc, + ], + refresh: 'wait_for', + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -1394,12 +1510,12 @@ describe('migration actions', () => { { _source: { title: 'doc 7' } }, ] as unknown) as SavedObjectsRawDoc[]; await expect( - bulkOverwriteTransformedDocuments( + bulkOverwriteTransformedDocuments({ client, - 'existing_index_with_write_block', - newDocs, - 'wait_for' - )() + index: 'existing_index_with_write_block', + transformedDocs: newDocs, + refresh: 'wait_for', + })() ).rejects.toMatchObject(expect.anything()); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts index 1881f9a712c293..e9cb33c0aa54a2 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts @@ -19,7 +19,7 @@ export async function cleanup( if (!state) return; if ('sourceIndexPitId' in state) { try { - await Actions.closePit(client, state.sourceIndexPitId)(); + await Actions.closePit({ client, pitId: state.sourceIndexPitId })(); } catch (e) { executionLog.push({ type: 'cleanup', diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 07ebf80271d48c..3c3e3c46a8d68f 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -58,38 +58,46 @@ export type ResponseType = UnwrapPromise< export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: TransformRawDocs) => { return { INIT: (state: InitState) => - Actions.fetchIndices(client, [state.currentAlias, state.versionAlias]), + Actions.fetchIndices({ client, indices: [state.currentAlias, state.versionAlias] }), WAIT_FOR_YELLOW_SOURCE: (state: WaitForYellowSourceState) => - Actions.waitForIndexStatusYellow(client, state.sourceIndex.value), + Actions.waitForIndexStatusYellow({ client, index: state.sourceIndex.value }), SET_SOURCE_WRITE_BLOCK: (state: SetSourceWriteBlockState) => - Actions.setWriteBlock(client, state.sourceIndex.value), + Actions.setWriteBlock({ client, index: state.sourceIndex.value }), CREATE_NEW_TARGET: (state: CreateNewTargetState) => - Actions.createIndex(client, state.targetIndex, state.targetIndexMappings), + Actions.createIndex({ + client, + indexName: state.targetIndex, + mappings: state.targetIndexMappings, + }), CREATE_REINDEX_TEMP: (state: CreateReindexTempState) => - Actions.createIndex(client, state.tempIndex, state.tempIndexMappings), + Actions.createIndex({ + client, + indexName: state.tempIndex, + mappings: state.tempIndexMappings, + }), REINDEX_SOURCE_TO_TEMP_OPEN_PIT: (state: ReindexSourceToTempOpenPit) => - Actions.openPit(client, state.sourceIndex.value), + Actions.openPit({ client, index: state.sourceIndex.value }), REINDEX_SOURCE_TO_TEMP_READ: (state: ReindexSourceToTempRead) => - Actions.readWithPit( + Actions.readWithPit({ client, - state.sourceIndexPitId, + pitId: state.sourceIndexPitId, /* When reading we use a source query to exclude saved objects types which * are no longer used. These saved objects will still be kept in the outdated * index for backup purposes, but won't be available in the upgraded index. */ - state.unusedTypesQuery, - state.batchSize, - state.lastHitSortValue - ), + query: state.unusedTypesQuery, + batchSize: state.batchSize, + searchAfter: state.lastHitSortValue, + }), REINDEX_SOURCE_TO_TEMP_CLOSE_PIT: (state: ReindexSourceToTempClosePit) => - Actions.closePit(client, state.sourceIndexPitId), + Actions.closePit({ client, pitId: state.sourceIndexPitId }), REINDEX_SOURCE_TO_TEMP_INDEX: (state: ReindexSourceToTempIndex) => - Actions.transformDocs(transformRawDocs, state.outdatedDocuments), + Actions.transformDocs({ transformRawDocs, outdatedDocuments: state.outdatedDocuments }), REINDEX_SOURCE_TO_TEMP_INDEX_BULK: (state: ReindexSourceToTempIndexBulk) => - Actions.bulkOverwriteTransformedDocuments( + Actions.bulkOverwriteTransformedDocuments({ client, - state.tempIndex, - state.transformedDocs, + index: state.tempIndex, + transformedDocs: state.transformedDocs, /** * Since we don't run a search against the target index, we disable "refresh" to speed up * the migration process. @@ -97,39 +105,48 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra * before we reach out to the OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT step. * Right now, it's performed during REFRESH_TARGET step. */ - false - ), + refresh: false, + }), SET_TEMP_WRITE_BLOCK: (state: SetTempWriteBlock) => - Actions.setWriteBlock(client, state.tempIndex), + Actions.setWriteBlock({ client, index: state.tempIndex }), CLONE_TEMP_TO_TARGET: (state: CloneTempToSource) => - Actions.cloneIndex(client, state.tempIndex, state.targetIndex), - REFRESH_TARGET: (state: RefreshTarget) => Actions.refreshIndex(client, state.targetIndex), + Actions.cloneIndex({ client, source: state.tempIndex, target: state.targetIndex }), + REFRESH_TARGET: (state: RefreshTarget) => + Actions.refreshIndex({ client, targetIndex: state.targetIndex }), UPDATE_TARGET_MAPPINGS: (state: UpdateTargetMappingsState) => - Actions.updateAndPickupMappings(client, state.targetIndex, state.targetIndexMappings), + Actions.updateAndPickupMappings({ + client, + index: state.targetIndex, + mappings: state.targetIndexMappings, + }), UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK: (state: UpdateTargetMappingsWaitForTaskState) => - Actions.waitForPickupUpdatedMappingsTask(client, state.updateTargetMappingsTaskId, '60s'), + Actions.waitForPickupUpdatedMappingsTask({ + client, + taskId: state.updateTargetMappingsTaskId, + timeout: '60s', + }), OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT: (state: OutdatedDocumentsSearchOpenPit) => - Actions.openPit(client, state.targetIndex), + Actions.openPit({ client, index: state.targetIndex }), OUTDATED_DOCUMENTS_SEARCH_READ: (state: OutdatedDocumentsSearchRead) => - Actions.readWithPit( + Actions.readWithPit({ client, - state.pitId, + pitId: state.pitId, // search for outdated documents only - state.outdatedDocumentsQuery, - state.batchSize, - state.lastHitSortValue - ), + query: state.outdatedDocumentsQuery, + batchSize: state.batchSize, + searchAfter: state.lastHitSortValue, + }), OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT: (state: OutdatedDocumentsSearchClosePit) => - Actions.closePit(client, state.pitId), + Actions.closePit({ client, pitId: state.pitId }), OUTDATED_DOCUMENTS_REFRESH: (state: OutdatedDocumentsRefresh) => - Actions.refreshIndex(client, state.targetIndex), + Actions.refreshIndex({ client, targetIndex: state.targetIndex }), OUTDATED_DOCUMENTS_TRANSFORM: (state: OutdatedDocumentsTransform) => - Actions.transformDocs(transformRawDocs, state.outdatedDocuments), + Actions.transformDocs({ transformRawDocs, outdatedDocuments: state.outdatedDocuments }), TRANSFORMED_DOCUMENTS_BULK_INDEX: (state: TransformedDocumentsBulkIndex) => - Actions.bulkOverwriteTransformedDocuments( + Actions.bulkOverwriteTransformedDocuments({ client, - state.targetIndex, - state.transformedDocs, + index: state.targetIndex, + transformedDocs: state.transformedDocs, /** * Since we don't run a search against the target index, we disable "refresh" to speed up * the migration process. @@ -137,29 +154,32 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra * before we reach out to the MARK_VERSION_INDEX_READY step. * Right now, it's performed during OUTDATED_DOCUMENTS_REFRESH step. */ - false - ), + }), MARK_VERSION_INDEX_READY: (state: MarkVersionIndexReady) => - Actions.updateAliases(client, state.versionIndexReadyActions.value), + Actions.updateAliases({ client, aliasActions: state.versionIndexReadyActions.value }), MARK_VERSION_INDEX_READY_CONFLICT: (state: MarkVersionIndexReadyConflict) => - Actions.fetchIndices(client, [state.currentAlias, state.versionAlias]), + Actions.fetchIndices({ client, indices: [state.currentAlias, state.versionAlias] }), LEGACY_SET_WRITE_BLOCK: (state: LegacySetWriteBlockState) => - Actions.setWriteBlock(client, state.legacyIndex), + Actions.setWriteBlock({ client, index: state.legacyIndex }), LEGACY_CREATE_REINDEX_TARGET: (state: LegacyCreateReindexTargetState) => - Actions.createIndex(client, state.sourceIndex.value, state.legacyReindexTargetMappings), + Actions.createIndex({ + client, + indexName: state.sourceIndex.value, + mappings: state.legacyReindexTargetMappings, + }), LEGACY_REINDEX: (state: LegacyReindexState) => - Actions.reindex( + Actions.reindex({ client, - state.legacyIndex, - state.sourceIndex.value, - state.preMigrationScript, - false, - state.unusedTypesQuery - ), + sourceIndex: state.legacyIndex, + targetIndex: state.sourceIndex.value, + reindexScript: state.preMigrationScript, + requireAlias: false, + unusedTypesQuery: state.unusedTypesQuery, + }), LEGACY_REINDEX_WAIT_FOR_TASK: (state: LegacyReindexWaitForTaskState) => - Actions.waitForReindexTask(client, state.legacyReindexTaskId, '60s'), + Actions.waitForReindexTask({ client, taskId: state.legacyReindexTaskId, timeout: '60s' }), LEGACY_DELETE: (state: LegacyDeleteState) => - Actions.updateAliases(client, state.legacyPreMigrationDoneActions), + Actions.updateAliases({ client, aliasActions: state.legacyPreMigrationDoneActions }), }; }; diff --git a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts index 186962b568792f..cf27505e8f073c 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts @@ -7,6 +7,7 @@ */ import { schema as s, ObjectType } from '@kbn/config-schema'; +import { sortOrderSchema } from './common_schemas'; /** * Schemas for the Bucket aggregations. @@ -85,6 +86,12 @@ export const bucketAggsSchemas: Record = { min_doc_count: s.maybe(s.number({ min: 1 })), size: s.maybe(s.number()), show_term_doc_count_error: s.maybe(s.boolean()), - order: s.maybe(s.oneOf([s.literal('asc'), s.literal('desc')])), + order: s.maybe( + s.oneOf([ + sortOrderSchema, + s.recordOf(s.string(), sortOrderSchema), + s.arrayOf(s.recordOf(s.string(), sortOrderSchema)), + ]) + ), }), }; diff --git a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/common_schemas.ts b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/common_schemas.ts new file mode 100644 index 00000000000000..92a3096374687d --- /dev/null +++ b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/common_schemas.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 { schema as s } from '@kbn/config-schema'; + +// note: these schemas are not exhaustive. See the `Sort` type of `@elastic/elasticsearch` if you need to enhance it. +const fieldSchema = s.string(); +export const sortOrderSchema = s.oneOf([s.literal('asc'), s.literal('desc'), s.literal('_doc')]); +const sortModeSchema = s.oneOf([ + s.literal('min'), + s.literal('max'), + s.literal('sum'), + s.literal('avg'), + s.literal('median'), +]); +const fieldSortSchema = s.object({ + missing: s.maybe(s.oneOf([s.string(), s.number(), s.boolean()])), + mode: s.maybe(sortModeSchema), + order: s.maybe(sortOrderSchema), + // nested and unmapped_type not implemented yet +}); +const sortContainerSchema = s.recordOf(s.string(), s.oneOf([sortOrderSchema, fieldSortSchema])); +const sortCombinationsSchema = s.oneOf([fieldSchema, sortContainerSchema]); +export const sortSchema = s.oneOf([sortCombinationsSchema, s.arrayOf(sortCombinationsSchema)]); diff --git a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/metrics_aggs.ts b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/metrics_aggs.ts index c05ae67cd2164e..fb7e25fae19f29 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/metrics_aggs.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/metrics_aggs.ts @@ -7,6 +7,7 @@ */ import { schema as s, ObjectType } from '@kbn/config-schema'; +import { sortSchema } from './common_schemas'; /** * Schemas for the metrics Aggregations @@ -68,7 +69,7 @@ export const metricsAggsSchemas: Record = { stored_fields: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])), from: s.maybe(s.number()), size: s.maybe(s.number()), - sort: s.maybe(s.oneOf([s.literal('asc'), s.literal('desc')])), + sort: s.maybe(sortSchema), seq_no_primary_term: s.maybe(s.boolean()), version: s.maybe(s.boolean()), track_scores: s.maybe(s.boolean()), diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7f108dbeb0086f..0c35177f51f996 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -874,6 +874,7 @@ export interface DeprecationsDetails { }; manualSteps?: string[]; }; + deprecationType?: 'config' | 'feature'; // (undocumented) documentationUrl?: string; level: 'warning' | 'critical' | 'fetch_error'; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 4d99368f9bf708..a31b9a061ac5d5 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -193,8 +193,6 @@ export class Server { const deprecationsSetup = this.deprecations.setup({ http: httpSetup, - elasticsearch: elasticsearchServiceSetup, - coreUsageData: coreUsageDataSetup, }); const coreSetup: InternalCoreSetup = { diff --git a/src/plugins/apm_oss/public/index.ts b/src/plugins/apm_oss/public/index.ts index d5fcabbe146a96..fea8ac4a8a1e42 100644 --- a/src/plugins/apm_oss/public/index.ts +++ b/src/plugins/apm_oss/public/index.ts @@ -14,5 +14,3 @@ export function plugin() { return new ApmOssPlugin(); } export { ApmOssPluginSetup, ApmOssPluginStart } from './types'; - -export { APM_STATIC_INDEX_PATTERN_ID } from '../common/index_pattern_constants'; diff --git a/src/plugins/apm_oss/server/index.ts b/src/plugins/apm_oss/server/index.ts index a02e28201a1b90..1424cb1c7126fb 100644 --- a/src/plugins/apm_oss/server/index.ts +++ b/src/plugins/apm_oss/server/index.ts @@ -7,7 +7,6 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import apmIndexPattern from './tutorial/index_pattern.json'; import { PluginInitializerContext } from '../../../core/server'; import { APMOSSPlugin } from './plugin'; @@ -32,20 +31,3 @@ export function plugin(initializerContext: PluginInitializerContext) { export type APMOSSConfig = TypeOf; export { APMOSSPluginSetup } from './plugin'; - -export { apmIndexPattern }; - -export { APM_STATIC_INDEX_PATTERN_ID } from '../common/index_pattern_constants'; - -export { - createNodeAgentInstructions, - createDjangoAgentInstructions, - createFlaskAgentInstructions, - createRailsAgentInstructions, - createRackAgentInstructions, - createJsAgentInstructions, - createGoAgentInstructions, - createJavaAgentInstructions, - createDotNetAgentInstructions, - createPhpAgentInstructions, -} from './tutorial/instructions/apm_agent_instructions'; diff --git a/src/plugins/apm_oss/server/plugin.ts b/src/plugins/apm_oss/server/plugin.ts index e504d5f0b9a9fa..02a8ac38be2a39 100644 --- a/src/plugins/apm_oss/server/plugin.ts +++ b/src/plugins/apm_oss/server/plugin.ts @@ -6,38 +6,18 @@ * Side Public License, v 1. */ -import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; +import { Plugin, PluginInitializerContext } from 'src/core/server'; import { APMOSSConfig } from './'; -import { HomeServerPluginSetup, TutorialProvider } from '../../home/server'; -import { tutorialProvider } from './tutorial'; export class APMOSSPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; } - public setup(core: CoreSetup, plugins: { home: HomeServerPluginSetup }) { + public setup() { const config$ = this.initContext.config.create(); - const config = this.initContext.config.get(); - - const apmTutorialProvider = tutorialProvider({ - indexPatternTitle: config.indexPattern, - indices: { - errorIndices: config.errorIndices, - metricsIndices: config.metricsIndices, - onboardingIndices: config.onboardingIndices, - sourcemapIndices: config.sourcemapIndices, - transactionIndices: config.transactionIndices, - }, - }); - plugins.home.tutorials.registerTutorial(apmTutorialProvider); - - return { - config, - config$, - getRegisteredTutorialProvider: () => apmTutorialProvider, - }; + return { config, config$ }; } start() {} @@ -47,5 +27,4 @@ export class APMOSSPlugin implements Plugin { export interface APMOSSPluginSetup { config: APMOSSConfig; config$: Observable; - getRegisteredTutorialProvider(): TutorialProvider; } diff --git a/src/plugins/apm_oss/server/tutorial/index.ts b/src/plugins/apm_oss/server/tutorial/index.ts deleted file mode 100644 index ce7fec406e7ac0..00000000000000 --- a/src/plugins/apm_oss/server/tutorial/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { onPremInstructions } from './envs/on_prem'; -import apmIndexPattern from './index_pattern.json'; -import { ArtifactsSchema, TutorialsCategory } from '../../../../../src/plugins/home/server'; -import { APM_STATIC_INDEX_PATTERN_ID } from '../../common/index_pattern_constants'; - -const apmIntro = i18n.translate('apmOss.tutorial.introduction', { - defaultMessage: 'Collect in-depth performance metrics and errors from inside your applications.', -}); -const moduleName = 'apm'; - -export const tutorialProvider = ({ - indexPatternTitle, - indices, -}: { - indexPatternTitle: string; - indices: { - errorIndices: string; - transactionIndices: string; - metricsIndices: string; - sourcemapIndices: string; - onboardingIndices: string; - }; -}) => () => { - const savedObjects = [ - { - ...apmIndexPattern, - id: APM_STATIC_INDEX_PATTERN_ID, - attributes: { - ...apmIndexPattern.attributes, - title: indexPatternTitle, - }, - }, - ]; - - const artifacts: ArtifactsSchema = { - dashboards: [ - { - id: '8d3ed660-7828-11e7-8c47-65b845b5cfb3', - linkLabel: i18n.translate('apmOss.tutorial.specProvider.artifacts.dashboards.linkLabel', { - defaultMessage: 'APM dashboard', - }), - isOverview: true, - }, - ], - }; - - return { - id: 'apm', - name: i18n.translate('apmOss.tutorial.specProvider.name', { - defaultMessage: 'APM', - }), - moduleName, - category: TutorialsCategory.OTHER, - shortDescription: apmIntro, - longDescription: i18n.translate('apmOss.tutorial.specProvider.longDescription', { - defaultMessage: - 'Application Performance Monitoring (APM) collects in-depth \ -performance metrics and errors from inside your application. \ -It allows you to monitor the performance of thousands of applications in real time. \ -[Learn more]({learnMoreLink}).', - values: { - learnMoreLink: - '{config.docs.base_url}guide/en/apm/get-started/{config.docs.version}/index.html', - }, - }), - euiIconType: 'apmApp', - artifacts, - onPrem: onPremInstructions(indices), - previewImagePath: '/plugins/apmOss/assets/apm.png', - savedObjects, - savedObjectsInstallMsg: i18n.translate('apmOss.tutorial.specProvider.savedObjectsInstallMsg', { - defaultMessage: 'An APM index pattern is required for some features in the APM UI.', - }), - }; -}; diff --git a/src/plugins/apm_oss/server/tutorial/instructions/apm_agent_instructions.ts b/src/plugins/apm_oss/server/tutorial/instructions/apm_agent_instructions.ts deleted file mode 100644 index ba2b062870cf68..00000000000000 --- a/src/plugins/apm_oss/server/tutorial/instructions/apm_agent_instructions.ts +++ /dev/null @@ -1,754 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; - -export const createNodeAgentInstructions = (apmServerUrl = '', secretToken = '') => [ - { - title: i18n.translate('apmOss.tutorial.nodeClient.install.title', { - defaultMessage: 'Install the APM agent', - }), - textPre: i18n.translate('apmOss.tutorial.nodeClient.install.textPre', { - defaultMessage: 'Install the APM agent for Node.js as a dependency to your application.', - }), - commands: ['npm install elastic-apm-node --save'], - }, - { - title: i18n.translate('apmOss.tutorial.nodeClient.configure.title', { - defaultMessage: 'Configure the agent', - }), - textPre: i18n.translate('apmOss.tutorial.nodeClient.configure.textPre', { - defaultMessage: - 'Agents are libraries that run inside of your application process. \ -APM services are created programmatically based on the `serviceName`. \ -This agent supports a variety of frameworks but can also be used with your custom stack.', - }), - commands: `// ${i18n.translate( - 'apmOss.tutorial.nodeClient.configure.commands.addThisToTheFileTopComment', - { - defaultMessage: 'Add this to the VERY top of the first file loaded in your app', - } - )} -var apm = require('elastic-apm-node').start({curlyOpen} - - // ${i18n.translate( - 'apmOss.tutorial.nodeClient.configure.commands.setRequiredServiceNameComment', - { - defaultMessage: 'Override the service name from package.json', - } - )} - // ${i18n.translate('apmOss.tutorial.nodeClient.configure.commands.allowedCharactersComment', { - defaultMessage: 'Allowed characters: a-z, A-Z, 0-9, -, _, and space', - })} - serviceName: '', - - // ${i18n.translate( - 'apmOss.tutorial.nodeClient.configure.commands.useIfApmRequiresTokenComment', - { - defaultMessage: 'Use if APM Server requires a secret token', - } - )} - secretToken: '${secretToken}', - - // ${i18n.translate( - 'apmOss.tutorial.nodeClient.configure.commands.setCustomApmServerUrlComment', - { - defaultMessage: 'Set the custom APM Server URL (default: {defaultApmServerUrl})', - values: { defaultApmServerUrl: 'http://localhost:8200' }, - } - )} - serverUrl: '${apmServerUrl}', - - // ${i18n.translate( - 'apmOss.tutorial.nodeClient.configure.commands.setCustomServiceEnvironmentComment', - { - defaultMessage: 'Set the service environment', - } - )} - environment: 'production' -{curlyClose})`.split('\n'), - textPost: i18n.translate('apmOss.tutorial.nodeClient.configure.textPost', { - defaultMessage: - 'See [the documentation]({documentationLink}) for advanced usage, including how to use with \ -[Babel/ES Modules]({babelEsModulesLink}).', - values: { - documentationLink: '{config.docs.base_url}guide/en/apm/agent/nodejs/current/index.html', - babelEsModulesLink: - '{config.docs.base_url}guide/en/apm/agent/nodejs/current/advanced-setup.html#es-modules', - }, - }), - }, -]; - -export const createDjangoAgentInstructions = (apmServerUrl = '', secretToken = '') => [ - { - title: i18n.translate('apmOss.tutorial.djangoClient.install.title', { - defaultMessage: 'Install the APM agent', - }), - textPre: i18n.translate('apmOss.tutorial.djangoClient.install.textPre', { - defaultMessage: 'Install the APM agent for Python as a dependency.', - }), - commands: ['$ pip install elastic-apm'], - }, - { - title: i18n.translate('apmOss.tutorial.djangoClient.configure.title', { - defaultMessage: 'Configure the agent', - }), - textPre: i18n.translate('apmOss.tutorial.djangoClient.configure.textPre', { - defaultMessage: - 'Agents are libraries that run inside of your application process. \ -APM services are created programmatically based on the `SERVICE_NAME`.', - }), - commands: `# ${i18n.translate( - 'apmOss.tutorial.djangoClient.configure.commands.addAgentComment', - { - defaultMessage: 'Add the agent to the installed apps', - } - )} -INSTALLED_APPS = ( - 'elasticapm.contrib.django', - # ... -) - -ELASTIC_APM = {curlyOpen} - # ${i18n.translate( - 'apmOss.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment', - { - defaultMessage: 'Set the required service name. Allowed characters:', - } - )} - # ${i18n.translate('apmOss.tutorial.djangoClient.configure.commands.allowedCharactersComment', { - defaultMessage: 'a-z, A-Z, 0-9, -, _, and space', - })} - 'SERVICE_NAME': '', - - # ${i18n.translate( - 'apmOss.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment', - { - defaultMessage: 'Use if APM Server requires a secret token', - } - )} - 'SECRET_TOKEN': '${secretToken}', - - # ${i18n.translate( - 'apmOss.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment', - { - defaultMessage: 'Set the custom APM Server URL (default: {defaultApmServerUrl})', - values: { defaultApmServerUrl: 'http://localhost:8200' }, - } - )} - 'SERVER_URL': '${apmServerUrl}', - - # ${i18n.translate( - 'apmOss.tutorial.djangoClient.configure.commands.setServiceEnvironmentComment', - { - defaultMessage: 'Set the service environment', - } - )} - 'ENVIRONMENT': 'production', -{curlyClose} - -# ${i18n.translate('apmOss.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment', { - defaultMessage: 'To send performance metrics, add our tracing middleware:', - })} -MIDDLEWARE = ( - 'elasticapm.contrib.django.middleware.TracingMiddleware', - #... -)`.split('\n'), - textPost: i18n.translate('apmOss.tutorial.djangoClient.configure.textPost', { - defaultMessage: 'See the [documentation]({documentationLink}) for advanced usage.', - values: { - documentationLink: - '{config.docs.base_url}guide/en/apm/agent/python/current/django-support.html', - }, - }), - }, -]; - -export const createFlaskAgentInstructions = (apmServerUrl = '', secretToken = '') => [ - { - title: i18n.translate('apmOss.tutorial.flaskClient.install.title', { - defaultMessage: 'Install the APM agent', - }), - textPre: i18n.translate('apmOss.tutorial.flaskClient.install.textPre', { - defaultMessage: 'Install the APM agent for Python as a dependency.', - }), - commands: ['$ pip install elastic-apm[flask]'], - }, - { - title: i18n.translate('apmOss.tutorial.flaskClient.configure.title', { - defaultMessage: 'Configure the agent', - }), - textPre: i18n.translate('apmOss.tutorial.flaskClient.configure.textPre', { - defaultMessage: - 'Agents are libraries that run inside of your application process. \ -APM services are created programmatically based on the `SERVICE_NAME`.', - }), - commands: `# ${i18n.translate( - 'apmOss.tutorial.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment', - { - defaultMessage: 'initialize using environment variables', - } - )} -from elasticapm.contrib.flask import ElasticAPM -app = Flask(__name__) -apm = ElasticAPM(app) - -# ${i18n.translate('apmOss.tutorial.flaskClient.configure.commands.configureElasticApmComment', { - defaultMessage: "or configure to use ELASTIC_APM in your application's settings", - })} -from elasticapm.contrib.flask import ElasticAPM -app.config['ELASTIC_APM'] = {curlyOpen} - # ${i18n.translate( - 'apmOss.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment', - { - defaultMessage: 'Set the required service name. Allowed characters:', - } - )} - # ${i18n.translate('apmOss.tutorial.flaskClient.configure.commands.allowedCharactersComment', { - defaultMessage: 'a-z, A-Z, 0-9, -, _, and space', - })} - 'SERVICE_NAME': '', - - # ${i18n.translate( - 'apmOss.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment', - { - defaultMessage: 'Use if APM Server requires a secret token', - } - )} - 'SECRET_TOKEN': '${secretToken}', - - # ${i18n.translate( - 'apmOss.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment', - { - defaultMessage: 'Set the custom APM Server URL (default: {defaultApmServerUrl})', - values: { defaultApmServerUrl: 'http://localhost:8200' }, - } - )} - 'SERVER_URL': '${apmServerUrl}', - - # ${i18n.translate( - 'apmOss.tutorial.flaskClient.configure.commands.setServiceEnvironmentComment', - { - defaultMessage: 'Set the service environment', - } - )} - 'ENVIRONMENT': 'production', -{curlyClose} - -apm = ElasticAPM(app)`.split('\n'), - textPost: i18n.translate('apmOss.tutorial.flaskClient.configure.textPost', { - defaultMessage: 'See the [documentation]({documentationLink}) for advanced usage.', - values: { - documentationLink: - '{config.docs.base_url}guide/en/apm/agent/python/current/flask-support.html', - }, - }), - }, -]; - -export const createRailsAgentInstructions = (apmServerUrl = '', secretToken = '') => [ - { - title: i18n.translate('apmOss.tutorial.railsClient.install.title', { - defaultMessage: 'Install the APM agent', - }), - textPre: i18n.translate('apmOss.tutorial.railsClient.install.textPre', { - defaultMessage: 'Add the agent to your Gemfile.', - }), - commands: [`gem 'elastic-apm'`], - }, - { - title: i18n.translate('apmOss.tutorial.railsClient.configure.title', { - defaultMessage: 'Configure the agent', - }), - textPre: i18n.translate('apmOss.tutorial.railsClient.configure.textPre', { - defaultMessage: - 'APM is automatically started when your app boots. Configure the agent, by creating the config file {configFile}', - values: { configFile: '`config/elastic_apm.yml`' }, - }), - commands: `# config/elastic_apm.yml: - -# Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space -# Defaults to the name of your Rails app -service_name: 'my-service' - -# Use if APM Server requires a secret token -secret_token: '${secretToken}' - -# Set the custom APM Server URL (default: http://localhost:8200) -server_url: '${apmServerUrl || 'http://localhost:8200'}' - -# Set the service environment -environment: 'production'`.split('\n'), - textPost: i18n.translate('apmOss.tutorial.railsClient.configure.textPost', { - defaultMessage: - 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', - values: { - documentationLink: '{config.docs.base_url}guide/en/apm/agent/ruby/current/index.html', - }, - }), - }, -]; - -export const createRackAgentInstructions = (apmServerUrl = '', secretToken = '') => [ - { - title: i18n.translate('apmOss.tutorial.rackClient.install.title', { - defaultMessage: 'Install the APM agent', - }), - textPre: i18n.translate('apmOss.tutorial.rackClient.install.textPre', { - defaultMessage: 'Add the agent to your Gemfile.', - }), - commands: [`gem 'elastic-apm'`], - }, - { - title: i18n.translate('apmOss.tutorial.rackClient.configure.title', { - defaultMessage: 'Configure the agent', - }), - textPre: i18n.translate('apmOss.tutorial.rackClient.configure.textPre', { - defaultMessage: - 'For Rack or a compatible framework (e.g. Sinatra), include the middleware in your app and start the agent.', - }), - commands: `# config.ru - require 'sinatra/base' - - class MySinatraApp < Sinatra::Base - use ElasticAPM::Middleware - - # ... - end - - ElasticAPM.start( - app: MySinatraApp, # ${i18n.translate( - 'apmOss.tutorial.rackClient.configure.commands.requiredComment', - { - defaultMessage: 'required', - } - )} - config_file: '' # ${i18n.translate( - 'apmOss.tutorial.rackClient.configure.commands.optionalComment', - { - defaultMessage: 'optional, defaults to config/elastic_apm.yml', - } - )} - ) - - run MySinatraApp - - at_exit {curlyOpen} ElasticAPM.stop {curlyClose}`.split('\n'), - }, - { - title: i18n.translate('apmOss.tutorial.rackClient.createConfig.title', { - defaultMessage: 'Create config file', - }), - textPre: i18n.translate('apmOss.tutorial.rackClient.createConfig.textPre', { - defaultMessage: 'Create a config file {configFile}:', - values: { configFile: '`config/elastic_apm.yml`' }, - }), - commands: `# config/elastic_apm.yml: - -# ${i18n.translate('apmOss.tutorial.rackClient.createConfig.commands.setServiceNameComment', { - defaultMessage: 'Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space', - })} -# ${i18n.translate( - 'apmOss.tutorial.rackClient.createConfig.commands.defaultsToTheNameOfRackAppClassComment', - { - defaultMessage: "Defaults to the name of your Rack app's class.", - } - )} -service_name: 'my-service' - -# ${i18n.translate( - 'apmOss.tutorial.rackClient.createConfig.commands.useIfApmServerRequiresTokenComment', - { - defaultMessage: 'Use if APM Server requires a token', - } - )} -secret_token: '${secretToken}' - -# ${i18n.translate('apmOss.tutorial.rackClient.createConfig.commands.setCustomApmServerComment', { - defaultMessage: 'Set custom APM Server URL (default: {defaultServerUrl})', - values: { defaultServerUrl: 'http://localhost:8200' }, - })} -server_url: '${apmServerUrl || 'http://localhost:8200'}', - -# ${i18n.translate('apmOss.tutorial.rackClient.createConfig.commands.setServiceEnvironment', { - defaultMessage: 'Set the service environment', - })} -environment: 'production'`.split('\n'), - textPost: i18n.translate('apmOss.tutorial.rackClient.createConfig.textPost', { - defaultMessage: - 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', - values: { - documentationLink: '{config.docs.base_url}guide/en/apm/agent/ruby/current/index.html', - }, - }), - }, -]; - -export const createJsAgentInstructions = (apmServerUrl = '') => [ - { - title: i18n.translate('apmOss.tutorial.jsClient.enableRealUserMonitoring.title', { - defaultMessage: 'Enable Real User Monitoring support in APM Server', - }), - textPre: i18n.translate('apmOss.tutorial.jsClient.enableRealUserMonitoring.textPre', { - defaultMessage: - 'APM Server disables RUM support by default. See the [documentation]({documentationLink}) \ -for details on how to enable RUM support.', - values: { - documentationLink: - '{config.docs.base_url}guide/en/apm/server/{config.docs.version}/configuration-rum.html', - }, - }), - }, - { - title: i18n.translate('apmOss.tutorial.jsClient.installDependency.title', { - defaultMessage: 'Set up the Agent as a dependency', - }), - textPre: i18n.translate('apmOss.tutorial.jsClient.installDependency.textPre', { - defaultMessage: - 'You can install the Agent as a dependency to your application with \ -`npm install @elastic/apm-rum --save`.\n\n\ -The Agent can then be initialized and configured in your application like this:', - }), - commands: `import {curlyOpen} init as initApm {curlyClose} from '@elastic/apm-rum' -var apm = initApm({curlyOpen} - - // ${i18n.translate( - 'apmOss.tutorial.jsClient.installDependency.commands.setRequiredServiceNameComment', - { - defaultMessage: - 'Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)', - } - )} - serviceName: 'your-app-name', - - // ${i18n.translate( - 'apmOss.tutorial.jsClient.installDependency.commands.setCustomApmServerUrlComment', - { - defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', - values: { defaultApmServerUrl: 'http://localhost:8200' }, - } - )} - serverUrl: '${apmServerUrl}', - - // ${i18n.translate( - 'apmOss.tutorial.jsClient.installDependency.commands.setServiceVersionComment', - { - defaultMessage: 'Set the service version (required for source map feature)', - } - )} - serviceVersion: '', - - // ${i18n.translate( - 'apmOss.tutorial.jsClient.installDependency.commands.setServiceEnvironmentComment', - { - defaultMessage: 'Set the service environment', - } - )} - environment: 'production' -{curlyClose})`.split('\n'), - textPost: i18n.translate('apmOss.tutorial.jsClient.installDependency.textPost', { - defaultMessage: - 'Framework integrations, like React or Angular, have custom dependencies. \ -See the [integration documentation]({docLink}) for more information.', - values: { - docLink: - '{config.docs.base_url}guide/en/apm/agent/rum-js/current/framework-integrations.html', - }, - }), - }, - { - title: i18n.translate('apmOss.tutorial.jsClient.scriptTags.title', { - defaultMessage: 'Set up the Agent with Script Tags', - }), - textPre: i18n.translate('apmOss.tutorial.jsClient.scriptTags.textPre', { - defaultMessage: - "Alternatively, you can use Script tags to set up and configure the Agent. \ -Add a ` - -`.split('\n'), - }, -]; - -export const createGoAgentInstructions = (apmServerUrl = '', secretToken = '') => [ - { - title: i18n.translate('apmOss.tutorial.goClient.install.title', { - defaultMessage: 'Install the APM agent', - }), - textPre: i18n.translate('apmOss.tutorial.goClient.install.textPre', { - defaultMessage: 'Install the APM agent packages for Go.', - }), - commands: ['go get go.elastic.co/apm'], - }, - { - title: i18n.translate('apmOss.tutorial.goClient.configure.title', { - defaultMessage: 'Configure the agent', - }), - textPre: i18n.translate('apmOss.tutorial.goClient.configure.textPre', { - defaultMessage: - 'Agents are libraries that run inside of your application process. \ -APM services are created programmatically based on the executable \ -file name, or the `ELASTIC_APM_SERVICE_NAME` environment variable.', - }), - commands: `# ${i18n.translate( - 'apmOss.tutorial.goClient.configure.commands.initializeUsingEnvironmentVariablesComment', - { - defaultMessage: 'Initialize using environment variables:', - } - )} - -# ${i18n.translate('apmOss.tutorial.goClient.configure.commands.setServiceNameComment', { - defaultMessage: 'Set the service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space.', - })} -# ${i18n.translate('apmOss.tutorial.goClient.configure.commands.usedExecutableNameComment', { - defaultMessage: - 'If ELASTIC_APM_SERVICE_NAME is not specified, the executable name will be used.', - })} -export ELASTIC_APM_SERVICE_NAME= - -# ${i18n.translate('apmOss.tutorial.goClient.configure.commands.setCustomApmServerUrlComment', { - defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', - values: { defaultApmServerUrl: 'http://localhost:8200' }, - })} -export ELASTIC_APM_SERVER_URL=${apmServerUrl} - -# ${i18n.translate('apmOss.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment', { - defaultMessage: 'Use if APM Server requires a secret token', - })} -export ELASTIC_APM_SECRET_TOKEN=${secretToken} - -# ${i18n.translate('apmOss.tutorial.goClient.configure.commands.setServiceEnvironment', { - defaultMessage: 'Set the service environment', - })} -export ELASTIC_APM_ENVIRONMENT= -`.split('\n'), - textPost: i18n.translate('apmOss.tutorial.goClient.configure.textPost', { - defaultMessage: 'See the [documentation]({documentationLink}) for advanced configuration.', - values: { - documentationLink: '{config.docs.base_url}guide/en/apm/agent/go/current/configuration.html', - }, - }), - }, - { - title: i18n.translate('apmOss.tutorial.goClient.instrument.title', { - defaultMessage: 'Instrument your application', - }), - textPre: i18n.translate('apmOss.tutorial.goClient.instrument.textPre', { - defaultMessage: - 'Instrument your Go application by using one of the provided instrumentation modules or \ -by using the tracer API directly.', - }), - commands: `\ -import ( - "net/http" - - "go.elastic.co/apm/module/apmhttp" -) - -func main() {curlyOpen} - mux := http.NewServeMux() - ... - http.ListenAndServe(":8080", apmhttp.Wrap(mux)) -{curlyClose} -`.split('\n'), - textPost: i18n.translate('apmOss.tutorial.goClient.instrument.textPost', { - defaultMessage: - 'See the [documentation]({documentationLink}) for a detailed \ -guide to instrumenting Go source code.', - values: { - documentationLink: - '{config.docs.base_url}guide/en/apm/agent/go/current/instrumenting-source.html', - }, - }), - }, -]; - -export const createJavaAgentInstructions = (apmServerUrl = '', secretToken = '') => [ - { - title: i18n.translate('apmOss.tutorial.javaClient.download.title', { - defaultMessage: 'Download the APM agent', - }), - textPre: i18n.translate('apmOss.tutorial.javaClient.download.textPre', { - defaultMessage: - 'Download the agent jar from [Maven Central]({mavenCentralLink}). \ -Do **not** add the agent as a dependency to your application.', - values: { - mavenCentralLink: 'http://search.maven.org/#search%7Cga%7C1%7Ca%3Aelastic-apm-agent', - }, - }), - }, - { - title: i18n.translate('apmOss.tutorial.javaClient.startApplication.title', { - defaultMessage: 'Start your application with the javaagent flag', - }), - textPre: i18n.translate('apmOss.tutorial.javaClient.startApplication.textPre', { - defaultMessage: - 'Add the `-javaagent` flag and configure the agent with system properties.\n\n \ -* Set the required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)\n \ -* Set the custom APM Server URL (default: {customApmServerUrl})\n \ -* Set the APM Server secret token\n \ -* Set the service environment\n \ -* Set the base package of your application', - values: { customApmServerUrl: 'http://localhost:8200' }, - }), - commands: `java -javaagent:/path/to/elastic-apm-agent-.jar \\ - -Delastic.apm.service_name=my-application \\ - -Delastic.apm.server_urls=${apmServerUrl || 'http://localhost:8200'} \\ - -Delastic.apm.secret_token=${secretToken} \\ - -Delastic.apm.environment=production \\ - -Delastic.apm.application_packages=org.example \\ - -jar my-application.jar`.split('\n'), - textPost: i18n.translate('apmOss.tutorial.javaClient.startApplication.textPost', { - defaultMessage: - 'See the [documentation]({documentationLink}) for configuration options and advanced \ -usage.', - values: { - documentationLink: '{config.docs.base_url}guide/en/apm/agent/java/current/index.html', - }, - }), - }, -]; - -export const createDotNetAgentInstructions = (apmServerUrl = '', secretToken = '') => [ - { - title: i18n.translate('apmOss.tutorial.dotNetClient.download.title', { - defaultMessage: 'Download the APM agent', - }), - textPre: i18n.translate('apmOss.tutorial.dotNetClient.download.textPre', { - defaultMessage: - 'Add the the agent package(s) from [NuGet]({allNuGetPackagesLink}) to your .NET application. There are multiple \ - NuGet packages available for different use cases. \n\nFor an ASP.NET Core application with Entity Framework \ - Core download the [Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}) package. This package will automatically add every \ - agent component to your application. \n\n In case you would like to minimize the dependencies, you can use the \ - [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) package for just \ - ASP.NET Core monitoring or the [Elastic.Apm.EfCore]({efCorePackageLink}) package for just Entity Framework Core monitoring. \n\n \ - In case you only want to use the public Agent API for manual instrumentation use the [Elastic.Apm]({elasticApmPackageLink}) package.', - values: { - allNuGetPackagesLink: 'https://www.nuget.org/packages?q=Elastic.apm', - netCoreAllApmPackageLink: 'https://www.nuget.org/packages/Elastic.Apm.NetCoreAll', - aspNetCorePackageLink: 'https://www.nuget.org/packages/Elastic.Apm.AspNetCore', - efCorePackageLink: 'https://www.nuget.org/packages/Elastic.Apm.EntityFrameworkCore', - elasticApmPackageLink: 'https://www.nuget.org/packages/Elastic.Apm', - }, - }), - }, - { - title: i18n.translate('apmOss.tutorial.dotNetClient.configureApplication.title', { - defaultMessage: 'Add the agent to the application', - }), - textPre: i18n.translate('apmOss.tutorial.dotNetClient.configureApplication.textPre', { - defaultMessage: - 'In case of ASP.NET Core with the `Elastic.Apm.NetCoreAll` package, call the `UseAllElasticApm` \ - method in the `Configure` method within the `Startup.cs` file.', - }), - commands: `public class Startup -{curlyOpen} - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - {curlyOpen} - app.UseAllElasticApm(Configuration); - //…rest of the method - {curlyClose} - //…rest of the class -{curlyClose}`.split('\n'), - textPost: i18n.translate('apmOss.tutorial.dotNetClient.configureApplication.textPost', { - defaultMessage: - 'Passing an `IConfiguration` instance is optional and by doing so, the agent will read config settings through this \ - `IConfiguration` instance (e.g. from the `appsettings.json` file).', - }), - }, - { - title: i18n.translate('apmOss.tutorial.dotNetClient.configureAgent.title', { - defaultMessage: 'Sample appsettings.json file:', - }), - commands: `{curlyOpen} - "ElasticApm": {curlyOpen} - "SecretToken": "${secretToken}", - "ServerUrls": "${ - apmServerUrl || 'http://localhost:8200' - }", //Set custom APM Server URL (default: http://localhost:8200) - "ServiceName": "MyApp", //allowed characters: a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application - "Environment": "production", // Set the service environment - {curlyClose} -{curlyClose}`.split('\n'), - textPost: i18n.translate('apmOss.tutorial.dotNetClient.configureAgent.textPost', { - defaultMessage: - 'In case you don’t pass an `IConfiguration` instance to the agent (e.g. in case of non ASP.NET Core applications) \ - you can also configure the agent through environment variables. \n \ - See [the documentation]({documentationLink}) for advanced usage.', - values: { - documentationLink: - '{config.docs.base_url}guide/en/apm/agent/dotnet/current/configuration.html', - }, - }), - }, -]; - -export const createPhpAgentInstructions = (apmServerUrl = '', secretToken = '') => [ - { - title: i18n.translate('apmOss.tutorial.phpClient.download.title', { - defaultMessage: 'Download the APM agent', - }), - textPre: i18n.translate('apmOss.tutorial.phpClient.download.textPre', { - defaultMessage: - 'Download the package corresponding to your platform from [GitHub releases]({githubReleasesLink}).', - values: { - githubReleasesLink: 'https://github.com/elastic/apm-agent-php/releases', - }, - }), - }, - { - title: i18n.translate('apmOss.tutorial.phpClient.installPackage.title', { - defaultMessage: 'Install the downloaded package', - }), - textPre: i18n.translate('apmOss.tutorial.phpClient.installPackage.textPre', { - defaultMessage: 'For example on Alpine Linux using APK package:', - }), - commands: ['apk add --allow-untrusted .apk'], - textPost: i18n.translate('apmOss.tutorial.phpClient.installPackage.textPost', { - defaultMessage: - 'See the [documentation]({documentationLink}) for installation commands on other supported platforms and advanced installation.', - values: { - documentationLink: '{config.docs.base_url}guide/en/apm/agent/php/current/setup.html', - }, - }), - }, - { - title: i18n.translate('apmOss.tutorial.phpClient.configureAgent.title', { - defaultMessage: 'Configure the agent', - }), - textPre: i18n.translate('apmOss.tutorial.phpClient.configureAgent.textPre', { - defaultMessage: - 'APM is automatically started when your app boots. Configure the agent either via `php.ini` file:', - }), - commands: `elastic_apm.server_url=http://localhost:8200 -elastic_apm.service_name="My service" -`.split('\n'), - textPost: i18n.translate('apmOss.tutorial.phpClient.configure.textPost', { - defaultMessage: - 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', - values: { - documentationLink: - '{config.docs.base_url}guide/en/apm/agent/php/current/configuration.html', - }, - }), - }, -]; diff --git a/src/plugins/bfetch/common/batch.ts b/src/plugins/bfetch/common/batch.ts index a84d94b541ae52..59b012751c66db 100644 --- a/src/plugins/bfetch/common/batch.ts +++ b/src/plugins/bfetch/common/batch.ts @@ -19,3 +19,8 @@ export interface BatchResponseItem new Promise((resolve) => setImmediate(resolve)); const getPromiseState = (promise: Promise): Promise<'resolved' | 'rejected' | 'pending'> => Promise.race<'resolved' | 'rejected' | 'pending'>([ @@ -52,6 +54,7 @@ describe('createStreamingBatchedFunction()', () => { const fn = createStreamingBatchedFunction({ url: '/test', fetchStreaming, + compressionDisabled$: rxof(true), }); expect(typeof fn).toBe('function'); }); @@ -61,6 +64,7 @@ describe('createStreamingBatchedFunction()', () => { const fn = createStreamingBatchedFunction({ url: '/test', fetchStreaming, + compressionDisabled$: rxof(true), }); const res = fn({}); expect(typeof res.then).toBe('function'); @@ -74,6 +78,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); expect(fetchStreaming).toHaveBeenCalledTimes(0); @@ -93,6 +98,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); expect(fetchStreaming).toHaveBeenCalledTimes(0); @@ -107,6 +113,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); fn({ foo: 'bar' }); @@ -125,6 +132,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); fn({ foo: 'bar' }); @@ -146,14 +154,18 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); expect(fetchStreaming).toHaveBeenCalledTimes(0); fn({ foo: 'bar' }); + await flushPromises(); expect(fetchStreaming).toHaveBeenCalledTimes(0); fn({ baz: 'quix' }); + await flushPromises(); expect(fetchStreaming).toHaveBeenCalledTimes(0); fn({ full: 'yep' }); + await flushPromises(); expect(fetchStreaming).toHaveBeenCalledTimes(1); }); @@ -164,6 +176,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const abortController = new AbortController(); @@ -186,11 +199,13 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); fn({ a: '1' }); fn({ b: '2' }); fn({ c: '3' }); + await flushPromises(); expect(fetchStreaming.mock.calls[0][0]).toMatchObject({ url: '/test', @@ -209,13 +224,16 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); fn({ a: '1' }); fn({ b: '2' }); fn({ c: '3' }); + await flushPromises(); expect(fetchStreaming).toHaveBeenCalledTimes(1); fn({ d: '4' }); + await flushPromises(); await new Promise((r) => setTimeout(r, 6)); expect(fetchStreaming).toHaveBeenCalledTimes(2); }); @@ -229,6 +247,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = fn({ a: '1' }); @@ -246,8 +265,11 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); + await flushPromises(); + const promise1 = fn({ a: '1' }); const promise2 = fn({ b: '2' }); const promise3 = fn({ c: '3' }); @@ -287,6 +309,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = fn({ a: '1' }); @@ -314,6 +337,20 @@ describe('createStreamingBatchedFunction()', () => { expect(await promise3).toEqual({ foo: 'bar 2' }); }); + test('compression is false by default', async () => { + const { fetchStreaming } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + flushOnMaxItems: 1, + fetchStreaming, + }); + + fn({ a: '1' }); + + const dontCompress = await fetchStreaming.mock.calls[0][0].compressionDisabled$.toPromise(); + expect(dontCompress).toBe(false); + }); + test('resolves falsy results', async () => { const { fetchStreaming, stream } = setup(); const fn = createStreamingBatchedFunction({ @@ -321,6 +358,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = fn({ a: '1' }); @@ -362,6 +400,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise = fn({ a: '1' }); @@ -390,6 +429,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = of(fn({ a: '1' })); @@ -442,6 +482,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const abortController = new AbortController(); @@ -471,6 +512,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const abortController = new AbortController(); @@ -509,6 +551,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = of(fn({ a: '1' })); @@ -539,6 +582,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = of(fn({ a: '1' })); @@ -576,6 +620,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = of(fn({ a: '1' })); @@ -608,6 +653,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = of(fn({ a: '1' })); @@ -644,7 +690,9 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); + await flushPromises(); const promise1 = of(fn({ a: '1' })); const promise2 = of(fn({ a: '2' })); diff --git a/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts b/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts index 2d81331f10a88e..d5f955f517d135 100644 --- a/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts +++ b/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts @@ -6,16 +6,16 @@ * Side Public License, v 1. */ +import { Observable, of } from 'rxjs'; import { AbortError, abortSignalToPromise, defer } from '../../../kibana_utils/public'; import { ItemBufferParams, TimedItemBufferParams, createBatchedFunction, - BatchResponseItem, ErrorLike, + normalizeError, } from '../../common'; -import { fetchStreaming, split } from '../streaming'; -import { normalizeError } from '../../common'; +import { fetchStreaming } from '../streaming'; import { BatchedFunc, BatchItem } from './types'; export interface BatchedFunctionProtocolError extends ErrorLike { @@ -47,6 +47,11 @@ export interface StreamingBatchedFunctionParams { * before sending the batch request. */ maxItemAge?: TimedItemBufferParams['maxItemAge']; + + /** + * Disabled zlib compression of response chunks. + */ + compressionDisabled$?: Observable; } /** @@ -64,6 +69,7 @@ export const createStreamingBatchedFunction = ( fetchStreaming: fetchStreamingInjected = fetchStreaming, flushOnMaxItems = 25, maxItemAge = 10, + compressionDisabled$ = of(false), } = params; const [fn] = createBatchedFunction({ onCall: (payload: Payload, signal?: AbortSignal) => { @@ -119,6 +125,7 @@ export const createStreamingBatchedFunction = ( body: JSON.stringify({ batch }), method: 'POST', signal: abortController.signal, + compressionDisabled$, }); const handleStreamError = (error: any) => { @@ -127,10 +134,10 @@ export const createStreamingBatchedFunction = ( for (const { future } of items) future.reject(normalizedError); }; - stream.pipe(split('\n')).subscribe({ + stream.subscribe({ next: (json: string) => { try { - const response = JSON.parse(json) as BatchResponseItem; + const response = JSON.parse(json); if (response.error) { items[response.id].future.reject(response.error); } else if (response.result !== undefined) { diff --git a/src/plugins/bfetch/public/batching/index.ts b/src/plugins/bfetch/public/batching/index.ts new file mode 100644 index 00000000000000..115fd84cbe979f --- /dev/null +++ b/src/plugins/bfetch/public/batching/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { + createStreamingBatchedFunction, + StreamingBatchedFunctionParams, +} from './create_streaming_batched_function'; diff --git a/src/plugins/bfetch/public/plugin.ts b/src/plugins/bfetch/public/plugin.ts index ed97d468eec0b1..f97a91a0e70d37 100644 --- a/src/plugins/bfetch/public/plugin.ts +++ b/src/plugins/bfetch/public/plugin.ts @@ -7,12 +7,11 @@ */ import { CoreStart, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/public'; +import { from, Observable, of } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; import { fetchStreaming as fetchStreamingStatic, FetchStreamingParams } from './streaming'; -import { removeLeadingSlash } from '../common'; -import { - createStreamingBatchedFunction, - StreamingBatchedFunctionParams, -} from './batching/create_streaming_batched_function'; +import { DISABLE_BFETCH_COMPRESSION, removeLeadingSlash } from '../common'; +import { createStreamingBatchedFunction, StreamingBatchedFunctionParams } from './batching'; import { BatchedFunc } from './batching/types'; // eslint-disable-next-line @@ -43,12 +42,23 @@ export class BfetchPublicPlugin constructor(private readonly initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, plugins: BfetchPublicSetupDependencies): BfetchPublicSetup { + public setup( + core: CoreSetup, + plugins: BfetchPublicSetupDependencies + ): BfetchPublicSetup { const { version } = this.initializerContext.env.packageInfo; const basePath = core.http.basePath.get(); - const fetchStreaming = this.fetchStreaming(version, basePath); - const batchedFunction = this.batchedFunction(fetchStreaming); + const compressionDisabled$ = from(core.getStartServices()).pipe( + switchMap((deps) => { + return of(deps[0]); + }), + switchMap((coreStart) => { + return coreStart.uiSettings.get$(DISABLE_BFETCH_COMPRESSION); + }) + ); + const fetchStreaming = this.fetchStreaming(version, basePath, compressionDisabled$); + const batchedFunction = this.batchedFunction(fetchStreaming, compressionDisabled$); this.contract = { fetchStreaming, @@ -66,7 +76,8 @@ export class BfetchPublicPlugin private fetchStreaming = ( version: string, - basePath: string + basePath: string, + compressionDisabled$: Observable ): BfetchPublicSetup['fetchStreaming'] => (params) => fetchStreamingStatic({ ...params, @@ -76,13 +87,16 @@ export class BfetchPublicPlugin 'kbn-version': version, ...(params.headers || {}), }, + compressionDisabled$, }); private batchedFunction = ( - fetchStreaming: BfetchPublicContract['fetchStreaming'] + fetchStreaming: BfetchPublicContract['fetchStreaming'], + compressionDisabled$: Observable ): BfetchPublicContract['batchedFunction'] => (params) => createStreamingBatchedFunction({ ...params, + compressionDisabled$, fetchStreaming: params.fetchStreaming || fetchStreaming, }); } diff --git a/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts b/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts index e804b3ea942275..a5d066f6d9a24d 100644 --- a/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts +++ b/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts @@ -8,6 +8,15 @@ import { fetchStreaming } from './fetch_streaming'; import { mockXMLHttpRequest } from '../test_helpers/xhr'; +import { of } from 'rxjs'; +import { promisify } from 'util'; +import { deflate } from 'zlib'; +const pDeflate = promisify(deflate); + +const compressResponse = async (resp: any) => { + const gzipped = await pDeflate(JSON.stringify(resp)); + return gzipped.toString('base64'); +}; const tick = () => new Promise((resolve) => setTimeout(resolve, 1)); @@ -21,6 +30,7 @@ test('returns XHR request', () => { setup(); const { xhr } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); expect(typeof xhr.readyState).toBe('number'); }); @@ -29,6 +39,7 @@ test('returns stream', () => { setup(); const { stream } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); expect(typeof stream.subscribe).toBe('function'); }); @@ -37,6 +48,7 @@ test('promise resolves when request completes', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); let resolved = false; @@ -65,10 +77,90 @@ test('promise resolves when request completes', async () => { expect(resolved).toBe(true); }); -test('streams incoming text as it comes through', async () => { +test('promise resolves when compressed request completes', async () => { + const env = setup(); + const { stream } = fetchStreaming({ + url: 'http://example.com', + compressionDisabled$: of(false), + }); + + let resolved = false; + let result; + stream.toPromise().then((r) => { + resolved = true; + result = r; + }); + + await tick(); + expect(resolved).toBe(false); + + const msg = { foo: 'bar' }; + + // Whole message in a response + (env.xhr as any).responseText = `${await compressResponse(msg)}\n`; + env.xhr.onprogress!({} as any); + + await tick(); + expect(resolved).toBe(false); + + (env.xhr as any).readyState = 4; + (env.xhr as any).status = 200; + env.xhr.onreadystatechange!({} as any); + + await tick(); + expect(resolved).toBe(true); + expect(result).toStrictEqual(JSON.stringify(msg)); +}); + +test('promise resolves when compressed chunked request completes', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(false), + }); + + let resolved = false; + let result; + stream.toPromise().then((r) => { + resolved = true; + result = r; + }); + + await tick(); + expect(resolved).toBe(false); + + const msg = { veg: 'tomato' }; + const msgToCut = await compressResponse(msg); + const part1 = msgToCut.substr(0, 3); + + // Message and a half in a response + (env.xhr as any).responseText = part1; + env.xhr.onprogress!({} as any); + + await tick(); + expect(resolved).toBe(false); + + // Half a message in a response + (env.xhr as any).responseText = `${msgToCut}\n`; + env.xhr.onprogress!({} as any); + + await tick(); + expect(resolved).toBe(false); + + (env.xhr as any).readyState = 4; + (env.xhr as any).status = 200; + env.xhr.onreadystatechange!({} as any); + + await tick(); + expect(resolved).toBe(true); + expect(result).toStrictEqual(JSON.stringify(msg)); +}); + +test('streams incoming text as it comes through, according to separators', async () => { + const env = setup(); + const { stream } = fetchStreaming({ + url: 'http://example.com', + compressionDisabled$: of(true), }); const spy = jest.fn(); @@ -80,16 +172,22 @@ test('streams incoming text as it comes through', async () => { (env.xhr as any).responseText = 'foo'; env.xhr.onprogress!({} as any); + await tick(); + expect(spy).toHaveBeenCalledTimes(0); + + (env.xhr as any).responseText = 'foo\nbar'; + env.xhr.onprogress!({} as any); + await tick(); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith('foo'); - (env.xhr as any).responseText = 'foo\nbar'; + (env.xhr as any).responseText = 'foo\nbar\n'; env.xhr.onprogress!({} as any); await tick(); expect(spy).toHaveBeenCalledTimes(2); - expect(spy).toHaveBeenCalledWith('\nbar'); + expect(spy).toHaveBeenCalledWith('bar'); (env.xhr as any).readyState = 4; (env.xhr as any).status = 200; @@ -103,6 +201,7 @@ test('completes stream observable when request finishes', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); const spy = jest.fn(); @@ -127,6 +226,7 @@ test('completes stream observable when aborted', async () => { const { stream } = fetchStreaming({ url: 'http://example.com', signal: abort.signal, + compressionDisabled$: of(true), }); const spy = jest.fn(); @@ -152,6 +252,7 @@ test('promise throws when request errors', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); const spy = jest.fn(); @@ -178,6 +279,7 @@ test('stream observable errors when request errors', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); const spy = jest.fn(); @@ -210,6 +312,7 @@ test('sets custom headers', async () => { 'Content-Type': 'text/plain', Authorization: 'Bearer 123', }, + compressionDisabled$: of(true), }); expect(env.xhr.setRequestHeader).toHaveBeenCalledWith('Content-Type', 'text/plain'); @@ -223,6 +326,7 @@ test('uses credentials', async () => { fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); expect(env.xhr.withCredentials).toBe(true); @@ -238,6 +342,7 @@ test('opens XHR request and sends specified body', async () => { url: 'http://elastic.co', method: 'GET', body: 'foobar', + compressionDisabled$: of(true), }); expect(env.xhr.open).toHaveBeenCalledTimes(1); @@ -250,6 +355,7 @@ test('uses POST request method by default', async () => { const env = setup(); fetchStreaming({ url: 'http://elastic.co', + compressionDisabled$: of(true), }); expect(env.xhr.open).toHaveBeenCalledWith('POST', 'http://elastic.co'); }); diff --git a/src/plugins/bfetch/public/streaming/fetch_streaming.ts b/src/plugins/bfetch/public/streaming/fetch_streaming.ts index d68e4d01b44f5f..1af35ef68fb85c 100644 --- a/src/plugins/bfetch/public/streaming/fetch_streaming.ts +++ b/src/plugins/bfetch/public/streaming/fetch_streaming.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ +import { Observable, of } from 'rxjs'; +import { map, share, switchMap } from 'rxjs/operators'; +import { inflateResponse } from '.'; import { fromStreamingXhr } from './from_streaming_xhr'; +import { split } from './split'; export interface FetchStreamingParams { url: string; @@ -14,6 +18,7 @@ export interface FetchStreamingParams { method?: 'GET' | 'POST'; body?: string; signal?: AbortSignal; + compressionDisabled$?: Observable; } /** @@ -26,23 +31,49 @@ export function fetchStreaming({ method = 'POST', body = '', signal, + compressionDisabled$ = of(false), }: FetchStreamingParams) { const xhr = new window.XMLHttpRequest(); - // Begin the request - xhr.open(method, url); - xhr.withCredentials = true; + const msgStream = compressionDisabled$.pipe( + switchMap((compressionDisabled) => { + // Begin the request + xhr.open(method, url); + xhr.withCredentials = true; - // Set the HTTP headers - Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v)); + if (!compressionDisabled) { + headers['X-Chunk-Encoding'] = 'deflate'; + } - const stream = fromStreamingXhr(xhr, signal); + // Set the HTTP headers + Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v)); - // Send the payload to the server - xhr.send(body); + const stream = fromStreamingXhr(xhr, signal); + + // Send the payload to the server + xhr.send(body); + + // Return a stream of chunked decompressed messages + return stream.pipe( + split('\n'), + map((msg) => { + return compressionDisabled ? msg : inflateResponse(msg); + }) + ); + }), + share() + ); + + // start execution + const msgStreamSub = msgStream.subscribe({ + error: (e) => {}, + complete: () => { + msgStreamSub.unsubscribe(); + }, + }); return { xhr, - stream, + stream: msgStream, }; } diff --git a/src/plugins/bfetch/public/streaming/index.ts b/src/plugins/bfetch/public/streaming/index.ts index afb442feffb294..545cae87aa3d6a 100644 --- a/src/plugins/bfetch/public/streaming/index.ts +++ b/src/plugins/bfetch/public/streaming/index.ts @@ -9,3 +9,4 @@ export * from './split'; export * from './from_streaming_xhr'; export * from './fetch_streaming'; +export { inflateResponse } from './inflate_response'; diff --git a/src/plugins/bfetch/public/streaming/inflate_response.ts b/src/plugins/bfetch/public/streaming/inflate_response.ts new file mode 100644 index 00000000000000..73cb52285987ca --- /dev/null +++ b/src/plugins/bfetch/public/streaming/inflate_response.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. + */ + +import { unzlibSync, strFromU8 } from 'fflate'; + +export function inflateResponse(response: string) { + const buff = Buffer.from(response, 'base64'); + const unzip = unzlibSync(buff); + return strFromU8(unzip); +} diff --git a/src/plugins/bfetch/server/plugin.ts b/src/plugins/bfetch/server/plugin.ts index 18f0813260f036..7fd46e2f6cc44e 100644 --- a/src/plugins/bfetch/server/plugin.ts +++ b/src/plugins/bfetch/server/plugin.ts @@ -16,6 +16,7 @@ import type { RouteMethod, RequestHandler, RequestHandlerContext, + StartServicesAccessor, } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { Subject } from 'rxjs'; @@ -28,7 +29,8 @@ import { normalizeError, } from '../common'; import { StreamingRequestHandler } from './types'; -import { createNDJSONStream } from './streaming'; +import { createStream } from './streaming'; +import { getUiSettings } from './ui_settings'; // eslint-disable-next-line export interface BfetchServerSetupDependencies {} @@ -112,9 +114,19 @@ export class BfetchServerPlugin public setup(core: CoreSetup, plugins: BfetchServerSetupDependencies): BfetchServerSetup { const logger = this.initializerContext.logger.get(); const router = core.http.createRouter(); - const addStreamingResponseRoute = this.addStreamingResponseRoute({ router, logger }); + + core.uiSettings.register(getUiSettings()); + + const addStreamingResponseRoute = this.addStreamingResponseRoute({ + getStartServices: core.getStartServices, + router, + logger, + }); const addBatchProcessingRoute = this.addBatchProcessingRoute(addStreamingResponseRoute); - const createStreamingRequestHandler = this.createStreamingRequestHandler({ logger }); + const createStreamingRequestHandler = this.createStreamingRequestHandler({ + getStartServices: core.getStartServices, + logger, + }); return { addBatchProcessingRoute, @@ -129,10 +141,16 @@ export class BfetchServerPlugin public stop() {} + private getCompressionDisabled(request: KibanaRequest) { + return request.headers['x-chunk-encoding'] !== 'deflate'; + } + private addStreamingResponseRoute = ({ + getStartServices, router, logger, }: { + getStartServices: StartServicesAccessor; router: ReturnType; logger: Logger; }): BfetchServerSetup['addStreamingResponseRoute'] => (path, handler) => { @@ -146,9 +164,10 @@ export class BfetchServerPlugin async (context, request, response) => { const handlerInstance = handler(request); const data = request.body; + const compressionDisabled = this.getCompressionDisabled(request); return response.ok({ headers: streamingHeaders, - body: createNDJSONStream(handlerInstance.getResponseStream(data), logger), + body: createStream(handlerInstance.getResponseStream(data), logger, compressionDisabled), }); } ); @@ -156,17 +175,20 @@ export class BfetchServerPlugin private createStreamingRequestHandler = ({ logger, + getStartServices, }: { logger: Logger; + getStartServices: StartServicesAccessor; }): BfetchServerSetup['createStreamingRequestHandler'] => (streamHandler) => async ( context, request, response ) => { const response$ = await streamHandler(context, request); + const compressionDisabled = this.getCompressionDisabled(request); return response.ok({ headers: streamingHeaders, - body: createNDJSONStream(response$, logger), + body: createStream(response$, logger, compressionDisabled), }); }; diff --git a/src/plugins/bfetch/server/streaming/create_compressed_stream.ts b/src/plugins/bfetch/server/streaming/create_compressed_stream.ts new file mode 100644 index 00000000000000..6814ed1dd79550 --- /dev/null +++ b/src/plugins/bfetch/server/streaming/create_compressed_stream.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 { promisify } from 'util'; +import { Observable } from 'rxjs'; +import { catchError, concatMap, finalize } from 'rxjs/operators'; +import { Logger } from 'src/core/server'; +import { Stream, PassThrough } from 'stream'; +import { constants, deflate } from 'zlib'; + +const delimiter = '\n'; +const pDeflate = promisify(deflate); + +async function zipMessageToStream(output: PassThrough, message: string) { + return new Promise(async (resolve, reject) => { + try { + const gzipped = await pDeflate(message, { + flush: constants.Z_SYNC_FLUSH, + }); + output.write(gzipped.toString('base64')); + output.write(delimiter); + resolve(undefined); + } catch (err) { + reject(err); + } + }); +} + +export const createCompressedStream = ( + results: Observable, + logger: Logger +): Stream => { + const output = new PassThrough(); + + const sub = results + .pipe( + concatMap((message: Response) => { + const strMessage = JSON.stringify(message); + return zipMessageToStream(output, strMessage); + }), + catchError((e) => { + logger.error('Could not serialize or stream a message.'); + logger.error(e); + throw e; + }), + finalize(() => { + output.end(); + sub.unsubscribe(); + }) + ) + .subscribe(); + + return output; +}; diff --git a/src/plugins/bfetch/server/streaming/create_stream.ts b/src/plugins/bfetch/server/streaming/create_stream.ts new file mode 100644 index 00000000000000..7d6981294341b3 --- /dev/null +++ b/src/plugins/bfetch/server/streaming/create_stream.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 { Logger } from 'kibana/server'; +import { Stream } from 'stream'; +import { Observable } from 'rxjs'; +import { createCompressedStream } from './create_compressed_stream'; +import { createNDJSONStream } from './create_ndjson_stream'; + +export function createStream( + response$: Observable, + logger: Logger, + compressionDisabled: boolean +): Stream { + return compressionDisabled + ? createNDJSONStream(response$, logger) + : createCompressedStream(response$, logger); +} diff --git a/src/plugins/bfetch/server/streaming/index.ts b/src/plugins/bfetch/server/streaming/index.ts index 2c31cc329295d0..dfd472b5034a1b 100644 --- a/src/plugins/bfetch/server/streaming/index.ts +++ b/src/plugins/bfetch/server/streaming/index.ts @@ -7,3 +7,5 @@ */ export * from './create_ndjson_stream'; +export * from './create_compressed_stream'; +export * from './create_stream'; diff --git a/src/plugins/bfetch/server/ui_settings.ts b/src/plugins/bfetch/server/ui_settings.ts new file mode 100644 index 00000000000000..cf7b13a9af1820 --- /dev/null +++ b/src/plugins/bfetch/server/ui_settings.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'; +import { UiSettingsParams } from 'src/core/server'; +import { schema } from '@kbn/config-schema'; +import { DISABLE_BFETCH_COMPRESSION } from '../common'; + +export function getUiSettings(): Record> { + return { + [DISABLE_BFETCH_COMPRESSION]: { + name: i18n.translate('bfetch.disableBfetchCompression', { + defaultMessage: 'Disable Batch Compression', + }), + value: false, + description: i18n.translate('bfetch.disableBfetchCompressionDesc', { + defaultMessage: + 'Disable batch compression. This allows you to debug individual requests, but increases response size.', + }), + schema: schema.boolean(), + category: [], + }, + }; +} diff --git a/src/plugins/charts/common/palette.test.ts b/src/plugins/charts/common/palette.test.ts index 0a26d71a9b9d53..86ba74d409cc6e 100644 --- a/src/plugins/charts/common/palette.test.ts +++ b/src/plugins/charts/common/palette.test.ts @@ -12,13 +12,14 @@ import { systemPalette, PaletteOutput, CustomPaletteState, + CustomPaletteArguments, } from './palette'; import { functionWrapper } from 'src/plugins/expressions/common/expression_functions/specs/tests/utils'; describe('palette', () => { const fn = functionWrapper(palette()) as ( context: null, - args?: { color?: string[]; gradient?: boolean; reverse?: boolean } + args?: Partial ) => PaletteOutput; it('results a palette', () => { @@ -39,6 +40,18 @@ describe('palette', () => { }); }); + describe('stop', () => { + it('sets stops', () => { + const result = fn(null, { color: ['red', 'green', 'blue'], stop: [1, 2, 3] }); + expect(result.params!.stops).toEqual([1, 2, 3]); + }); + + it('defaults to pault_tor_14 colors', () => { + const result = fn(null); + expect(result.params!.colors).toEqual(defaultCustomColors); + }); + }); + describe('gradient', () => { it('sets gradient', () => { let result = fn(null, { gradient: true }); @@ -69,6 +82,16 @@ describe('palette', () => { const result = fn(null); expect(result.params!.colors).toEqual(defaultCustomColors); }); + + it('keeps the stops order pristine when set', () => { + const stops = [1, 2, 3]; + const result = fn(null, { + color: ['red', 'green', 'blue'], + stop: [1, 2, 3], + reverse: true, + }); + expect(result.params!.stops).toEqual(stops); + }); }); }); }); diff --git a/src/plugins/charts/common/palette.ts b/src/plugins/charts/common/palette.ts index c9232b22cfae14..78c6fcc8120284 100644 --- a/src/plugins/charts/common/palette.ts +++ b/src/plugins/charts/common/palette.ts @@ -14,11 +14,21 @@ export interface CustomPaletteArguments { color?: string[]; gradient: boolean; reverse?: boolean; + stop?: number[]; + range?: 'number' | 'percent'; + rangeMin?: number; + rangeMax?: number; + continuity?: 'above' | 'below' | 'all' | 'none'; } export interface CustomPaletteState { colors: string[]; gradient: boolean; + stops: number[]; + range: 'number' | 'percent'; + rangeMin: number; + rangeMax: number; + continuity?: 'above' | 'below' | 'all' | 'none'; } export interface SystemPaletteArguments { @@ -83,6 +93,35 @@ export function palette(): ExpressionFunctionDefinition< }), required: false, }, + stop: { + multi: true, + types: ['number'], + help: i18n.translate('charts.functions.palette.args.stopHelpText', { + defaultMessage: + 'The palette color stops. When used, it must be associated with each color.', + }), + required: false, + }, + continuity: { + types: ['string'], + options: ['above', 'below', 'all', 'none'], + default: 'above', + help: '', + }, + rangeMin: { + types: ['number'], + help: '', + }, + rangeMax: { + types: ['number'], + help: '', + }, + range: { + types: ['string'], + options: ['number', 'percent'], + default: 'percent', + help: '', + }, gradient: { types: ['boolean'], default: false, @@ -101,15 +140,32 @@ export function palette(): ExpressionFunctionDefinition< }, }, fn: (input, args) => { - const { color, reverse, gradient } = args; + const { + color, + continuity, + reverse, + gradient, + stop, + range, + rangeMin = 0, + rangeMax = 100, + } = args; const colors = ([] as string[]).concat(color || defaultCustomColors); - + const stops = ([] as number[]).concat(stop || []); + if (stops.length > 0 && colors.length !== stops.length) { + throw Error('When stop is used, each color must have an associated stop value.'); + } return { type: 'palette', name: 'custom', params: { colors: reverse ? colors.reverse() : colors, + stops, + range: range ?? 'percent', gradient, + continuity, + rangeMin, + rangeMax, }, }; }, diff --git a/src/plugins/charts/public/services/palettes/helpers.test.ts b/src/plugins/charts/public/services/palettes/helpers.test.ts new file mode 100644 index 00000000000000..90f5745570cc8b --- /dev/null +++ b/src/plugins/charts/public/services/palettes/helpers.test.ts @@ -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 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 { workoutColorForValue } from './helpers'; +import { CustomPaletteState } from '../..'; + +describe('workoutColorForValue', () => { + it('should return no color for empty value', () => { + expect( + workoutColorForValue( + undefined, + { + continuity: 'above', + colors: ['red', 'green', 'blue', 'yellow'], + range: 'number', + gradient: false, + rangeMin: 0, + rangeMax: 200, + stops: [], + }, + { min: 0, max: 200 } + ) + ).toBeUndefined(); + }); + + describe('range: "number"', () => { + const DEFAULT_PROPS: CustomPaletteState = { + continuity: 'above', + colors: ['red', 'green', 'blue', 'yellow'], + range: 'number', + gradient: false, + rangeMin: 0, + rangeMax: 200, + stops: [], + }; + it('find the right color for predefined palettes', () => { + expect(workoutColorForValue(123, DEFAULT_PROPS, { min: 0, max: 200 })).toBe('blue'); + }); + + it('find the right color for custom stops palettes', () => { + expect( + workoutColorForValue( + 50, + { + ...DEFAULT_PROPS, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBe('blue'); + }); + + it('find the right color for custom stops palettes when value is higher than rangeMax', () => { + expect( + workoutColorForValue( + 123, + { + ...DEFAULT_PROPS, + rangeMax: 100, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBe('yellow'); + expect( + workoutColorForValue( + 123, + { + ...DEFAULT_PROPS, + continuity: 'all', + rangeMax: 100, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBe('yellow'); + }); + + it('returns no color if the value if higher than rangeMax and continuity is nor "above" or "all"', () => { + expect( + workoutColorForValue( + 123, + { + ...DEFAULT_PROPS, + continuity: 'below', + rangeMax: 100, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBeUndefined(); + expect( + workoutColorForValue( + 123, + { + ...DEFAULT_PROPS, + continuity: 'none', + rangeMax: 100, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBeUndefined(); + }); + + it('find the right color for custom stops palettes when value is lower than rangeMin', () => { + expect( + workoutColorForValue( + 10, + { + ...DEFAULT_PROPS, + continuity: 'below', + rangeMin: 20, + rangeMax: 100, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBe('red'); + expect( + workoutColorForValue( + 10, + { + ...DEFAULT_PROPS, + continuity: 'all', + rangeMin: 20, + rangeMax: 100, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBe('red'); + }); + + it('returns no color if the value if lower than rangeMin and continuity is nor "below" or "all"', () => { + expect( + workoutColorForValue( + 0, + { + ...DEFAULT_PROPS, + rangeMin: 10, + rangeMax: 100, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBeUndefined(); + expect( + workoutColorForValue( + 0, + { + ...DEFAULT_PROPS, + continuity: 'none', + rangeMin: 10, + rangeMax: 100, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBeUndefined(); + }); + }); + + describe('range: "percent"', () => { + const DEFAULT_PROPS: CustomPaletteState = { + continuity: 'above', + colors: ['red', 'green', 'blue', 'yellow'], + range: 'percent', + gradient: false, + rangeMin: 0, + rangeMax: 100, + stops: [], + }; + it('find the right color for predefined palettes', () => { + expect(workoutColorForValue(123, DEFAULT_PROPS, { min: 0, max: 200 })).toBe('blue'); + }); + + it('find the right color for custom stops palettes', () => { + expect( + workoutColorForValue( + 113, + { + ...DEFAULT_PROPS, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBe('blue'); // 113/200 ~ 56% + }); + + it('find the right color for custom stops palettes when value is higher than rangeMax', () => { + expect( + workoutColorForValue( + 123, + { + ...DEFAULT_PROPS, + rangeMax: 90, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBe('yellow'); + expect( + workoutColorForValue( + 123, + { + ...DEFAULT_PROPS, + continuity: 'all', + rangeMax: 90, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBe('yellow'); + }); + + it('returns no color if the value if higher than rangeMax and continuity is nor "above" or "all"', () => { + expect( + workoutColorForValue( + 190, + { + ...DEFAULT_PROPS, + continuity: 'below', + rangeMax: 90, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBeUndefined(); + expect( + workoutColorForValue( + 190, + { + ...DEFAULT_PROPS, + continuity: 'none', + rangeMax: 90, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBeUndefined(); + }); + + it('find the right color for custom stops palettes when value is lower than rangeMin', () => { + expect( + workoutColorForValue( + 10, + { + ...DEFAULT_PROPS, + continuity: 'below', + rangeMin: 20, + rangeMax: 100, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBe('red'); + expect( + workoutColorForValue( + 10, + { + ...DEFAULT_PROPS, + continuity: 'all', + rangeMin: 20, + rangeMax: 100, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBe('red'); + }); + + it('returns no color if the value if lower than rangeMin and continuity is nor "below" or "all"', () => { + expect( + workoutColorForValue( + 0, + { + ...DEFAULT_PROPS, + continuity: 'above', + rangeMin: 10, + rangeMax: 100, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBeUndefined(); + expect( + workoutColorForValue( + 0, + { + ...DEFAULT_PROPS, + continuity: 'none', + rangeMin: 10, + rangeMax: 100, + stops: [20, 40, 60, 80], + }, + { min: 0, max: 200 } + ) + ).toBeUndefined(); + }); + }); +}); diff --git a/src/plugins/charts/public/services/palettes/helpers.ts b/src/plugins/charts/public/services/palettes/helpers.ts new file mode 100644 index 00000000000000..d4b1e98f94cc8a --- /dev/null +++ b/src/plugins/charts/public/services/palettes/helpers.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CustomPaletteState } from '../..'; + +function findColorSegment( + value: number, + comparison: (value: number, bucket: number) => number, + colors: string[], + rangeMin: number, + rangeMax: number +) { + // assume uniform distribution within the provided range, can ignore stops + const step = (rangeMax - rangeMin) / colors.length; + + // what about values in range + const index = colors.findIndex((c, i) => comparison(value, rangeMin + (1 + i) * step) <= 0); + return colors[index] || colors[0]; +} + +function findColorsByStops( + value: number, + comparison: (value: number, bucket: number) => number, + colors: string[], + stops: number[] +) { + const index = stops.findIndex((s) => comparison(value, s) < 0); + return colors[index] || colors[0]; +} + +function getNormalizedValueByRange( + value: number, + { range }: CustomPaletteState, + minMax: { min: number; max: number } +) { + let result = value; + if (range === 'percent') { + result = (100 * (value - minMax.min)) / (minMax.max - minMax.min); + } + // for a range of 1 value the formulas above will divide by 0, so here's a safety guard + if (Number.isNaN(result)) { + return 1; + } + return result; +} + +/** + * When stops are empty, it is assumed a predefined palette, so colors are distributed uniformly in the whole data range + * When stops are passed, then rangeMin/rangeMax are used as reference for user defined limits: + * continuity is defined over rangeMin/rangeMax, not these stops values (rangeMin/rangeMax are computed from user's stop inputs) + */ +export function workoutColorForValue( + value: number | undefined, + params: CustomPaletteState, + minMax: { min: number; max: number } +) { + if (value == null) { + return; + } + const { colors, stops, range = 'percent', continuity = 'above', rangeMax, rangeMin } = params; + // ranges can be absolute numbers or percentages + // normalized the incoming value to the same format as range to make easier comparisons + const normalizedValue = getNormalizedValueByRange(value, params, minMax); + const dataRangeArguments = range === 'percent' ? [0, 100] : [minMax.min, minMax.max]; + const comparisonFn = (v: number, threshold: number) => v - threshold; + + // if steps are defined consider the specific rangeMax/Min as data boundaries + const maxRange = stops.length ? rangeMax : dataRangeArguments[1]; + const minRange = stops.length ? rangeMin : dataRangeArguments[0]; + + // in case of shorter rangers, extends the steps on the sides to cover the whole set + if (comparisonFn(normalizedValue, maxRange) > 0) { + if (continuity === 'above' || continuity === 'all') { + return colors[colors.length - 1]; + } + return; + } + if (comparisonFn(normalizedValue, minRange) < 0) { + if (continuity === 'below' || continuity === 'all') { + return colors[0]; + } + return; + } + + if (stops.length) { + return findColorsByStops(normalizedValue, comparisonFn, colors, stops); + } + + return findColorSegment( + normalizedValue, + comparisonFn, + colors, + dataRangeArguments[0], + dataRangeArguments[1] + ); +} diff --git a/src/plugins/charts/public/services/palettes/mock.ts b/src/plugins/charts/public/services/palettes/mock.ts index 1c112ec800c922..e94f47477ab11e 100644 --- a/src/plugins/charts/public/services/palettes/mock.ts +++ b/src/plugins/charts/public/services/palettes/mock.ts @@ -14,8 +14,8 @@ export const getPaletteRegistry = () => { const mockPalette1: jest.Mocked = { id: 'default', title: 'My Palette', - getColor: jest.fn((_: SeriesLayer[]) => 'black'), - getColors: jest.fn((num: number) => ['red', 'black']), + getCategoricalColor: jest.fn((_: SeriesLayer[]) => 'black'), + getCategoricalColors: jest.fn((num: number) => ['red', 'black']), toExpression: jest.fn(() => ({ type: 'expression', chain: [ @@ -33,8 +33,32 @@ export const getPaletteRegistry = () => { const mockPalette2: jest.Mocked = { id: 'mocked', title: 'Mocked Palette', - getColor: jest.fn((_: SeriesLayer[]) => 'blue'), - getColors: jest.fn((num: number) => ['blue', 'yellow']), + getCategoricalColor: jest.fn((_: SeriesLayer[]) => 'blue'), + getCategoricalColors: jest.fn((num: number) => ['blue', 'yellow']), + toExpression: jest.fn(() => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'system_palette', + arguments: { + name: ['mocked'], + }, + }, + ], + })), + }; + + const mockPalette3: jest.Mocked = { + id: 'custom', + title: 'Custom Mocked Palette', + getCategoricalColor: jest.fn((_: SeriesLayer[]) => 'blue'), + getCategoricalColors: jest.fn((num: number) => ['blue', 'yellow']), + getColorForValue: jest.fn( + (num: number | undefined, state: unknown, minMax: { min: number; max: number }) => + num == null || num < 1 ? undefined : 'blue' + ), + canDynamicColoring: true, toExpression: jest.fn(() => ({ type: 'expression', chain: [ @@ -50,8 +74,9 @@ export const getPaletteRegistry = () => { }; return { - get: (name: string) => (name !== 'default' ? mockPalette2 : mockPalette1), - getAll: () => [mockPalette1, mockPalette2], + get: (name: string) => + name === 'custom' ? mockPalette3 : name !== 'default' ? mockPalette2 : mockPalette1, + getAll: () => [mockPalette1, mockPalette2, mockPalette3], }; }; diff --git a/src/plugins/charts/public/services/palettes/palettes.test.tsx b/src/plugins/charts/public/services/palettes/palettes.test.tsx index 8f495df7f882af..8cb477b0e0838b 100644 --- a/src/plugins/charts/public/services/palettes/palettes.test.tsx +++ b/src/plugins/charts/public/services/palettes/palettes.test.tsx @@ -19,14 +19,14 @@ describe('palettes', () => { it('should return different colors based on behind text flag', () => { const palette = palettes.default; - const color1 = palette.getColor([ + const color1 = palette.getCategoricalColor([ { name: 'abc', rankAtDepth: 0, totalSeriesAtDepth: 5, }, ]); - const color2 = palette.getColor( + const color2 = palette.getCategoricalColor( [ { name: 'abc', @@ -44,14 +44,14 @@ describe('palettes', () => { it('should return different colors based on rank at current series', () => { const palette = palettes.default; - const color1 = palette.getColor([ + const color1 = palette.getCategoricalColor([ { name: 'abc', rankAtDepth: 0, totalSeriesAtDepth: 5, }, ]); - const color2 = palette.getColor([ + const color2 = palette.getCategoricalColor([ { name: 'abc', rankAtDepth: 1, @@ -64,7 +64,7 @@ describe('palettes', () => { it('should return the same color for different positions on outer series layers', () => { const palette = palettes.default; - const color1 = palette.getColor([ + const color1 = palette.getCategoricalColor([ { name: 'abc', rankAtDepth: 0, @@ -76,7 +76,7 @@ describe('palettes', () => { totalSeriesAtDepth: 2, }, ]); - const color2 = palette.getColor([ + const color2 = palette.getCategoricalColor([ { name: 'abc', rankAtDepth: 0, @@ -96,7 +96,7 @@ describe('palettes', () => { it('should return different colors based on behind text flag', () => { const palette = palettes.default; - const color1 = palette.getColor( + const color1 = palette.getCategoricalColor( [ { name: 'abc', @@ -108,7 +108,7 @@ describe('palettes', () => { syncColors: true, } ); - const color2 = palette.getColor( + const color2 = palette.getCategoricalColor( [ { name: 'abc', @@ -127,7 +127,7 @@ describe('palettes', () => { it('should return different colors for different keys', () => { const palette = palettes.default; - const color1 = palette.getColor( + const color1 = palette.getCategoricalColor( [ { name: 'abc', @@ -139,7 +139,7 @@ describe('palettes', () => { syncColors: true, } ); - const color2 = palette.getColor( + const color2 = palette.getCategoricalColor( [ { name: 'def', @@ -157,7 +157,7 @@ describe('palettes', () => { it('should return the same color for the same key, irregardless of rank', () => { const palette = palettes.default; - const color1 = palette.getColor( + const color1 = palette.getCategoricalColor( [ { name: 'hij', @@ -169,7 +169,7 @@ describe('palettes', () => { syncColors: true, } ); - const color2 = palette.getColor( + const color2 = palette.getCategoricalColor( [ { name: 'hij', @@ -187,7 +187,7 @@ describe('palettes', () => { it('should return the same color for different positions on outer series layers', () => { const palette = palettes.default; - const color1 = palette.getColor( + const color1 = palette.getCategoricalColor( [ { name: 'klm', @@ -204,7 +204,7 @@ describe('palettes', () => { syncColors: true, } ); - const color2 = palette.getColor( + const color2 = palette.getCategoricalColor( [ { name: 'klm', @@ -227,7 +227,7 @@ describe('palettes', () => { it('should return the same index of the behind text palette for same key', () => { const palette = palettes.default; - const color1 = palette.getColor( + const color1 = palette.getCategoricalColor( [ { name: 'klm', @@ -244,7 +244,7 @@ describe('palettes', () => { syncColors: true, } ); - const color2 = palette.getColor( + const color2 = palette.getCategoricalColor( [ { name: 'klm', @@ -273,15 +273,15 @@ describe('palettes', () => { const palette = palettes.warm; it('should use the whole gradient', () => { - const wholePalette = palette.getColors(10); - const color1 = palette.getColor([ + const wholePalette = palette.getCategoricalColors(10); + const color1 = palette.getCategoricalColor([ { name: 'abc', rankAtDepth: 0, totalSeriesAtDepth: 10, }, ]); - const color2 = palette.getColor([ + const color2 = palette.getCategoricalColor([ { name: 'def', rankAtDepth: 9, @@ -304,7 +304,7 @@ describe('palettes', () => { describe('syncColors: false', () => { it('should not query legacy color service', () => { - palette.getColor( + palette.getCategoricalColor( [ { name: 'abc', @@ -323,7 +323,7 @@ describe('palettes', () => { it('should respect the advanced settings color mapping', () => { const configColorGetter = colorsServiceMock.mappedColors.getColorFromConfig as jest.Mock; configColorGetter.mockImplementation(() => 'blue'); - const result = palette.getColor( + const result = palette.getCategoricalColor( [ { name: 'abc', @@ -345,7 +345,7 @@ describe('palettes', () => { }); it('should return a color from the legacy palette based on position of first series', () => { - const result = palette.getColor( + const result = palette.getCategoricalColor( [ { name: 'abc', @@ -368,7 +368,7 @@ describe('palettes', () => { describe('syncColors: true', () => { it('should query legacy color service', () => { - palette.getColor( + palette.getCategoricalColor( [ { name: 'abc', @@ -387,7 +387,7 @@ describe('palettes', () => { it('should respect the advanced settings color mapping', () => { const configColorGetter = colorsServiceMock.mappedColors.getColorFromConfig as jest.Mock; configColorGetter.mockImplementation(() => 'blue'); - const result = palette.getColor( + const result = palette.getCategoricalColor( [ { name: 'abc', @@ -409,7 +409,7 @@ describe('palettes', () => { }); it('should always use root series', () => { - palette.getColor( + palette.getCategoricalColor( [ { name: 'abc', @@ -437,7 +437,7 @@ describe('palettes', () => { describe('custom palette', () => { const palette = palettes.custom; it('should return different colors based on rank at current series', () => { - const color1 = palette.getColor( + const color1 = palette.getCategoricalColor( [ { name: 'abc', @@ -450,7 +450,7 @@ describe('palettes', () => { colors: ['#00ff00', '#000000'], } ); - const color2 = palette.getColor( + const color2 = palette.getCategoricalColor( [ { name: 'abc', @@ -467,7 +467,7 @@ describe('palettes', () => { }); it('should return the same color for different positions on outer series layers', () => { - const color1 = palette.getColor( + const color1 = palette.getCategoricalColor( [ { name: 'abc', @@ -485,7 +485,7 @@ describe('palettes', () => { colors: ['#00ff00', '#000000'], } ); - const color2 = palette.getColor( + const color2 = palette.getCategoricalColor( [ { name: 'abc', @@ -507,7 +507,7 @@ describe('palettes', () => { }); it('should use passed in colors', () => { - const color = palette.getColor( + const color = palette.getCategoricalColor( [ { name: 'abc', @@ -523,5 +523,56 @@ describe('palettes', () => { ); expect(color).toEqual('#00ff00'); }); + + // just an integration test here. More in depth tests on the subject can be found on the helper file + it('should return a color for the given value with its domain', () => { + expect( + palette.getColorForValue!( + 0, + { colors: ['red', 'green', 'blue'], stops: [], gradient: false }, + { min: 0, max: 100 } + ) + ).toBe('red'); + }); + + it('should return a color for the given value with its domain based on custom stops', () => { + expect( + palette.getColorForValue!( + 60, + { + colors: ['red', 'green', 'blue'], + stops: [10, 50, 100], + range: 'percent', + gradient: false, + rangeMin: 0, + rangeMax: 100, + }, + { min: 0, max: 100 } + ) + ).toBe('blue'); + }); + + // just make sure to not have broken anything + it('should work with only legacy arguments, filling with default values the new ones', () => { + expect(palette.toExpression({ colors: [], gradient: false })).toEqual({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'palette', + arguments: { + color: [], + gradient: [false], + reverse: [false], + continuity: ['above'], + stop: [], + range: ['percent'], + rangeMax: [], + rangeMin: [], + }, + }, + ], + }); + }); }); }); diff --git a/src/plugins/charts/public/services/palettes/palettes.tsx b/src/plugins/charts/public/services/palettes/palettes.tsx index b11d598c1c1cbc..65e3f9a84203dd 100644 --- a/src/plugins/charts/public/services/palettes/palettes.tsx +++ b/src/plugins/charts/public/services/palettes/palettes.tsx @@ -30,6 +30,7 @@ import { lightenColor } from './lighten_color'; import { ChartColorConfiguration, PaletteDefinition, SeriesLayer } from './types'; import { LegacyColorsService } from '../legacy_colors'; import { MappedColors } from '../mapped_colors'; +import { workoutColorForValue } from './helpers'; function buildRoundRobinCategoricalWithMappedColors(): Omit { const colors = euiPaletteColorBlind({ rotations: 2 }); @@ -64,8 +65,8 @@ function buildRoundRobinCategoricalWithMappedColors(): Omit euiPaletteColorBlind(), + getCategoricalColor: getColor, + getCategoricalColors: () => euiPaletteColorBlind(), toExpression: () => ({ type: 'expression', chain: [ @@ -102,8 +103,9 @@ function buildGradient( } return { id, - getColor, - getColors: colors, + getCategoricalColor: getColor, + getCategoricalColors: colors, + canDynamicColoring: true, toExpression: () => ({ type: 'expression', chain: [ @@ -141,8 +143,8 @@ function buildSyncedKibanaPalette( } return { id: 'kibana_palette', - getColor, - getColors: () => colors.seedColors.slice(0, 10), + getCategoricalColor: getColor, + getCategoricalColors: () => colors.seedColors.slice(0, 10), toExpression: () => ({ type: 'expression', chain: [ @@ -161,7 +163,24 @@ function buildSyncedKibanaPalette( function buildCustomPalette(): PaletteDefinition { return { id: 'custom', - getColor: ( + getColorForValue: ( + value, + params: { + colors: string[]; + range: 'number' | 'percent'; + continuity: 'above' | 'below' | 'none' | 'all'; + gradient: boolean; + /** Stops values mark where colors end (non-inclusive value) */ + stops: number[]; + /** Important: specify rangeMin/rangeMax if custom stops are defined! */ + rangeMax: number; + rangeMin: number; + }, + dataBounds + ) => { + return workoutColorForValue(value, params, dataBounds); + }, + getCategoricalColor: ( series: SeriesLayer[], chartConfiguration: ChartColorConfiguration = { behindText: false }, { colors, gradient }: { colors: string[]; gradient: boolean } @@ -179,10 +198,48 @@ function buildCustomPalette(): PaletteDefinition { }, internal: true, title: i18n.translate('charts.palettes.customLabel', { defaultMessage: 'Custom' }), - getColors: (size: number, { colors, gradient }: { colors: string[]; gradient: boolean }) => { + getCategoricalColors: ( + size: number, + { + colors, + gradient, + stepped, + stops, + }: { colors: string[]; gradient: boolean; stepped: boolean; stops: number[] } = { + colors: [], + gradient: false, + stepped: false, + stops: [], + } + ) => { + if (stepped) { + const range = stops[stops.length - 1] - stops[0]; + const offset = stops[0]; + const finalStops = [...stops.map((stop) => (stop - offset) / range)]; + return chroma.scale(colors).domain(finalStops).colors(size); + } return gradient ? chroma.scale(colors).colors(size) : colors; }, - toExpression: ({ colors, gradient }: { colors: string[]; gradient: boolean }) => ({ + canDynamicColoring: false, + toExpression: ({ + colors, + gradient, + stops = [], + rangeMax, + rangeMin, + rangeType = 'percent', + continuity = 'above', + reverse = false, + }: { + colors: string[]; + gradient: boolean; + stops: number[]; + rangeMax?: number; + rangeMin?: number; + rangeType: 'percent' | 'number'; + continuity?: 'all' | 'none' | 'above' | 'below'; + reverse?: boolean; + }) => ({ type: 'expression', chain: [ { @@ -191,6 +248,12 @@ function buildCustomPalette(): PaletteDefinition { arguments: { color: colors, gradient: [gradient], + reverse: [reverse], + continuity: [continuity], + stop: stops, + range: [rangeType], + rangeMax: rangeMax == null ? [] : [rangeMax], + rangeMin: rangeMin == null ? [] : [rangeMin], }, }, ], diff --git a/src/plugins/charts/public/services/palettes/types.ts b/src/plugins/charts/public/services/palettes/types.ts index 3d2a6b032f63e9..6f13f621783640 100644 --- a/src/plugins/charts/public/services/palettes/types.ts +++ b/src/plugins/charts/public/services/palettes/types.ts @@ -79,22 +79,12 @@ export interface PaletteDefinition { * @param state The internal state of the palette */ toExpression: (state?: T) => Ast; - /** - * Renders the UI for editing the internal state of the palette. - * Not each palette has to feature an internal state, so this is an optional property. - * @param domElement The dom element to the render the editor UI into - * @param props Current state and state setter to issue updates - */ - renderEditor?: ( - domElement: Element, - props: { state?: T; setState: (updater: (oldState: T) => T) => void } - ) => void; /** * Color a series according to the internal rules of the palette. * @param series The current series along with its ancestors. * @param state The internal state of the palette */ - getColor: ( + getCategoricalColor: ( series: SeriesLayer[], chartConfiguration?: ChartColorConfiguration, state?: T @@ -103,7 +93,20 @@ export interface PaletteDefinition { * Get a spectrum of colors of the current palette. * This can be used if the chart wants to control color assignment locally. */ - getColors: (size: number, state?: T) => string[]; + getCategoricalColors: (size: number, state?: T) => string[]; + /** + * Define whether a palette supports dynamic coloring (i.e. gradient colors mapped to number values) + */ + canDynamicColoring?: boolean; + /** + * Get the assigned color for the given value based on its data domain and state settings. + * This can be used for dynamic coloring based on uniform color distribution or custom stops. + */ + getColorForValue?: ( + value: number | undefined, + state: T, + { min, max }: { min: number; max: number } + ) => string | undefined; } export interface PaletteRegistry { diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index fa86fb81bd4073..93310bb8213615 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -295,13 +295,6 @@ export function DashboardApp({ }; }, [dashboardStateManager, dashboardContainer, onAppLeave, embeddable]); - // clear search session when leaving dashboard route - useEffect(() => { - return () => { - data.search.session.clear(); - }; - }, [data.search.session]); - return ( <> {savedDashboard && dashboardStateManager && dashboardContainer && viewMode && ( diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index d5eddf6bb48647..be279ed98492e7 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -198,8 +198,14 @@ export async function mountApp({ return ; }; - // make sure the index pattern list is up to date - await dataStart.indexPatterns.clearCache(); + const hasEmbeddableIncoming = Boolean( + dashboardServices.embeddable + .getStateTransfer() + .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, false) + ); + if (!hasEmbeddableIncoming) { + dataStart.indexPatterns.clearCache(); + } // dispatch synthetic hash change event to update hash history objects // this is necessary because hash updates triggered by using popState won't trigger this event naturally. @@ -242,7 +248,6 @@ export async function mountApp({ } render(app, element); return () => { - dataStart.search.session.clear(); unlistenParentHistory(); unmountComponentAtNode(element); appUnMounted(); diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts index 0be29f67a94922..d715fb70ec91a8 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts @@ -85,6 +85,7 @@ export const useDashboardContainer = ({ let canceled = false; let pendingContainer: DashboardContainer | ErrorEmbeddable | null | undefined; (async function createContainer() { + const existingSession = searchSession.getSessionId(); pendingContainer = await dashboardFactory.create( getDashboardContainerInput({ isEmbeddedExternally: Boolean(isEmbeddedExternally), @@ -92,7 +93,9 @@ export const useDashboardContainer = ({ dashboardStateManager, incomingEmbeddable, query, - searchSessionId: searchSessionIdFromURL ?? searchSession.start(), + searchSessionId: + searchSessionIdFromURL ?? + (existingSession && incomingEmbeddable ? existingSession : searchSession.start()), }) ); diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index db0404595af6c8..e2c11d614d7970 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -83,6 +83,11 @@ export const DashboardListing = ({ }; }, [title, savedObjectsClient, redirectTo, data.query, kbnUrlStateStorage]); + // clear dangling session because they are not required here + useEffect(() => { + data.search.session.clear(); + }, [data.search.session]); + const hideWriteControls = dashboardCapabilities.hideWriteControls; const listingLimit = savedObjects.settings.getListingLimit(); const defaultFilter = title ? `"${title}"` : ''; diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts b/src/plugins/data/common/es_query/es_query/build_es_query.test.ts index c6d923f4505f01..fa9a2c85aaef54 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.test.ts @@ -39,12 +39,16 @@ describe('build query', () => { { query: 'extension:jpg', language: 'kuery' }, { query: 'bar:baz', language: 'lucene' }, ] as Query[]; - const filters = [ - { - match_all: {}, - meta: { type: 'match_all' }, - } as MatchAllFilter, - ]; + const filters = { + match: { + a: 'b', + }, + meta: { + alias: '', + disabled: false, + negate: false, + }, + }; const config = { allowLeadingWildcards: true, queryStringOptions: {}, @@ -56,7 +60,11 @@ describe('build query', () => { must: [decorateQuery(luceneStringToDsl('bar:baz'), config.queryStringOptions)], filter: [ toElasticsearchQuery(fromKueryExpression('extension:jpg'), indexPattern), - { match_all: {} }, + { + match: { + a: 'b', + }, + }, ], should: [], must_not: [], @@ -71,9 +79,15 @@ describe('build query', () => { it('should accept queries and filters as either single objects or arrays', () => { const queries = { query: 'extension:jpg', language: 'lucene' } as Query; const filters = { - match_all: {}, - meta: { type: 'match_all' }, - } as MatchAllFilter; + match: { + a: 'b', + }, + meta: { + alias: '', + disabled: false, + negate: false, + }, + }; const config = { allowLeadingWildcards: true, queryStringOptions: {}, @@ -83,7 +97,13 @@ describe('build query', () => { const expectedResult = { bool: { must: [decorateQuery(luceneStringToDsl('extension:jpg'), config.queryStringOptions)], - filter: [{ match_all: {} }], + filter: [ + { + match: { + a: 'b', + }, + }, + ], should: [], must_not: [], }, @@ -94,6 +114,49 @@ describe('build query', () => { expect(result).toEqual(expectedResult); }); + it('should remove match_all clauses', () => { + const filters = [ + { + match_all: {}, + meta: { type: 'match_all' }, + } as MatchAllFilter, + { + match: { + a: 'b', + }, + meta: { + alias: '', + disabled: false, + negate: false, + }, + }, + ]; + const config = { + allowLeadingWildcards: true, + queryStringOptions: {}, + ignoreFilterIfFieldNotInIndex: false, + }; + + const expectedResult = { + bool: { + must: [], + filter: [ + { + match: { + a: 'b', + }, + }, + ], + should: [], + must_not: [], + }, + }; + + const result = buildEsQuery(indexPattern, [], filters, config); + + expect(result).toEqual(expectedResult); + }); + it('should use the default time zone set in the Advanced Settings in queries and filters', () => { const queries = [ { query: '@timestamp:"2019-03-23T13:18:00"', language: 'kuery' }, @@ -122,7 +185,6 @@ describe('build query', () => { indexPattern, config ), - { match_all: {} }, ], should: [], must_not: [], diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.ts b/src/plugins/data/common/es_query/es_query/build_es_query.ts index 18b360de9aaa64..45724796c3518c 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { groupBy, has } from 'lodash'; +import { groupBy, has, isEqual } from 'lodash'; import { buildQueryFromKuery } from './from_kuery'; import { buildQueryFromFilters } from './from_filters'; import { buildQueryFromLucene } from './from_lucene'; @@ -21,6 +21,12 @@ export interface EsQueryConfig { dateFormatTZ?: string; } +function removeMatchAll(filters: T[]) { + return filters.filter( + (filter) => !filter || typeof filter !== 'object' || !isEqual(filter, { match_all: {} }) + ); +} + /** * @param indexPattern * @param queries - a query object or array of query objects. Each query has a language property and a query property. @@ -63,9 +69,9 @@ export function buildEsQuery( return { bool: { - must: [...kueryQuery.must, ...luceneQuery.must, ...filterQuery.must], - filter: [...kueryQuery.filter, ...luceneQuery.filter, ...filterQuery.filter], - should: [...kueryQuery.should, ...luceneQuery.should, ...filterQuery.should], + must: removeMatchAll([...kueryQuery.must, ...luceneQuery.must, ...filterQuery.must]), + filter: removeMatchAll([...kueryQuery.filter, ...luceneQuery.filter, ...filterQuery.filter]), + should: removeMatchAll([...kueryQuery.should, ...luceneQuery.should, ...filterQuery.should]), must_not: [...kueryQuery.must_not, ...luceneQuery.must_not, ...filterQuery.must_not], }, }; diff --git a/src/plugins/data/common/field_formats/converters/duration.test.ts b/src/plugins/data/common/field_formats/converters/duration.test.ts index fc019720425dfd..72551f4b7b236f 100644 --- a/src/plugins/data/common/field_formats/converters/duration.test.ts +++ b/src/plugins/data/common/field_formats/converters/duration.test.ts @@ -139,17 +139,182 @@ describe('Duration Format', () => { ], }); + testCase({ + inputFormat: 'nanoseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 2, + showSuffix: true, + fixtures: [ + { + input: 1988, + output: '0.00 Milliseconds', + }, + { + input: 658, + output: '0.00 Milliseconds', + }, + { + input: 3857, + output: '0.00 Milliseconds', + }, + ], + }); + + testCase({ + inputFormat: 'microseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 2, + showSuffix: true, + fixtures: [ + { + input: 1988, + output: '1.99 Milliseconds', + }, + { + input: 658, + output: '0.66 Milliseconds', + }, + { + input: 3857, + output: '3.86 Milliseconds', + }, + ], + }); + + testCase({ + inputFormat: 'microseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 1, + showSuffix: true, + fixtures: [ + { + input: 1988, + output: '2.0 Milliseconds', + }, + { + input: 0, + output: '0.0 Milliseconds', + }, + { + input: 658, + output: '0.7 Milliseconds', + }, + { + input: 3857, + output: '3.9 Milliseconds', + }, + ], + }); + + testCase({ + inputFormat: 'seconds', + outputFormat: 'humanizePrecise', + outputPrecision: 0, + showSuffix: true, + fixtures: [ + { + input: 600, + output: '10 Minutes', + }, + { + input: 30, + output: '30 Seconds', + }, + { + input: 3000, + output: '50 Minutes', + }, + ], + }); + + testCase({ + inputFormat: 'milliseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 0, + showSuffix: true, + useShortSuffix: true, + fixtures: [ + { + input: -123, + output: '-123 ms', + }, + { + input: 1, + output: '1 ms', + }, + { + input: 600, + output: '600 ms', + }, + { + input: 30, + output: '30 ms', + }, + { + input: 3000, + output: '3 s', + }, + { + input: 300000, + output: '5 min', + }, + { + input: 30000000, + output: '8 h', + }, + { + input: 90000000, + output: '1 d', + }, + { + input: 9000000000, + output: '3 mon', + }, + { + input: 99999999999, + output: '3 y', + }, + ], + }); + + testCase({ + inputFormat: 'milliseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 0, + showSuffix: true, + useShortSuffix: true, + includeSpaceWithSuffix: false, + fixtures: [ + { + input: -123, + output: '-123ms', + }, + { + input: 1, + output: '1ms', + }, + { + input: 600, + output: '600ms', + }, + ], + }); + function testCase({ inputFormat, outputFormat, outputPrecision, showSuffix, + useShortSuffix, + includeSpaceWithSuffix, fixtures, }: { inputFormat: string; outputFormat: string; outputPrecision: number | undefined; showSuffix: boolean | undefined; + useShortSuffix?: boolean; + includeSpaceWithSuffix?: boolean; fixtures: any[]; }) { fixtures.forEach((fixture: Record) => { @@ -160,7 +325,14 @@ describe('Duration Format', () => { outputPrecision ? `, ${outputPrecision} decimals` : '' }`, () => { const duration = new DurationFormat( - { inputFormat, outputFormat, outputPrecision, showSuffix }, + { + inputFormat, + outputFormat, + outputPrecision, + showSuffix, + useShortSuffix, + includeSpaceWithSuffix, + }, jest.fn() ); expect(duration.convert(input)).toBe(output); diff --git a/src/plugins/data/common/field_formats/converters/duration.ts b/src/plugins/data/common/field_formats/converters/duration.ts index ef8c1df3704a83..c9a7091db84716 100644 --- a/src/plugins/data/common/field_formats/converters/duration.ts +++ b/src/plugins/data/common/field_formats/converters/duration.ts @@ -18,6 +18,7 @@ const ratioToSeconds: Record = { microseconds: 0.000001, }; const HUMAN_FRIENDLY = 'humanize'; +const HUMAN_FRIENDLY_PRECISE = 'humanizePrecise'; const DEFAULT_OUTPUT_PRECISION = 2; const DEFAULT_INPUT_FORMAT = { text: i18n.translate('data.fieldFormats.duration.inputFormats.seconds', { @@ -89,59 +90,89 @@ const inputFormats = [ }, ]; const DEFAULT_OUTPUT_FORMAT = { - text: i18n.translate('data.fieldFormats.duration.outputFormats.humanize', { - defaultMessage: 'Human Readable', + text: i18n.translate('data.fieldFormats.duration.outputFormats.humanize.approximate', { + defaultMessage: 'Human-readable (approximate)', }), method: 'humanize', }; const outputFormats = [ { ...DEFAULT_OUTPUT_FORMAT }, + { + text: i18n.translate('data.fieldFormats.duration.outputFormats.humanize.precise', { + defaultMessage: 'Human-readable (precise)', + }), + method: 'humanizePrecise', + }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asMilliseconds', { defaultMessage: 'Milliseconds', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asMilliseconds.short', { + defaultMessage: 'ms', + }), method: 'asMilliseconds', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asSeconds', { defaultMessage: 'Seconds', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asSeconds.short', { + defaultMessage: 's', + }), method: 'asSeconds', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asMinutes', { defaultMessage: 'Minutes', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asMinutes.short', { + defaultMessage: 'min', + }), method: 'asMinutes', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asHours', { defaultMessage: 'Hours', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asHours.short', { + defaultMessage: 'h', + }), method: 'asHours', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asDays', { defaultMessage: 'Days', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asDays.short', { + defaultMessage: 'd', + }), method: 'asDays', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asWeeks', { defaultMessage: 'Weeks', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asWeeks.short', { + defaultMessage: 'w', + }), method: 'asWeeks', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asMonths', { defaultMessage: 'Months', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asMonths.short', { + defaultMessage: 'mon', + }), method: 'asMonths', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asYears', { defaultMessage: 'Years', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asYears.short', { + defaultMessage: 'y', + }), method: 'asYears', }, ]; @@ -154,6 +185,29 @@ function parseInputAsDuration(val: number, inputFormat: string) { return moment.duration(val * ratio, kind); } +function formatInputHumanPrecise( + val: number, + inputFormat: string, + outputPrecision: number, + useShortSuffix: boolean, + includeSpace: string +) { + const ratio = ratioToSeconds[inputFormat] || 1; + const kind = (inputFormat in ratioToSeconds + ? 'seconds' + : inputFormat) as unitOfTime.DurationConstructor; + const valueInDuration = moment.duration(val * ratio, kind); + + return formatDuration( + val, + valueInDuration, + inputFormat, + outputPrecision, + useShortSuffix, + includeSpace + ); +} + export class DurationFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.DURATION; static title = i18n.translate('data.fieldFormats.duration.title', { @@ -167,11 +221,17 @@ export class DurationFormat extends FieldFormat { isHuman() { return this.param('outputFormat') === HUMAN_FRIENDLY; } + + isHumanPrecise() { + return this.param('outputFormat') === HUMAN_FRIENDLY_PRECISE; + } + getParamDefaults() { return { inputFormat: DEFAULT_INPUT_FORMAT.kind, outputFormat: DEFAULT_OUTPUT_FORMAT.method, outputPrecision: DEFAULT_OUTPUT_PRECISION, + includeSpaceWithSuffix: true, }; } @@ -180,19 +240,84 @@ export class DurationFormat extends FieldFormat { const outputFormat = this.param('outputFormat') as keyof Duration; const outputPrecision = this.param('outputPrecision'); const showSuffix = Boolean(this.param('showSuffix')); + const useShortSuffix = Boolean(this.param('useShortSuffix')); + const includeSpaceWithSuffix = this.param('includeSpaceWithSuffix'); + + const includeSpace = includeSpaceWithSuffix ? ' ' : ''; + const human = this.isHuman(); + const humanPrecise = this.isHumanPrecise(); + const prefix = val < 0 && human ? i18n.translate('data.fieldFormats.duration.negativeLabel', { defaultMessage: 'minus', }) + ' ' : ''; + const duration = parseInputAsDuration(val, inputFormat) as Record; - const formatted = duration[outputFormat](); - const precise = human ? formatted : formatted.toFixed(outputPrecision); + const formatted = humanPrecise + ? formatInputHumanPrecise(val, inputFormat, outputPrecision, useShortSuffix, includeSpace) + : duration[outputFormat](); + + const precise = human || humanPrecise ? formatted : formatted.toFixed(outputPrecision); const type = outputFormats.find(({ method }) => method === outputFormat); - const suffix = showSuffix && type ? ` ${type.text}` : ''; - return prefix + precise + suffix; + const unitText = useShortSuffix ? type?.shortText : type?.text; + + const suffix = showSuffix && unitText && !human ? `${includeSpace}${unitText}` : ''; + + return humanPrecise ? precise : prefix + precise + suffix; }; } + +function formatDuration( + val: number, + duration: moment.Duration, + inputFormat: string, + outputPrecision: number, + useShortSuffix: boolean, + includeSpace: string +) { + // return nothing when the duration is falsy or not correctly parsed (P0D) + if (!duration || !duration.isValid()) return; + const units = [ + { unit: duration.years(), nextUnitRate: 12, method: 'asYears' }, + { unit: duration.months(), nextUnitRate: 4, method: 'asMonths' }, + { unit: duration.weeks(), nextUnitRate: 7, method: 'asWeeks' }, + { unit: duration.days(), nextUnitRate: 24, method: 'asDays' }, + { unit: duration.hours(), nextUnitRate: 60, method: 'asHours' }, + { unit: duration.minutes(), nextUnitRate: 60, method: 'asMinutes' }, + { unit: duration.seconds(), nextUnitRate: 1000, method: 'asSeconds' }, + { unit: duration.milliseconds(), nextUnitRate: 1000, method: 'asMilliseconds' }, + ]; + + const getUnitText = (method: string) => { + const type = outputFormats.find(({ method: methodT }) => method === methodT); + return useShortSuffix ? type?.shortText : type?.text; + }; + + for (let i = 0; i < units.length; i++) { + const unitValue = units[i].unit; + if (unitValue >= 1) { + const unitText = getUnitText(units[i].method); + + const value = Math.floor(unitValue); + if (units?.[i + 1]) { + const decimalPointValue = Math.floor(units[i + 1].unit); + return ( + (value + decimalPointValue / units[i].nextUnitRate).toFixed(outputPrecision) + + includeSpace + + unitText + ); + } else { + return unitValue.toFixed(outputPrecision) + includeSpace + unitText; + } + } + } + + const unitValue = units[units.length - 1].unit; + const unitText = getUnitText(units[units.length - 1].method); + + return unitValue.toFixed(outputPrecision) + includeSpace + unitText; +} 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 13a1a1bd388ba6..39680c49483667 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -98,14 +98,6 @@ describe('Session service', () => { expect(nowProvider.reset).toHaveBeenCalled(); }); - it("Can't clear other apps' session", async () => { - sessionService.start(); - expect(sessionService.getSessionId()).not.toBeUndefined(); - currentAppId$.next('change'); - sessionService.clear(); - expect(sessionService.getSessionId()).not.toBeUndefined(); - }); - it("Can start a new session in case there is other apps' stale session", async () => { const s1 = sessionService.start(); expect(sessionService.getSessionId()).not.toBeUndefined(); diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index 71f51b4bc8d83c..629d76b07d7caf 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -128,21 +128,6 @@ export class SessionService { this.subscription.add( coreStart.application.currentAppId$.subscribe((newAppName) => { this.currentApp = newAppName; - if (!this.getSessionId()) return; - - // Apps required to clean up their sessions before unmounting - // Make sure that apps don't leave sessions open by throwing an error in DEV mode - const message = `Application '${ - this.state.get().appName - }' had an open session while navigating`; - if (initializerContext.env.mode.dev) { - coreStart.fatalErrors.add(message); - } else { - // this should never happen in prod because should be caught in dev mode - // in case this happen we don't want to throw fatal error, as most likely possible bugs are not that critical - // eslint-disable-next-line no-console - console.warn(message); - } }) ); }); @@ -230,18 +215,6 @@ export class SessionService { * Cleans up current state */ public clear() { - // make sure apps can't clear other apps' sessions - const currentSessionApp = this.state.get().appName; - if (currentSessionApp && currentSessionApp !== this.currentApp) { - // eslint-disable-next-line no-console - console.warn( - `Skip clearing session "${this.getSessionId()}" because it belongs to a different app. current: "${ - this.currentApp - }", owner: "${currentSessionApp}"` - ); - return; - } - this.state.transitions.clear(); this.searchSessionInfoProvider = undefined; this.searchSessionIndicatorUiConfig = undefined; diff --git a/src/plugins/discover/public/__mocks__/ui_settings.ts b/src/plugins/discover/public/__mocks__/ui_settings.ts index e021a39a568e99..8347ff18edd7db 100644 --- a/src/plugins/discover/public/__mocks__/ui_settings.ts +++ b/src/plugins/discover/public/__mocks__/ui_settings.ts @@ -7,7 +7,7 @@ */ import { IUiSettingsClient } from 'kibana/public'; -import { DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING } from '../../common'; +import { DEFAULT_COLUMNS_SETTING, DOC_TABLE_LEGACY, SAMPLE_SIZE_SETTING } from '../../common'; export const uiSettingsMock = ({ get: (key: string) => { @@ -15,6 +15,8 @@ export const uiSettingsMock = ({ return 10; } else if (key === DEFAULT_COLUMNS_SETTING) { return ['default_column']; + } else if (key === DOC_TABLE_LEGACY) { + return true; } }, } as unknown) as IUiSettingsClient; diff --git a/src/plugins/discover/public/application/angular/context.html b/src/plugins/discover/public/application/angular/context.html index 2c8e9a2a5d6f0b..adafb3a62275ff 100644 --- a/src/plugins/discover/public/application/angular/context.html +++ b/src/plugins/discover/public/application/angular/context.html @@ -2,8 +2,9 @@ anchor-id="contextAppRoute.anchorId" columns="contextAppRoute.state.columns" index-pattern="contextAppRoute.indexPattern" + app-state="contextAppRoute.state" + state-container="contextAppRoute.stateContainer" filters="contextAppRoute.filters" predecessor-count="contextAppRoute.state.predecessorCount" successor-count="contextAppRoute.state.successorCount" - sort="contextAppRoute.state.sort" -> + sort="contextAppRoute.state.sort"> \ No newline at end of file diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js index 01a28a5c174b6b..10c0fe9db19507 100644 --- a/src/plugins/discover/public/application/angular/context.js +++ b/src/plugins/discover/public/application/angular/context.js @@ -15,19 +15,12 @@ import { getState } from './context_state'; import contextAppRouteTemplate from './context.html'; import { getRootBreadcrumbs } from '../helpers/breadcrumbs'; -const k7Breadcrumbs = ($route) => { - const { indexPattern } = $route.current.locals; - const { id } = $route.current.params; - +const k7Breadcrumbs = () => { return [ ...getRootBreadcrumbs(), { text: i18n.translate('discover.context.breadcrumb', { - defaultMessage: 'Context of {indexPatternTitle}#{docId}', - values: { - indexPatternTitle: indexPattern.title, - docId: id, - }, + defaultMessage: 'Surrounding documents', }), }, ]; @@ -51,6 +44,14 @@ getAngularModule().config(($routeProvider) => { function ContextAppRouteController($routeParams, $scope, $route) { const filterManager = getServices().filterManager; const indexPattern = $route.current.locals.indexPattern.ip; + const stateContainer = getState({ + defaultStepSize: getServices().uiSettings.get(CONTEXT_DEFAULT_SIZE_SETTING), + timeFieldName: indexPattern.timeFieldName, + storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'), + history: getServices().history(), + toasts: getServices().core.notifications.toasts, + uiSettings: getServices().core.uiSettings, + }); const { startSync: startStateSync, stopSync: stopStateSync, @@ -59,14 +60,8 @@ function ContextAppRouteController($routeParams, $scope, $route) { setFilters, setAppState, flushToUrl, - } = getState({ - defaultStepSize: getServices().uiSettings.get(CONTEXT_DEFAULT_SIZE_SETTING), - timeFieldName: indexPattern.timeFieldName, - storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'), - history: getServices().history(), - toasts: getServices().core.notifications.toasts, - uiSettings: getServices().core.uiSettings, - }); + } = stateContainer; + this.stateContainer = stateContainer; this.state = { ...appState.getState() }; this.anchorId = $routeParams.id; this.indexPattern = indexPattern; diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts index 62c9a2a5e3b908..4da8ddc7980036 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts @@ -8,22 +8,21 @@ import { EsQuerySortValue, SortDirection } from '../../../../../../data/public'; import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; -import { AnchorHitRecord, fetchAnchorProvider } from './anchor'; +import { fetchAnchorProvider } from './anchor'; +import { EsHitRecord, EsHitRecordList } from './context'; describe('context app', function () { let fetchAnchor: ( indexPatternId: string, anchorId: string, sort: EsQuerySortValue[] - ) => Promise; + ) => Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any let searchSourceStub: any; describe('function fetchAnchor', function () { beforeEach(() => { - searchSourceStub = createSearchSourceStub([ - { _id: 'hit1', fields: [], sort: [], _source: {} }, - ]); + searchSourceStub = createSearchSourceStub(([{ _id: 'hit1' }] as unknown) as EsHitRecordList); fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub); }); @@ -139,16 +138,14 @@ describe('context app', function () { { _doc: SortDirection.desc }, ]).then((anchorDocument) => { expect(anchorDocument).toHaveProperty('property1', 'value1'); - expect(anchorDocument).toHaveProperty('$$_isAnchor', true); + expect(anchorDocument).toHaveProperty('isAnchor', true); }); }); }); describe('useNewFields API', () => { beforeEach(() => { - searchSourceStub = createSearchSourceStub([ - { _id: 'hit1', fields: [], sort: [], _source: {} }, - ]); + searchSourceStub = createSearchSourceStub(([{ _id: 'hit1' }] as unknown) as EsHitRecordList); fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub, true); }); diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.ts b/src/plugins/discover/public/application/angular/context/api/anchor.ts index da81ce525331ac..f2111d020aade8 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.ts +++ b/src/plugins/discover/public/application/angular/context/api/anchor.ts @@ -16,11 +16,6 @@ import { } from '../../../../../../data/public'; import { EsHitRecord } from './context'; -export interface AnchorHitRecord extends EsHitRecord { - // eslint-disable-next-line @typescript-eslint/naming-convention - $$_isAnchor: boolean; -} - export function fetchAnchorProvider( indexPatterns: IndexPatternsContract, searchSource: ISearchSource, @@ -30,7 +25,7 @@ export function fetchAnchorProvider( indexPatternId: string, anchorId: string, sort: EsQuerySortValue[] - ): Promise { + ): Promise { const indexPattern = await indexPatterns.get(indexPatternId); searchSource .setParent(undefined) @@ -66,8 +61,7 @@ export function fetchAnchorProvider( return { ...get(response, ['hits', 'hits', 0]), - // eslint-disable-next-line @typescript-eslint/naming-convention - $$_isAnchor: true, - } as AnchorHitRecord; + isAnchor: true, + } as EsHitRecord; }; } diff --git a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts index dc097bc110e205..1acf57411c7959 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts @@ -11,7 +11,7 @@ import { get, last } from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; import { EsHitRecordList, fetchContextProvider } from './context'; import { setServices, SortDirection } from '../../../../kibana_services'; -import { AnchorHitRecord } from './anchor'; +import { EsHitRecord } from './context'; import { Query } from '../../../../../../data/public'; import { DiscoverServices } from '../../../../build_services'; @@ -75,7 +75,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( 'predecessors', indexPatternId, - anchor as AnchorHitRecord, + anchor as EsHitRecord, timeField, tieBreakerField, sortDir, @@ -267,7 +267,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( 'predecessors', indexPatternId, - anchor as AnchorHitRecord, + anchor as EsHitRecord, timeField, tieBreakerField, sortDir, diff --git a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts index f8fc7eb343206b..957a13e8daf093 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts @@ -13,7 +13,7 @@ import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs import { setServices, SortDirection } from '../../../../kibana_services'; import { Query } from '../../../../../../data/public'; import { EsHitRecordList, fetchContextProvider } from './context'; -import { AnchorHitRecord } from './anchor'; +import { EsHitRecord } from './context'; import { DiscoverServices } from '../../../../build_services'; const MS_PER_DAY = 24 * 60 * 60 * 1000; @@ -75,7 +75,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( 'successors', indexPatternId, - anchor as AnchorHitRecord, + anchor as EsHitRecord, timeField, tieBreakerField, sortDir, @@ -270,7 +270,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( 'successors', indexPatternId, - anchor as AnchorHitRecord, + anchor as EsHitRecord, timeField, tieBreakerField, sortDir, diff --git a/src/plugins/discover/public/application/angular/context/api/context.ts b/src/plugins/discover/public/application/angular/context/api/context.ts index 4309b9ca4c391b..cd81ca7b216b27 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { estypes } from '@elastic/elasticsearch'; import { Filter, IndexPatternsContract, IndexPattern } from 'src/plugins/data/public'; import { reverseSortDir, SortDirection } from './utils/sorting'; import { extractNanos, convertIsoToMillis } from './utils/date_conversion'; @@ -14,17 +15,19 @@ import { generateIntervals } from './utils/generate_intervals'; import { getEsQuerySearchAfter } from './utils/get_es_query_search_after'; import { getEsQuerySort } from './utils/get_es_query_sort'; import { getServices } from '../../../../kibana_services'; -import { AnchorHitRecord } from './anchor'; export type SurrDocType = 'successors' | 'predecessors'; -export interface EsHitRecord { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fields: Record; - sort: number[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - _source: Record; - _id: string; -} +export type EsHitRecord = Required< + Pick< + estypes.SearchResponse['hits']['hits'][number], + '_id' | 'fields' | 'sort' | '_index' | '_version' + > +> & { + _source?: Record; + _score?: number; + isAnchor?: boolean; +}; + export type EsHitRecordList = EsHitRecord[]; const DAY_MILLIS = 24 * 60 * 60 * 1000; @@ -53,7 +56,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields async function fetchSurroundingDocs( type: SurrDocType, indexPatternId: string, - anchor: AnchorHitRecord, + anchor: EsHitRecord, timeField: string, tieBreakerField: string, sortDir: SortDirection, @@ -71,7 +74,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields const timeValueMillis = nanos !== '' ? convertIsoToMillis(anchor.fields[timeField][0]) : anchor.sort[0]; - const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis, type, sortDir); + const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis as number, type, sortDir); let documents: EsHitRecordList = []; for (const interval of intervals) { diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index fb0e58832a202e..c703abaf2e5237 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -28,23 +28,23 @@ export function getEsQuerySearchAfter( // already surrounding docs -> first or last record is used const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; - let afterTimeValue: string | number = afterTimeDoc.sort[0]; + let afterTimeValue = afterTimeDoc.sort[0] as string | number; if (nanoSeconds) { afterTimeValue = useNewFieldsApi - ? (afterTimeDoc.fields[timeFieldName] as Array)[0] - : (afterTimeDoc._source[timeFieldName] as string | number); + ? afterTimeDoc.fields[timeFieldName][0] + : afterTimeDoc._source?.[timeFieldName]; } - return [afterTimeValue, afterTimeDoc.sort[1]]; + return [afterTimeValue, afterTimeDoc.sort[1] as string | number]; } // if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser // ES search_after also works when number is provided as string const searchAfter = new Array(2) as EsQuerySearchAfter; - searchAfter[0] = anchor.sort[0]; + searchAfter[0] = anchor.sort[0] as string | number; if (nanoSeconds) { searchAfter[0] = useNewFieldsApi - ? (anchor.fields[timeFieldName] as Array)[0] - : (anchor._source[timeFieldName] as string | number); + ? anchor.fields[timeFieldName][0] + : anchor._source?.[timeFieldName]; } - searchAfter[1] = anchor.sort[1]; + searchAfter[1] = anchor.sort[1] as string | number; return searchAfter; } diff --git a/src/plugins/discover/public/application/angular/context/query/actions.tsx b/src/plugins/discover/public/application/angular/context/query/actions.tsx index 52c56d379d2591..f79c28bf6a1206 100644 --- a/src/plugins/discover/public/application/angular/context/query/actions.tsx +++ b/src/plugins/discover/public/application/angular/context/query/actions.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { getServices } from '../../../../kibana_services'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; import { MarkdownSimple, toMountPoint } from '../../../../../../kibana_react/public'; -import { AnchorHitRecord, fetchAnchorProvider } from '../api/anchor'; +import { fetchAnchorProvider } from '../api/anchor'; import { EsHitRecord, EsHitRecordList, fetchContextProvider, SurrDocType } from '../api/context'; import { getQueryParameterActions } from '../query_parameters'; import { @@ -77,11 +77,12 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { } setLoadingStatus(state)('anchor'); + const [[, sortDir]] = sort; return Promise.try(() => - fetchAnchor(indexPatternId, anchorId, [fromPairs([sort]), { [tieBreakerField]: sort[1] }]) + fetchAnchor(indexPatternId, anchorId, [fromPairs(sort), { [tieBreakerField]: sortDir }]) ).then( - (anchorDocument: AnchorHitRecord) => { + (anchorDocument: EsHitRecord) => { setLoadedStatus(state)('anchor'); state.rows.anchor = anchorDocument; return anchorDocument; @@ -120,7 +121,7 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { } setLoadingStatus(state)(type); - const [sortField, sortDir] = sort; + const [[sortField, sortDir]] = sort; return Promise.try(() => fetchSurroundingDocs( diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts b/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts index b54f11e9e67062..fac3e1ea6fad6e 100644 --- a/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts +++ b/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts @@ -10,6 +10,7 @@ import { getQueryParameterActions } from './actions'; import { FilterManager, SortDirection } from '../../../../../../data/public'; import { coreMock } from '../../../../../../../core/public/mocks'; import { ContextAppState, LoadingStatus, QueryParameters } from '../../context_app_state'; +import { EsHitRecord } from '../api/context'; const setupMock = coreMock.createSetup(); let state: ContextAppState; @@ -29,7 +30,7 @@ beforeEach(() => { anchorId: '', columns: [], filters: [], - sort: ['field', SortDirection.asc], + sort: [['field', SortDirection.asc]], tieBreakerField: '', }, loadingStatus: { @@ -39,8 +40,7 @@ beforeEach(() => { }, rows: { all: [], - // eslint-disable-next-line @typescript-eslint/naming-convention - anchor: { $$_isAnchor: true, fields: [], sort: [], _source: [], _id: '' }, + anchor: ({ isAnchor: true, fields: [], sort: [], _id: '' } as unknown) as EsHitRecord, predecessors: [], successors: [], }, @@ -129,7 +129,7 @@ describe('context query_parameter actions', function () { indexPatternId: 'INDEX_PATTERN', predecessorCount: 100, successorCount: 100, - sort: ['field', SortDirection.asc], + sort: [['field', SortDirection.asc]], tieBreakerField: '', }); @@ -142,7 +142,7 @@ describe('context query_parameter actions', function () { indexPatternId: 'INDEX_PATTERN', predecessorCount: 100, successorCount: 100, - sort: ['field', SortDirection.asc], + sort: [['field', SortDirection.asc]], tieBreakerField: '', }); }); diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index 3d731459ad8d72..21aad2688d2a3c 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -3,11 +3,14 @@ filter="contextApp.actions.addFilter" hits="contextApp.state.rows.all" index-pattern="contextApp.indexPattern" + app-state="contextApp.appState" + state-container="contextApp.stateContainer" sorting="contextApp.state.queryParameters.sort" columns="contextApp.state.queryParameters.columns" minimum-visible-rows="contextApp.state.rows.all.length" - status="contextApp.state.loadingStatus.anchor.status" - reason="contextApp.state.loadingStatus.anchor.reason" + anchor-id="contextApp.anchorId" + anchor-status="contextApp.state.loadingStatus.anchor.status" + anchor-reason="contextApp.state.loadingStatus.anchor.reason" default-step-size="contextApp.state.queryParameters.defaultStepSize" predecessor-count="contextApp.state.queryParameters.predecessorCount" predecessor-available="contextApp.state.rows.predecessors.length" @@ -18,5 +21,4 @@ successor-status="contextApp.state.loadingStatus.successors.status" on-change-successor-count="contextApp.actions.fetchGivenSuccessorRows" use-new-fields-api="contextApp.state.useNewFieldsApi" - top-nav-menu="contextApp.topNavMenu" -> + top-nav-menu="contextApp.topNavMenu"> \ No newline at end of file diff --git a/src/plugins/discover/public/application/angular/context_app.js b/src/plugins/discover/public/application/angular/context_app.js index a90904fa2ccea8..7c9c5f8ce4b425 100644 --- a/src/plugins/discover/public/application/angular/context_app.js +++ b/src/plugins/discover/public/application/angular/context_app.js @@ -34,6 +34,8 @@ getAngularModule().directive('contextApp', function ContextApp() { anchorId: '=', columns: '=', indexPattern: '=', + appState: '=', + stateContainer: '=', filters: '=', predecessorCount: '=', successorCount: '=', @@ -55,7 +57,6 @@ function ContextAppController($scope, Private) { ); this.state.useNewFieldsApi = useNewFieldsApi; this.topNavMenu = navigation.ui.TopNavMenu; - this.actions = _.mapValues( { ...queryParameterActions, diff --git a/src/plugins/discover/public/application/angular/context_app_state.ts b/src/plugins/discover/public/application/angular/context_app_state.ts index 1593b2457019ce..0d9d6d6ea59780 100644 --- a/src/plugins/discover/public/application/angular/context_app_state.ts +++ b/src/plugins/discover/public/application/angular/context_app_state.ts @@ -7,7 +7,7 @@ */ import { Filter } from '../../../../data/public'; -import { AnchorHitRecord } from './context/api/anchor'; +import { EsHitRecord } from './context/api/context'; import { EsHitRecordList } from './context/api/context'; import { SortDirection } from './context/api/utils/sorting'; @@ -48,13 +48,13 @@ export interface QueryParameters { indexPatternId: string; predecessorCount: number; successorCount: number; - sort: [string, SortDirection]; + sort: Array<[string, SortDirection]>; tieBreakerField: string; } interface ContextRows { all: EsHitRecordList; - anchor: AnchorHitRecord; + anchor: EsHitRecord; predecessors: EsHitRecordList; successors: EsHitRecordList; } diff --git a/src/plugins/discover/public/application/angular/context_state.test.ts b/src/plugins/discover/public/application/angular/context_state.test.ts index ed4a74c70112bd..e9294567032c49 100644 --- a/src/plugins/discover/public/application/angular/context_state.test.ts +++ b/src/plugins/discover/public/application/angular/context_state.test.ts @@ -45,8 +45,10 @@ describe('Test Discover Context State', () => { "filters": Array [], "predecessorCount": 4, "sort": Array [ - "time", - "desc", + Array [ + "time", + "desc", + ], ], "successorCount": 4, } @@ -60,7 +62,7 @@ describe('Test Discover Context State', () => { state.setAppState({ predecessorCount: 10 }); state.flushToUrl(); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_a=(columns:!(_source),filters:!(),predecessorCount:10,sort:!(time,desc),successorCount:4)"` + `"/#?_a=(columns:!(_source),filters:!(),predecessorCount:10,sort:!(!(time,desc)),successorCount:4)"` ); }); test('getState -> url to appState syncing', async () => { @@ -183,7 +185,7 @@ describe('Test Discover Context State', () => { `); state.flushToUrl(); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!f,params:(query:jpg),type:phrase),query:(match:(extension:(query:jpg,type:phrase))))))&_a=(columns:!(_source),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!t,params:(query:png),type:phrase),query:(match:(extension:(query:png,type:phrase))))),predecessorCount:4,sort:!(time,desc),successorCount:4)"` + `"/#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!f,params:(query:jpg),type:phrase),query:(match:(extension:(query:jpg,type:phrase))))))&_a=(columns:!(_source),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!t,params:(query:png),type:phrase),query:(match:(extension:(query:png,type:phrase))))),predecessorCount:4,sort:!(!(time,desc)),successorCount:4)"` ); }); }); diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index d60f2e655c4eb5..9cfea7f01e4ab8 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -35,7 +35,7 @@ export interface AppState { /** * Sorting of the records to be fetched, assumed to be a legacy parameter */ - sort: string[]; + sort: string[][]; /** * Number of records to be fetched after the anchor records (older records) */ @@ -50,7 +50,7 @@ interface GlobalState { filters: Filter[]; } -interface GetStateParams { +export interface GetStateParams { /** * Number of records to be fetched when 'Load' link/button is clicked */ @@ -81,7 +81,7 @@ interface GetStateParams { uiSettings: IUiSettingsClient; } -interface GetStateReturn { +export interface GetStateReturn { /** * Global state, the _g part of the URL */ @@ -276,7 +276,7 @@ function createInitialAppState( columns: ['_source'], filters: [], predecessorCount: parseInt(defaultSize, 10), - sort: [timeFieldName, 'desc'], + sort: [[timeFieldName, 'desc']], successorCount: parseInt(defaultSize, 10), }; if (typeof urlState !== 'object') { diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index fadaffde5c5c31..fa3656d1529d28 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -10,13 +10,13 @@ opts="opts" reset-query="resetQuery" result-state="resultState" + fetch-status="fetchStatus" rows="rows" search-source="volatileSearchSource" state="state" top-nav-menu="topNavMenu" use-new-fields-api="useNewFieldsApi" unmapped-fields-config="unmappedFieldsConfig" - refresh-app-state="refreshAppState" - > + refresh-app-state="refreshAppState"> - + \ No newline at end of file diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts index 8028aa6c086343..0907844aa1c54d 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts @@ -8,7 +8,14 @@ import { Capabilities, IUiSettingsClient } from 'kibana/public'; import { popularizeField } from '../../../helpers/popularize_field'; import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services'; -import { AppState } from '../../discover_state'; +import { + AppState as DiscoverState, + GetStateReturn as DiscoverGetStateReturn, +} from '../../discover_state'; +import { + AppState as ContextState, + GetStateReturn as ContextGetStateReturn, +} from '../../context_state'; import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; /** @@ -67,8 +74,8 @@ export function getStateColumnActions({ indexPattern: IndexPattern; indexPatterns: IndexPatternsContract; useNewFieldsApi: boolean; - setAppState: (state: Partial) => void; - state: AppState; + setAppState: DiscoverGetStateReturn['setAppState'] | ContextGetStateReturn['setAppState']; + state: DiscoverState | ContextState; }) { function onAddColumn(columnName: string) { if (capabilities.discover.save) { diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.html b/src/plugins/discover/public/application/angular/doc_table/doc_table.html index 4f297643a28f70..ecd7aa8f3dcf46 100644 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.html +++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.html @@ -95,8 +95,8 @@ index-pattern="indexPattern" filter="filter" class="kbnDocTable__row" - ng-class="{'kbnDocTable__row--highlight': row['$$_isAnchor']}" - data-test-subj="docTableRow{{ row['$$_isAnchor'] ? ' docTableAnchorRow' : ''}}" + ng-class="{'kbnDocTable__row--highlight': row['isAnchor']}" + data-test-subj="docTableRow{{ row['isAnchor'] ? ' docTableAnchorRow' : ''}}" on-add-column="onAddColumn" on-remove-column="onRemoveColumn" use-new-fields-api="useNewFieldsApi" diff --git a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts index f2377b61b51512..2029354376f261 100644 --- a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts +++ b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts @@ -34,11 +34,12 @@ export function createInfiniteScrollDirective() { const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; const usedScrollDiv = isMobileView ? scrollDivMobile : scrollDiv; const scrollTop = usedScrollDiv.scrollTop(); + const scrollOffset = usedScrollDiv.prop('offsetTop') || 0; const winHeight = Number(usedScrollDiv.height()); const winBottom = Number(winHeight) + Number(scrollTop); const elTop = $element.get(0).offsetTop || 0; - const remaining = elTop - winBottom; + const remaining = elTop - scrollOffset - winBottom; if (remaining <= winHeight) { $scope[$scope.$$phase ? '$eval' : '$apply'](function () { diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss new file mode 100644 index 00000000000000..9ff36ca4527429 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss @@ -0,0 +1,24 @@ +@import '../../../../../../core/public/mixins'; + +.dscDocsPage { + @include kibanaFullBodyHeight(54px); // action bar height +} + +.dscDocsContent { + display: flex; + flex-direction: column; + height: 100%; +} + +.dscDocsGrid { + flex: 1 1 100%; + overflow: auto; + + &__cell--highlight { + background-color: tintOrShade($euiColorPrimary, 90%, 70%); + } + + .euiDataGridRowCell.euiDataGridRowCell--firstColumn { + padding: 0; + } +} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx index 63845ab97b9540..7d947d8412be5c 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx @@ -7,14 +7,34 @@ */ import React from 'react'; -import { ContextAppLegacy } from './context_app_legacy'; -import { IIndexPattern } from '../../../../../data/common/index_patterns'; import { mountWithIntl } from '@kbn/test/jest'; +import { uiSettingsMock as mockUiSettings } from '../../../__mocks__/ui_settings'; +import { IndexPattern } from '../../../../../data/common/index_patterns'; +import { ContextAppLegacy } from './context_app_legacy'; import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; import { ContextErrorMessage } from '../context_error_message'; import { TopNavMenuMock } from './__mocks__/top_nav_menu'; +import { AppState, GetStateReturn } from '../../angular/context_state'; +import { SortDirection } from 'src/plugins/data/common'; +import { EsHitRecordList } from '../../angular/context/api/context'; + +jest.mock('../../../kibana_services', () => { + return { + getServices: () => ({ + metadata: { + branch: 'test', + }, + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: mockUiSettings, + }), + }; +}); describe('ContextAppLegacy test', () => { const hit = { @@ -35,16 +55,19 @@ describe('ContextAppLegacy test', () => { }; const indexPattern = { id: 'test_index_pattern', - } as IIndexPattern; + } as IndexPattern; const defaultProps = { columns: ['_source'], filter: () => {}, - hits: [hit], - sorting: ['order_date', 'desc'], + hits: ([hit] as unknown) as EsHitRecordList, + sorting: [['order_date', 'desc']] as Array<[string, SortDirection]>, minimumVisibleRows: 5, indexPattern, - status: 'loaded', - reason: 'no reason', + appState: ({} as unknown) as AppState, + stateContainer: ({} as unknown) as GetStateReturn, + anchorId: 'test_anchor_id', + anchorStatus: 'loaded', + anchorReason: 'no reason', defaultStepSize: 5, predecessorCount: 10, successorCount: 10, @@ -55,6 +78,8 @@ describe('ContextAppLegacy test', () => { predecessorStatus: 'loaded', successorStatus: 'loaded', topNavMenu: TopNavMenuMock, + useNewFieldsApi: false, + isPaginationEnabled: false, }; const topNavProps = { appName: 'context', @@ -80,7 +105,7 @@ describe('ContextAppLegacy test', () => { it('renders loading indicator', () => { const props = { ...defaultProps }; - props.status = 'loading'; + props.anchorStatus = 'loading'; const component = mountWithIntl(); expect(component.find(DocTableLegacy).length).toBe(0); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); @@ -91,8 +116,8 @@ describe('ContextAppLegacy test', () => { it('renders error message', () => { const props = { ...defaultProps }; - props.status = 'failed'; - props.reason = 'something went wrong'; + props.anchorStatus = 'failed'; + props.anchorReason = 'something went wrong'; const component = mountWithIntl(); expect(component.find(DocTableLegacy).length).toBe(0); expect(component.find(TopNavMenuMock).length).toBe(0); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx index 55c2208105f136..1251687805af19 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx @@ -6,29 +6,43 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useState, Fragment } from 'react'; +import classNames from 'classnames'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage } from '@elastic/eui'; +import './context_app_legacy.scss'; +import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; +import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY } from '../../../../common'; import { ContextErrorMessage } from '../context_error_message'; import { DocTableLegacy, DocTableLegacyProps, } from '../../angular/doc_table/create_doc_table_react'; -import { IIndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; +import { IndexPattern } from '../../../../../data/common/index_patterns'; import { LoadingStatus } from '../../angular/context_app_state'; import { ActionBar, ActionBarProps } from '../../angular/context/components/action_bar/action_bar'; import { TopNavMenuProps } from '../../../../../navigation/public'; +import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid'; +import { DocViewFilterFn } from '../../doc_views/doc_views_types'; +import { getServices, SortDirection } from '../../../kibana_services'; +import { GetStateReturn, AppState } from '../../angular/context_state'; +import { useDataGridColumns } from '../../helpers/use_data_grid_columns'; +import { EsHitRecord, EsHitRecordList } from '../../angular/context/api/context'; export interface ContextAppProps { topNavMenu: React.ComponentType; columns: string[]; - hits: Array>; - indexPattern: IIndexPattern; - filter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + hits: EsHitRecordList; + indexPattern: IndexPattern; + appState: AppState; + stateContainer: GetStateReturn; + filter: DocViewFilterFn; minimumVisibleRows: number; - sorting: string[]; - status: string; - reason: string; + sorting: Array<[string, SortDirection]>; + anchorId: string; + anchorStatus: string; + anchorReason: string; + predecessorStatus: string; + successorStatus: string; defaultStepSize: number; predecessorCount: number; successorCount: number; @@ -36,11 +50,10 @@ export interface ContextAppProps { successorAvailable: number; onChangePredecessorCount: (count: number) => void; onChangeSuccessorCount: (count: number) => void; - predecessorStatus: string; - successorStatus: string; useNewFieldsApi?: boolean; } +const DataGridMemoized = React.memo(DiscoverGrid); const PREDECESSOR_TYPE = 'predecessors'; const SUCCESSOR_TYPE = 'successors'; @@ -49,9 +62,36 @@ function isLoading(status: string) { } export function ContextAppLegacy(renderProps: ContextAppProps) { - const status = renderProps.status; - const isLoaded = status === LoadingStatus.LOADED; - const isFailed = status === LoadingStatus.FAILED; + const services = getServices(); + const { uiSettings: config, capabilities, indexPatterns } = services; + const { + indexPattern, + anchorId, + anchorStatus, + predecessorStatus, + successorStatus, + appState, + stateContainer, + hits: rows, + sorting, + filter, + minimumVisibleRows, + useNewFieldsApi, + } = renderProps; + const [expandedDoc, setExpandedDoc] = useState(undefined); + const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED; + const isFailed = anchorStatus === LoadingStatus.FAILED; + const isLegacy = config.get(DOC_TABLE_LEGACY); + + const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ + capabilities, + config, + indexPattern, + indexPatterns, + setAppState: stateContainer.setAppState, + state: appState, + useNewFieldsApi: !!useNewFieldsApi, + }); const actionBarProps = (type: string) => { const { @@ -60,8 +100,6 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { predecessorCount, predecessorAvailable, successorAvailable, - predecessorStatus, - successorStatus, onChangePredecessorCount, onChangeSuccessorCount, } = renderProps; @@ -73,27 +111,44 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { onChangeCount: isPredecessorType ? onChangePredecessorCount : onChangeSuccessorCount, isLoading: isPredecessorType ? isLoading(predecessorStatus) : isLoading(successorStatus), type, - isDisabled: !isLoaded, + isDisabled: !isAnchorLoaded, } as ActionBarProps; }; const docTableProps = () => { - const { - hits, - filter, - sorting, + return { + ariaLabelledBy: 'surDocumentsAriaLabel', columns, + rows, indexPattern, - minimumVisibleRows, + expandedDoc, + isLoading: isLoading(anchorStatus), + sampleSize: 0, + sort: sorting, + isSortEnabled: false, + showTimeCol: !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, + services, useNewFieldsApi, - } = renderProps; + isPaginationEnabled: false, + controlColumnIds: ['openDetails'], + setExpandedDoc, + onFilter: filter, + onAddColumn, + onRemoveColumn, + onSetColumns, + } as DiscoverGridProps; + }; + + const legacyDocTableProps = () => { // @ts-expect-error doesn't implement full DocTableLegacyProps interface return { columns, indexPattern, minimumVisibleRows, - rows: hits, + rows, onFilter: filter, + onAddColumn, + onRemoveColumn, sort: sorting.map((el) => [el]), useNewFieldsApi, } as DocTableLegacyProps; @@ -114,7 +169,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { }; const loadingFeedback = () => { - if (status === LoadingStatus.UNINITIALIZED || status === LoadingStatus.LOADING) { + if (anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING) { return ( @@ -127,25 +182,42 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { return ( {isFailed ? ( - + ) : ( -

+ - - + + + + + + + + + - {loadingFeedback()} + {isLegacy && loadingFeedback()} - {isLoaded ? ( -
- + {isLegacy ? ( + isAnchorLoaded && ( +
+ +
+ ) + ) : ( +
+
- ) : null} + )} -
+
)} ); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts index fc64abfb510258..767ab8c94d80fe 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts @@ -14,11 +14,14 @@ export function createContextAppLegacy(reactDirective: any) { ['filter', { watchDepth: 'reference' }], ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], + ['appState', { watchDepth: 'reference' }], + ['stateContainer', { watchDepth: 'reference' }], ['sorting', { watchDepth: 'reference' }], ['columns', { watchDepth: 'collection' }], ['minimumVisibleRows', { watchDepth: 'reference' }], - ['status', { watchDepth: 'reference' }], - ['reason', { watchDepth: 'reference' }], + ['anchorId', { watchDepth: 'reference' }], + ['anchorStatus', { watchDepth: 'reference' }], + ['anchorReason', { watchDepth: 'reference' }], ['defaultStepSize', { watchDepth: 'reference' }], ['predecessorCount', { watchDepth: 'reference' }], ['predecessorAvailable', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts index f8c74c07457aa1..049c9ac177eeab 100644 --- a/src/plugins/discover/public/application/components/create_discover_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_directive.ts @@ -20,6 +20,7 @@ export function createDiscoverDirective(reactDirective: any) { ['opts', { watchDepth: 'reference' }], ['resetQuery', { watchDepth: 'reference' }], ['resultState', { watchDepth: 'reference' }], + ['fetchStatus', { watchDepth: 'reference' }], ['rows', { watchDepth: 'reference' }], ['savedSearch', { watchDepth: 'reference' }], ['searchSource', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 90dfd2ef9dce9d..f962c56cc4690c 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -34,9 +34,12 @@ import { esFilters, IndexPatternField, search } from '../../../../data/public'; import { DiscoverSidebarResponsive } from './sidebar'; import { DiscoverProps } from './types'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; +import { + DOC_HIDE_TIME_COLUMN_SETTING, + DOC_TABLE_LEGACY, + SEARCH_FIELDS_FROM_SOURCE, +} from '../../../common'; import { popularizeField } from '../helpers/popularize_field'; -import { getStateColumnActions } from '../angular/doc_table/actions/columns'; import { DocViewFilterFn } from '../doc_views/doc_views_types'; import { DiscoverGrid } from './discover_grid/discover_grid'; import { DiscoverTopNav } from './discover_topnav'; @@ -44,6 +47,7 @@ import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { setBreadcrumbsTitle } from '../helpers/breadcrumbs'; import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; import { InspectorSession } from '../../../../inspector/public'; +import { useDataGridColumns } from '../helpers/use_data_grid_columns'; const DocTableLegacyMemoized = React.memo(DocTableLegacy); const SidebarMemoized = React.memo(DiscoverSidebarResponsive); @@ -96,7 +100,7 @@ export function Discover({ }, [opts.chartAggConfigs]); const contentCentered = resultState === 'uninitialized'; - const isLegacy = services.uiSettings.get('doc_table:legacy'); + const isLegacy = services.uiSettings.get(DOC_TABLE_LEGACY); const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); const updateQuery = useCallback( (_payload, isUpdate?: boolean) => { @@ -108,6 +112,16 @@ export function Discover({ [opts] ); + const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useDataGridColumns({ + capabilities, + config, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, + }); + useEffect(() => { const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : ''; chrome.docTitle.change(`Discover${pageTitleSuffix}`); @@ -116,20 +130,6 @@ export function Discover({ addHelpMenuToAppChrome(chrome, docLinks); }, [savedSearch, chrome, docLinks]); - const { onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useMemo( - () => - getStateColumnActions({ - capabilities, - config, - indexPattern, - indexPatterns, - setAppState, - state, - useNewFieldsApi, - }), - [capabilities, config, indexPattern, indexPatterns, setAppState, state, useNewFieldsApi] - ); - const onOpenInspector = useCallback(() => { // prevent overlapping setExpandedDoc(undefined); @@ -225,12 +225,6 @@ export function Discover({ } }; - const columns = useMemo(() => { - if (!state.columns) { - return []; - } - return useNewFieldsApi ? state.columns.filter((col) => col !== '_source') : state.columns; - }, [state, useNewFieldsApi]); return ( @@ -439,13 +433,13 @@ export function Discover({ searchTitle={opts.savedSearch.lastSavedTitle} setExpandedDoc={setExpandedDoc} showTimeCol={ - !config.get('doc_table:hideTimeColumn', false) && + !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName } services={services} settings={state.grid} - onAddColumn={onAddColumn} onFilter={onAddFilter as DocViewFilterFn} + onAddColumn={onAddColumn} onRemoveColumn={onRemoveColumn} onSetColumns={onSetColumns} onSort={onSort} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss index cb1b9a8ea191ea..053b405b90acb2 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss @@ -66,6 +66,7 @@ text-align: right; } +.euiDataGrid__loading, .euiDataGrid__noResults { display: flex; flex-direction: column; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index f969eb32f3791a..65a6ee80564e9f 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -14,11 +14,12 @@ import { EuiDataGridStyle, EuiDataGridProps, EuiDataGrid, - EuiIcon, EuiScreenReaderOnly, EuiSpacer, EuiText, htmlIdGenerator, + EuiLoadingSpinner, + EuiIcon, } from '@elastic/eui'; import { IndexPattern } from '../../../kibana_services'; import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; @@ -88,9 +89,9 @@ export interface DiscoverGridProps { */ onSetColumns: (columns: string[]) => void; /** - * function to change sorting of the documents + * function to change sorting of the documents, skipped when isSortEnabled is set to false */ - onSort: (sort: string[][]) => void; + onSort?: (sort: string[][]) => void; /** * Array of documents provided by Elasticsearch */ @@ -123,6 +124,10 @@ export interface DiscoverGridProps { * Determines whether the time columns should be displayed (legacy settings) */ showTimeCol: boolean; + /** + * Manage user sorting control + */ + isSortEnabled?: boolean; /** * Current sort setting */ @@ -131,6 +136,14 @@ export interface DiscoverGridProps { * How the data is fetched */ useNewFieldsApi: boolean; + /** + * Manage pagination control + */ + isPaginationEnabled?: boolean; + /** + * List of used control columns (available: 'openDetails', 'select') + */ + controlColumnIds?: string[]; } export const EuiDataGridMemoized = React.memo((props: EuiDataGridProps) => { @@ -159,6 +172,9 @@ export const DiscoverGrid = ({ showTimeCol, sort, useNewFieldsApi, + isSortEnabled = true, + isPaginationEnabled = true, + controlColumnIds = ['openDetails', 'select'], }: DiscoverGridProps) => { const [selectedDocs, setSelectedDocs] = useState([]); const [isFilterActive, setIsFilterActive] = useState(false); @@ -210,14 +226,16 @@ export const DiscoverGrid = ({ const onChangePage = (pageIndex: number) => setPagination((paginationData) => ({ ...paginationData, pageIndex })); - return { - onChangeItemsPerPage, - onChangePage, - pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex, - pageSize: pagination.pageSize, - pageSizeOptions: pageSizeArr, - }; - }, [pagination, pageCount]); + return isPaginationEnabled + ? { + onChangeItemsPerPage, + onChangePage, + pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex, + pageSize: pagination.pageSize, + pageSizeOptions: pageSizeArr, + } + : undefined; + }, [pagination, pageCount, isPaginationEnabled]); /** * Sorting @@ -226,9 +244,11 @@ export const DiscoverGrid = ({ const onTableSort = useCallback( (sortingColumnsData) => { - onSort(sortingColumnsData.map(({ id, direction }: SortObj) => [id, direction])); + if (isSortEnabled && onSort) { + onSort(sortingColumnsData.map(({ id, direction }: SortObj) => [id, direction])); + } }, - [onSort] + [onSort, isSortEnabled] ); /** @@ -253,8 +273,16 @@ export const DiscoverGrid = ({ const randomId = useMemo(() => htmlIdGenerator()(), []); const euiGridColumns = useMemo( - () => getEuiGridColumns(displayedColumns, settings, indexPattern, showTimeCol, defaultColumns), - [displayedColumns, indexPattern, showTimeCol, settings, defaultColumns] + () => + getEuiGridColumns( + displayedColumns, + settings, + indexPattern, + showTimeCol, + defaultColumns, + isSortEnabled + ), + [displayedColumns, indexPattern, showTimeCol, settings, defaultColumns, isSortEnabled] ); const schemaDetectors = useMemo(() => getSchemaDetectors(), []); const columnsVisibility = useMemo( @@ -266,11 +294,16 @@ export const DiscoverGrid = ({ }), [displayedColumns, indexPattern, showTimeCol, onSetColumns] ); - const sorting = useMemo(() => ({ columns: sortingColumns, onSort: onTableSort }), [ - sortingColumns, - onTableSort, - ]); - const lead = useMemo(() => getLeadControlColumns(), []); + const sorting = useMemo(() => { + if (isSortEnabled) { + return { columns: sortingColumns, onSort: onTableSort }; + } + return { columns: sortingColumns, onSort: () => {} }; + }, [sortingColumns, onTableSort, isSortEnabled]); + const lead = useMemo( + () => getLeadControlColumns().filter(({ id }) => controlColumnIds.includes(id)), + [controlColumnIds] + ); const additionalControls = useMemo( () => @@ -286,6 +319,18 @@ export const DiscoverGrid = ({ [usedSelectedDocs, isFilterActive, rows, setIsFilterActive] ); + if (!rowCount && isLoading) { + return ( +
+ + + + + +
+ ); + } + if (!rowCount) { return (
@@ -348,10 +393,12 @@ export const DiscoverGrid = ({ ? { ...toolbarVisibility, showColumnSelector: false, + showSortSelector: isSortEnabled, additionalControls, } : { ...toolbarVisibility, + showSortSelector: isSortEnabled, additionalControls, } } diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx index 93b5bf8fde0c18..3cbac90aa39cb3 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx @@ -12,7 +12,14 @@ import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_ describe('Discover grid columns ', function () { it('returns eui grid columns without time column', async () => { - const actual = getEuiGridColumns(['extension', 'message'], {}, indexPatternMock, false, false); + const actual = getEuiGridColumns( + ['extension', 'message'], + {}, + indexPatternMock, + false, + false, + true + ); expect(actual).toMatchInlineSnapshot(` Array [ Object { @@ -54,6 +61,7 @@ describe('Discover grid columns ', function () { {}, indexPatternWithTimefieldMock, false, + true, true ); expect(actual).toMatchInlineSnapshot(` @@ -94,7 +102,8 @@ describe('Discover grid columns ', function () { {}, indexPatternWithTimefieldMock, true, - false + false, + true ); expect(actual).toMatchInlineSnapshot(` Array [ diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx index df7e2285a07544..3a27772662b56f 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx @@ -53,7 +53,8 @@ export function buildEuiGridColumn( columnName: string, columnWidth: number | undefined = 0, indexPattern: IndexPattern, - defaultColumns: boolean + defaultColumns: boolean, + isSortEnabled: boolean ) { const timeString = i18n.translate('discover.timeLabel', { defaultMessage: 'Time', @@ -62,7 +63,7 @@ export function buildEuiGridColumn( const column: EuiDataGridColumn = { id: columnName, schema: getSchemaByKbnType(indexPatternField?.type), - isSortable: indexPatternField?.sortable === true, + isSortable: isSortEnabled && indexPatternField?.sortable === true, display: columnName === '_source' ? i18n.translate('discover.grid.documentHeader', { @@ -100,7 +101,8 @@ export function getEuiGridColumns( settings: DiscoverGridSettings | undefined, indexPattern: IndexPattern, showTimeCol: boolean, - defaultColumns: boolean + defaultColumns: boolean, + isSortEnabled: boolean ) { const timeFieldName = indexPattern.timeFieldName; const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0; @@ -108,12 +110,12 @@ export function getEuiGridColumns( if (showTimeCol && indexPattern.timeFieldName && !columns.find((col) => col === timeFieldName)) { const usedColumns = [indexPattern.timeFieldName, ...columns]; return usedColumns.map((column) => - buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns) + buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns, isSortEnabled) ); } return columns.map((column) => - buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns) + buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns, isSortEnabled) ); } diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx index 9ebe3ee95f7974..41cf3f5a68edbf 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx @@ -51,7 +51,14 @@ describe('document selection', () => { const component = mountWithIntl( - + ); @@ -73,7 +80,14 @@ describe('document selection', () => { const component = mountWithIntl( - + ); @@ -95,7 +109,14 @@ describe('document selection', () => { const component = mountWithIntl( - + ); @@ -117,7 +138,14 @@ describe('document selection', () => { const component = mountWithIntl( - + ); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx index 4aaefc99479c1d..03c17c801fa963 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx @@ -5,7 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useCallback, useState, useContext, useMemo } from 'react'; +import React, { useCallback, useState, useContext, useMemo, useEffect } from 'react'; +import classNames from 'classnames'; import { EuiButtonEmpty, EuiContextMenuItem, @@ -13,9 +14,11 @@ import { EuiCopy, EuiPopover, EuiCheckbox, + EuiDataGridCellValueElementProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import classNames from 'classnames'; +import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import themeLight from '@elastic/eui/dist/eui_theme_light.json'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; @@ -27,11 +30,25 @@ export const getDocId = (doc: ElasticSearchHit & { _routing?: string }) => { const routing = doc._routing ? doc._routing : ''; return [doc._index, doc._id, routing].join('::'); }; -export const SelectButton = ({ rowIndex }: { rowIndex: number }) => { - const ctx = useContext(DiscoverGridContext); - const doc = useMemo(() => ctx.rows[rowIndex], [ctx.rows, rowIndex]); +export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => { + const { selectedDocs, expanded, rows, isDarkMode, setSelectedDocs } = useContext( + DiscoverGridContext + ); + const doc = useMemo(() => rows[rowIndex], [rows, rowIndex]); const id = useMemo(() => getDocId(doc), [doc]); - const checked = useMemo(() => ctx.selectedDocs.includes(id), [ctx.selectedDocs, id]); + const checked = useMemo(() => selectedDocs.includes(id), [selectedDocs, id]); + + useEffect(() => { + if (expanded && doc && expanded._id === doc._id) { + setCellProps({ + style: { + backgroundColor: isDarkMode ? themeDark.euiColorHighlight : themeLight.euiColorHighlight, + }, + }); + } else { + setCellProps({ style: undefined }); + } + }, [expanded, doc, setCellProps, isDarkMode]); return ( { data-test-subj={`dscGridSelectDoc-${id}`} onChange={() => { if (checked) { - const newSelection = ctx.selectedDocs.filter((docId) => docId !== id); - ctx.setSelectedDocs(newSelection); + const newSelection = selectedDocs.filter((docId) => docId !== id); + setSelectedDocs(newSelection); } else { - ctx.setSelectedDocs([...ctx.selectedDocs, id]); + setSelectedDocs([...selectedDocs, id]); } }} /> @@ -96,19 +113,6 @@ export function DiscoverGridDocumentToolbarBtn({ /> ), - - { - setIsSelectionPopoverOpen(false); - setSelectedDocs([]); - setIsFilterActive(false); - }} - > - - , )} , + { + setIsSelectionPopoverOpen(false); + setSelectedDocs([]); + setIsFilterActive(false); + }} + > + + , ]; }, [ isFilterActive, diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx index 115acb84b95d80..b11733c1595206 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx @@ -12,6 +12,7 @@ import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; import themeLight from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { DiscoverGridContext } from './discover_grid_context'; +import { EsHitRecord } from '../../angular/context/api/context'; /** * Button to expand a given row */ @@ -19,7 +20,11 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle const { expanded, setExpanded, rows, isDarkMode } = useContext(DiscoverGridContext); const current = rows[rowIndex]; useEffect(() => { - if (expanded && current && expanded._id === current._id) { + if ((current as EsHitRecord).isAnchor) { + setCellProps({ + className: 'dscDocsGrid__cell--highlight', + }); + } else if (expanded && current && expanded._id === current._id) { setCellProps({ style: { backgroundColor: isDarkMode ? themeDark.euiColorHighlight : themeLight.euiColorHighlight, diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index fc3dd499f92e08..b3c205e072508f 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -21,6 +21,7 @@ import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; import { JsonCodeEditor } from '../json_code_editor/json_code_editor'; import { defaultMonacoEditorWidth } from './constants'; +import { EsHitRecord } from '../../angular/context/api/context'; export const getRenderCellValueFn = ( indexPattern: IndexPattern, @@ -38,7 +39,11 @@ export const getRenderCellValueFn = ( const ctx = useContext(DiscoverGridContext); useEffect(() => { - if (ctx.expanded && row && ctx.expanded._id === row._id) { + if ((row as EsHitRecord).isAnchor) { + setCellProps({ + className: 'dscDocsGrid__cell--highlight', + }); + } else if (ctx.expanded && row && ctx.expanded._id === row._id) { setCellProps({ style: { backgroundColor: ctx.isDarkMode diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 99ecb4c11eef2c..1e3c7e77d36150 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -33,6 +33,7 @@ import { getServices, IndexPattern, ISearchSource } from '../../kibana_services' import { SEARCH_EMBEDDABLE_TYPE } from './constants'; import { SavedSearch } from '../..'; import { + DOC_HIDE_TIME_COLUMN_SETTING, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, SORT_DEFAULT_ORDER_SETTING, @@ -256,7 +257,7 @@ export class SearchEmbeddable if (this.savedSearch.grid) { searchScope.settings = this.savedSearch.grid; } - searchScope.showTimeCol = !this.services.uiSettings.get('doc_table:hideTimeColumn', false); + searchScope.showTimeCol = !this.services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false); searchScope.filter = async (field, value, operator) => { let filters = esFilters.generateFilters( diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx b/src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx new file mode 100644 index 00000000000000..c9e1899aff8de6 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx @@ -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 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 { renderHook } from '@testing-library/react-hooks'; +import { useDataGridColumns } from './use_data_grid_columns'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { configMock } from '../../__mocks__/config'; +import { indexPatternsMock } from '../../__mocks__/index_patterns'; +import { AppState } from '../angular/context_state'; +import { Capabilities } from '../../../../../core/types'; + +describe('useDataGridColumns', () => { + const defaultProps = { + capabilities: ({ discover: { save: true } } as unknown) as Capabilities, + config: configMock, + indexPattern: indexPatternMock, + indexPatterns: indexPatternsMock, + setAppState: () => {}, + state: { + columns: ['Time', 'message'], + } as AppState, + useNewFieldsApi: false, + }; + + test('should return valid result', () => { + const { result } = renderHook(() => { + return useDataGridColumns(defaultProps); + }); + + expect(result.current.columns).toEqual(['Time', 'message']); + expect(result.current.onAddColumn).toBeInstanceOf(Function); + expect(result.current.onRemoveColumn).toBeInstanceOf(Function); + expect(result.current.onMoveColumn).toBeInstanceOf(Function); + expect(result.current.onSetColumns).toBeInstanceOf(Function); + }); + + test('should skip _source column when useNewFieldsApi is set to true', () => { + const { result } = renderHook(() => { + return useDataGridColumns({ + ...defaultProps, + state: { + columns: ['Time', '_source'], + }, + useNewFieldsApi: true, + }); + }); + + expect(result.current.columns).toEqual(['Time']); + }); + + test('should return empty columns array', () => { + const { result } = renderHook(() => { + return useDataGridColumns({ + ...defaultProps, + state: { + columns: [], + }, + }); + }); + expect(result.current.columns).toEqual([]); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts new file mode 100644 index 00000000000000..c913b9abd1b433 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useMemo } from 'react'; + +import { Capabilities, IUiSettingsClient } from 'kibana/public'; +import { IndexPattern, IndexPatternsContract } from '../../kibana_services'; +import { + AppState as DiscoverState, + GetStateReturn as DiscoverGetStateReturn, +} from '../angular/discover_state'; +import { + AppState as ContextState, + GetStateReturn as ContextGetStateReturn, +} from '../angular/context_state'; +import { getStateColumnActions } from '../angular/doc_table/actions/columns'; + +interface UseDataGridColumnsProps { + capabilities: Capabilities; + config: IUiSettingsClient; + indexPattern: IndexPattern; + indexPatterns: IndexPatternsContract; + useNewFieldsApi: boolean; + setAppState: DiscoverGetStateReturn['setAppState'] | ContextGetStateReturn['setAppState']; + state: DiscoverState | ContextState; +} + +export const useDataGridColumns = ({ + capabilities, + config, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, +}: UseDataGridColumnsProps) => { + const { onAddColumn, onRemoveColumn, onSetColumns, onMoveColumn } = useMemo( + () => + getStateColumnActions({ + capabilities, + config, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, + }), + [capabilities, config, indexPattern, indexPatterns, setAppState, state, useNewFieldsApi] + ); + + const columns = useMemo(() => { + if (!state.columns) { + return []; + } + return useNewFieldsApi ? state.columns.filter((col) => col !== '_source') : state.columns; + }, [state, useNewFieldsApi]); + + return { + columns, + onAddColumn, + onRemoveColumn, + onMoveColumn, + onSetColumns, + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap index cb7949deda64f6..d000af4453cd14 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap @@ -1,5 +1,184 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`DurationFormatEditor should not render show suffix on dynamic output 1`] = ` + + + } + labelType="label" + > + + + + } + labelType="label" + > + + + + } + labelType="label" + > + + + + + } + onChange={[Function]} + /> + + + + } + onChange={[Function]} + /> + + + +`; + exports[`DurationFormatEditor should render human readable output normally 1`] = ` + + + } + onChange={[Function]} + /> + + + + } + onChange={[Function]} + /> + true, + isHumanPrecise: () => false, type: { inputFormats: [ { @@ -78,6 +81,7 @@ describe('DurationFormatEditor', () => { inputFormat: 'seconds', outputFormat: 'asMinutes', outputPrecision: 10, + includeSpaceWithSuffix: true, }; }), isHuman: () => false, @@ -91,6 +95,55 @@ describe('DurationFormatEditor', () => { onError={onError} /> ); + const labels = component.find(EuiSwitch); + expect(labels.length).toEqual(3); + expect(labels.get(0).props.label.props.defaultMessage).toEqual('Show suffix'); + expect(labels.get(1).props.label.props.defaultMessage).toEqual('Use short suffix'); + expect(labels.get(2).props.label.props.defaultMessage).toEqual( + 'Include space between suffix and value' + ); + + expect(component).toMatchSnapshot(); + }); + + it('should not render show suffix on dynamic output', async () => { + const newFormat = { + ...format, + getParamDefaults: jest.fn().mockImplementation(() => { + return { + inputFormat: 'seconds', + outputFormat: 'dynamic', + outputPrecision: 2, + includeSpaceWithSuffix: true, + }; + }), + isHuman: () => false, + isHumanPrecise: () => true, + }; + const component = shallow( + + ); + + const labels = component.find(EuiSwitch); + expect(labels.length).toEqual(2); + const useShortSuffixSwitch = labels.get(0); + + expect(useShortSuffixSwitch.props.label.props.defaultMessage).toEqual('Use short suffix'); + expect(useShortSuffixSwitch.props.disabled).toEqual(false); + + const includeSpaceSwitch = labels.get(1); + + expect(includeSpaceSwitch.props.disabled).toEqual(false); + expect(includeSpaceSwitch.props.label.props.defaultMessage).toEqual( + 'Include space between suffix and value' + ); + expect(component).toMatchSnapshot(); }); }); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx index de413d02c5011c..d61d14aac3fc7b 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx @@ -41,6 +41,8 @@ interface DurationFormatEditorFormatParams { inputFormat: string; outputFormat: string; showSuffix?: boolean; + useShortSuffix?: boolean; + includeSpaceWithSuffix?: boolean; } export class DurationFormatEditor extends DefaultFormatEditor< @@ -83,9 +85,14 @@ export class DurationFormatEditor extends DefaultFormatEditor< } render() { - const { format, formatParams } = this.props; + const { format } = this.props; const { error, samples, hasDecimalError } = this.state; + const formatParams: DurationFormatEditorFormatParams = { + includeSpaceWithSuffix: format.getParamDefaults().includeSpaceWithSuffix, + ...this.props.formatParams, + }; + return ( + {!(format as DurationFormat).isHumanPrecise() && ( + + + } + checked={Boolean(formatParams.showSuffix)} + onChange={(e) => { + this.onChange({ + showSuffix: !formatParams.showSuffix, + }); + }} + /> + + )} + } + checked={Boolean(formatParams.useShortSuffix)} + onChange={(e) => { + this.onChange({ useShortSuffix: !formatParams.useShortSuffix }); + }} + /> + + + } - checked={Boolean(formatParams.showSuffix)} + checked={Boolean(formatParams.includeSpaceWithSuffix)} onChange={(e) => { - this.onChange({ showSuffix: !formatParams.showSuffix }); + this.onChange({ includeSpaceWithSuffix: !formatParams.includeSpaceWithSuffix }); }} /> diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index f1592d5a8cf0b1..5f70deccba93cf 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -388,6 +388,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'long', _meta: { description: 'Non-default value of setting.' }, }, + 'bfetch:disableCompression': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'visualization:visualize:legacyChartsLibrary': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 570b52171be283..bf28bb6cc01f5d 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -22,6 +22,7 @@ export interface UsageStats { /** * non-sensitive settings */ + 'bfetch:disableCompression': boolean; 'autocomplete:useTimeRange': boolean; 'search:timeout': number; 'visualization:visualize:legacyChartsLibrary': boolean; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 693957057f1084..0ca1b863f91a7b 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8217,6 +8217,12 @@ "description": "Non-default value of setting." } }, + "bfetch:disableCompression": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "visualization:visualize:legacyChartsLibrary": { "type": "boolean", "_meta": { diff --git a/src/plugins/vis_default_editor/public/components/controls/palette_picker.tsx b/src/plugins/vis_default_editor/public/components/controls/palette_picker.tsx index b09a806e8fc253..9249edef8af921 100644 --- a/src/plugins/vis_default_editor/public/components/controls/palette_picker.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/palette_picker.tsx @@ -39,12 +39,12 @@ export function PalettePicker({ palettes={palettes .getAll() .filter(({ internal }) => !internal) - .map(({ id, title, getColors }) => { + .map(({ id, title, getCategoricalColors }) => { return { value: id, title, type: 'fixed', - palette: getColors( + palette: getCategoricalColors( 10, id === activePalette?.name ? activePalette?.params : undefined ), diff --git a/src/plugins/vis_type_timeseries/common/get_last_value.test.js b/src/plugins/vis_type_timeseries/common/get_last_value.test.js deleted file mode 100644 index 794bbe17a1e7a1..00000000000000 --- a/src/plugins/vis_type_timeseries/common/get_last_value.test.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { getLastValue } from './get_last_value'; - -describe('getLastValue(data)', () => { - test('should returns data if data is not array', () => { - expect(getLastValue('foo')).toBe('foo'); - }); - - test('should returns 0 as a value when not an array', () => { - expect(getLastValue(0)).toBe(0); - }); - - test('should returns the last value', () => { - expect(getLastValue([[1, 2]])).toBe(2); - }); - - test('should return 0 as a valid value', () => { - expect(getLastValue([[0, 0]])).toBe(0); - }); - - test('should returns the default value ', () => { - expect(getLastValue()).toBe('-'); - }); - - test('should returns 0 if second to last is not defined (default)', () => { - expect( - getLastValue([ - [1, null], - [2, null], - ]) - ).toBe('-'); - }); -}); diff --git a/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts b/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts index 7828ee33736eec..a601da234e0782 100644 --- a/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts +++ b/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts @@ -90,7 +90,7 @@ describe('fetchIndexPattern', () => { ] as IndexPattern[]; const value = await fetchIndexPattern('indexTitle', indexPatternsService, { - fetchKibabaIndexForStringIndexes: true, + fetchKibanaIndexForStringIndexes: true, }); expect(value).toMatchInlineSnapshot(` @@ -104,9 +104,9 @@ describe('fetchIndexPattern', () => { `); }); - test('should return only indexPatternString if Kibana index does not exist (fetchKibabaIndexForStringIndexes is true)', async () => { + test('should return only indexPatternString if Kibana index does not exist (fetchKibanaIndexForStringIndexes is true)', async () => { const value = await fetchIndexPattern('indexTitle', indexPatternsService, { - fetchKibabaIndexForStringIndexes: true, + fetchKibanaIndexForStringIndexes: true, }); expect(value).toMatchInlineSnapshot(` diff --git a/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts b/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts index 152fd5182225be..1224fd33daee34 100644 --- a/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts +++ b/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts @@ -51,9 +51,9 @@ export const fetchIndexPattern = async ( indexPatternValue: IndexPatternValue | undefined, indexPatternsService: Pick, options: { - fetchKibabaIndexForStringIndexes: boolean; + fetchKibanaIndexForStringIndexes: boolean; } = { - fetchKibabaIndexForStringIndexes: false, + fetchKibanaIndexForStringIndexes: false, } ): Promise => { let indexPattern: FetchedIndexPattern['indexPattern']; @@ -63,7 +63,7 @@ export const fetchIndexPattern = async ( indexPattern = await indexPatternsService.getDefault(); } else { if (isStringTypeIndexPattern(indexPatternValue)) { - if (options.fetchKibabaIndexForStringIndexes) { + if (options.fetchKibanaIndexForStringIndexes) { indexPattern = (await indexPatternsService.find(indexPatternValue)).find( (index) => index.title === indexPatternValue ); diff --git a/src/plugins/vis_type_timeseries/common/last_value_utils.test.ts b/src/plugins/vis_type_timeseries/common/last_value_utils.test.ts new file mode 100644 index 00000000000000..34e1265b9a6a26 --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/last_value_utils.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { getLastValue, isEmptyValue, EMPTY_VALUE } from './last_value_utils'; +import { clone } from 'lodash'; + +describe('getLastValue(data)', () => { + test('should return data, if data is not an array', () => { + const data = 'foo'; + expect(getLastValue(data)).toBe(data); + }); + + test('should return 0 as a value, when data is not an array', () => { + expect(getLastValue(0)).toBe(0); + }); + + test('should return the last value', () => { + const lastVal = 2; + expect(getLastValue([[1, lastVal]])).toBe(lastVal); + }); + + test('should return 0 as a valid value', () => { + expect(getLastValue([[0, 0]])).toBe(0); + }); + + test("should return empty value (null), if second array is empty or it's last element is null/undefined (default)", () => { + expect( + getLastValue([ + [1, null], + [2, null], + ]) + ).toBe(EMPTY_VALUE); + + expect( + getLastValue([ + [1, null], + [2, undefined], + ]) + ).toBe(EMPTY_VALUE); + }); +}); + +describe('isEmptyValue(value)', () => { + test('should return true if is equal to the empty value', () => { + // if empty value will change, no need to rewrite test for passing it. + const emptyValue = + typeof EMPTY_VALUE === 'object' && EMPTY_VALUE != null ? clone(EMPTY_VALUE) : EMPTY_VALUE; + expect(isEmptyValue(emptyValue)).toBe(true); + }); + + test('should return the last value', () => { + const notEmptyValue = [...Array(10).keys()]; + expect(isEmptyValue(notEmptyValue)).toBe(false); + }); +}); diff --git a/src/plugins/vis_type_timeseries/common/last_value_utils.ts b/src/plugins/vis_type_timeseries/common/last_value_utils.ts new file mode 100644 index 00000000000000..a51a04962a8918 --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/last_value_utils.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isArray, last, isEqual } from 'lodash'; + +export const EMPTY_VALUE = null; +export const DISPLAY_EMPTY_VALUE = '-'; + +const extractValue = (data: unknown[] | void) => (data && data[1]) ?? EMPTY_VALUE; + +export const getLastValue = (data: unknown) => { + if (!isArray(data)) { + return data; + } + + return extractValue(last(data)); +}; + +export const isEmptyValue = (value: unknown) => isEqual(value, EMPTY_VALUE); diff --git a/src/plugins/vis_type_timeseries/common/operators_utils.test.ts b/src/plugins/vis_type_timeseries/common/operators_utils.test.ts new file mode 100644 index 00000000000000..ad66f058a4918c --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/operators_utils.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 { getOperator, shouldOperate, Rule, Operator } from './operators_utils'; + +describe('getOperator(operator)', () => { + test('should return operator function', () => { + const operatorName = Operator.Gte; + const operator = getOperator(operatorName); + expect(typeof operator).toBe('function'); + }); +}); + +describe('shouldOperate(rule, value)', () => { + test('should operate, if value is not null and rule value is not null', () => { + const rule: Rule = { + value: 1, + operator: Operator.Gte, + }; + const value = 2; + + expect(shouldOperate(rule, value)).toBeTruthy(); + }); + + test('should operate, if value is null and operator allows null value', () => { + const rule: Rule = { + operator: Operator.Empty, + value: null, + }; + const value = null; + + expect(shouldOperate(rule, value)).toBeTruthy(); + }); + + test("should not operate, if value is null and operator doesn't allow null values", () => { + const rule: Rule = { + operator: Operator.Gte, + value: 2, + }; + const value = null; + + expect(shouldOperate(rule, value)).toBeFalsy(); + }); + + test("should not operate, if rule value is null and operator doesn't allow null values", () => { + const rule: Rule = { + operator: Operator.Gte, + value: null, + }; + const value = 3; + + expect(shouldOperate(rule, value)).toBeFalsy(); + }); +}); diff --git a/src/plugins/vis_type_timeseries/common/operators_utils.ts b/src/plugins/vis_type_timeseries/common/operators_utils.ts new file mode 100644 index 00000000000000..603e63159b22d0 --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/operators_utils.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 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 { gt, gte, lt, lte, isNull } from 'lodash'; + +export enum Operator { + Gte = 'gte', + Lte = 'lte', + Gt = 'gt', + Lt = 'lt', + Empty = 'empty', +} + +export interface Rule { + operator: Operator; + value: unknown; +} + +type OperatorsAllowNullType = { + [name in Operator]?: boolean; +}; + +const OPERATORS = { + [Operator.Gte]: gte, + [Operator.Lte]: lte, + [Operator.Gt]: gt, + [Operator.Lt]: lt, + [Operator.Empty]: isNull, +}; + +const OPERATORS_ALLOW_NULL: OperatorsAllowNullType = { + [Operator.Empty]: true, +}; + +export const getOperator = (operator: Operator) => { + return OPERATORS[operator]; +}; + +// This check is necessary for preventing from comparing null values with numeric rules. +export const shouldOperate = (rule: Rule, value: unknown) => + (isNull(rule.value) && OPERATORS_ALLOW_NULL[rule.operator]) || + (!isNull(rule.value) && !isNull(value)); diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx index 9ea8898636cec4..3b1356d571749c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx @@ -12,22 +12,51 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { mountWithIntl } from '@kbn/test/jest'; import { collectionActions } from './lib/collection_actions'; -import { ColorRules, ColorRulesProps } from './color_rules'; +import { + ColorRules, + ColorRulesProps, + colorRulesOperatorsList, + ColorRulesOperator, +} from './color_rules'; +import { Operator } from '../../../common/operators_utils'; describe('src/legacy/core_plugins/metrics/public/components/color_rules.test.js', () => { - const defaultProps = ({ + const emptyRule: ColorRulesOperator = colorRulesOperatorsList.filter( + (operator) => operator.method === Operator.Empty + )[0]; + const notEmptyRule: ColorRulesOperator = colorRulesOperatorsList.filter( + (operator) => operator.method !== Operator.Empty + )[0]; + + const getColorRulesProps = (gaugeColorRules: unknown = []) => ({ name: 'gauge_color_rules', - model: { - gauge_color_rules: [ - { - gauge: null, - value: 0, - id: 'unique value', - }, - ], - }, + model: { gauge_color_rules: gaugeColorRules }, onChange: jest.fn(), - } as unknown) as ColorRulesProps; + }); + + const defaultProps = (getColorRulesProps([ + { + gauge: null, + value: 0, + id: 'unique value', + }, + ]) as unknown) as ColorRulesProps; + + const emptyColorRuleProps = (getColorRulesProps([ + { + operator: emptyRule?.method, + value: emptyRule?.value, + id: 'unique value', + }, + ]) as unknown) as ColorRulesProps; + + const notEmptyColorRuleProps = (getColorRulesProps([ + { + operator: notEmptyRule?.method, + value: notEmptyRule?.value, + id: 'unique value', + }, + ]) as unknown) as ColorRulesProps; describe('ColorRules', () => { it('should render empty
node', () => { @@ -47,6 +76,7 @@ describe('src/legacy/core_plugins/metrics/public/components/color_rules.test.js' expect(isNode).toBeTruthy(); }); + it('should handle change of operator and value correctly', () => { collectionActions.handleChange = jest.fn(); const wrapper = mountWithIntl(); @@ -57,8 +87,23 @@ describe('src/legacy/core_plugins/metrics/public/components/color_rules.test.js' expect((collectionActions.handleChange as jest.Mock).mock.calls[0][1].operator).toEqual('gt'); const numberInput = findTestSubject(wrapper, 'colorRuleValue'); + numberInput.simulate('change', { target: { value: '123' } }); expect((collectionActions.handleChange as jest.Mock).mock.calls[1][1].value).toEqual(123); }); + + it('should handle render of value field if empty value oparetor is selected by default', () => { + collectionActions.handleChange = jest.fn(); + const wrapper = mountWithIntl(); + const numberInput = findTestSubject(wrapper, 'colorRuleValue'); + expect(numberInput.exists()).toBeFalsy(); + }); + + it('should handle render of value field if not empty operator is selected by default', () => { + collectionActions.handleChange = jest.fn(); + const wrapper = mountWithIntl(); + const numberInput = findTestSubject(wrapper, 'colorRuleValue'); + expect(numberInput.exists()).toBeTruthy(); + }); }); }); diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx index 7aea5f934ee90b..0cc64528ae3f37 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx @@ -23,6 +23,7 @@ import { AddDeleteButtons } from './add_delete_buttons'; import { collectionActions } from './lib/collection_actions'; import { ColorPicker, ColorPickerProps } from './color_picker'; import { TimeseriesVisParams } from '../../types'; +import { Operator } from '../../../common/operators_utils'; export interface ColorRulesProps { name: keyof TimeseriesVisParams; @@ -40,10 +41,17 @@ interface ColorRule { id: string; background_color?: string; color?: string; - operator?: string; + operator?: Operator; text?: string; } +export interface ColorRulesOperator { + label: string; + method: Operator; + value?: unknown; + hideValueSelector?: boolean; +} + const defaultSecondaryName = i18n.translate( 'visTypeTimeseries.colorRules.defaultSecondaryNameLabel', { @@ -54,33 +62,45 @@ const defaultPrimaryName = i18n.translate('visTypeTimeseries.colorRules.defaultP defaultMessage: 'background', }); -const operatorOptions = [ +export const colorRulesOperatorsList: ColorRulesOperator[] = [ { label: i18n.translate('visTypeTimeseries.colorRules.greaterThanLabel', { defaultMessage: '> greater than', }), - value: 'gt', + method: Operator.Gt, }, { label: i18n.translate('visTypeTimeseries.colorRules.greaterThanOrEqualLabel', { defaultMessage: '>= greater than or equal', }), - value: 'gte', + method: Operator.Gte, }, { label: i18n.translate('visTypeTimeseries.colorRules.lessThanLabel', { defaultMessage: '< less than', }), - value: 'lt', + method: Operator.Lt, }, { label: i18n.translate('visTypeTimeseries.colorRules.lessThanOrEqualLabel', { defaultMessage: '<= less than or equal', }), - value: 'lte', + method: Operator.Lte, + }, + { + label: i18n.translate('visTypeTimeseries.colorRules.emptyLabel', { + defaultMessage: 'empty', + }), + method: Operator.Empty, + hideValueSelector: true, }, ]; +const operatorOptions = colorRulesOperatorsList.map((operator) => ({ + label: operator.label, + value: operator.method, +})); + export class ColorRules extends Component { constructor(props: ColorRulesProps) { super(props); @@ -100,9 +120,14 @@ export class ColorRules extends Component { handleOperatorChange = (item: ColorRule) => { return (options: Array>) => { + const selectedOperator = colorRulesOperatorsList.find( + (operator) => options[0]?.value === operator.method + ); + const value = selectedOperator?.value ?? null; collectionActions.handleChange(this.props, { ...item, - operator: options[0].value, + operator: options[0]?.value, + value, }); }; }; @@ -119,7 +144,11 @@ export class ColorRules extends Component { const selectedOperatorOption = operatorOptions.find( (option) => model.operator === option.value ); + const selectedOperator = colorRulesOperatorsList.find( + (operator) => model.operator === operator.method + ); + const hideValueSelectorField = selectedOperator?.hideValueSelector ?? false; const labelStyle = { marginBottom: 0 }; let secondary; @@ -203,19 +232,19 @@ export class ColorRules extends Component { fullWidth /> - - - - - + {!hideValueSelectorField && ( + + + + )} { @@ -118,6 +125,7 @@ export const IndexPattern = ({ }; const model = { ...defaults, ..._model }; + const index = model[indexPatternName]; const intervalValidation = validateIntervalValue(model[intervalName]); const selectedTimeRangeOption = timeRangeOptions.find( @@ -133,11 +141,40 @@ export const IndexPattern = ({ updateControlValidity(intervalName, intervalValidation.isValid); }, [intervalName, intervalValidation.isValid, updateControlValidity]); + useEffect(() => { + async function fetchIndex() { + const { indexPatterns } = getDataStart(); + + setFetchedIndex( + index + ? await fetchIndexPattern(index, indexPatterns, { + fetchKibanaIndexForStringIndexes: true, + }) + : { + indexPattern: undefined, + indexPatternString: undefined, + } + ); + } + + fetchIndex(); + }, [index]); + const toggleIndicatorDisplay = useCallback( () => onChange({ [HIDE_LAST_VALUE_INDICATOR]: !model.hide_last_value_indicator }), [model.hide_last_value_indicator, onChange] ); + const getTimefieldPlaceholder = () => { + if (!model[indexPatternName]) { + return defaultIndex?.timeFieldName; + } + + if (useKibanaIndices) { + return fetchedIndex?.indexPattern?.timeFieldName ?? undefined; + } + }; + return (
{!isTimeSeries && ( @@ -207,6 +244,7 @@ export const IndexPattern = ({ diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js index 478b41810719c7..3616a8c8b348d7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js @@ -8,7 +8,7 @@ import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; -import { getLastValue } from '../../../../common/get_last_value'; +import { getLastValue } from '../../../../common/last_value_utils'; import { emptyLabel } from '../../../../common/empty_label'; import { createTickFormatter } from './tick_formatter'; import { labelDateFormatter } from './label_date_formatter'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx index ece90d47993093..07edfc2e6e0d70 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx @@ -6,18 +6,15 @@ * Side Public License, v 1. */ -import React, { useState, useContext, useCallback, useEffect } from 'react'; +import React, { useContext, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFormRow, EuiText, EuiLink, htmlIdGenerator } from '@elastic/eui'; -import { getCoreStart, getDataStart } from '../../../../services'; +import { getCoreStart } from '../../../../services'; import { PanelModelContext } from '../../../contexts/panel_model_context'; -import { - isStringTypeIndexPattern, - fetchIndexPattern, -} from '../../../../../common/index_patterns_utils'; +import { isStringTypeIndexPattern } from '../../../../../common/index_patterns_utils'; import { FieldTextSelect } from './field_text_select'; import { ComboBoxSelect } from './combo_box_select'; @@ -32,6 +29,7 @@ interface IndexPatternSelectProps { onChange: Function; disabled?: boolean; allowIndexSwitchingMode?: boolean; + fetchedIndex: FetchedIndexPattern | null; } const defaultIndexPatternHelpText = i18n.translate( @@ -57,13 +55,13 @@ export const IndexPatternSelect = ({ indexPatternName, onChange, disabled, + fetchedIndex, allowIndexSwitchingMode, }: IndexPatternSelectProps) => { const htmlId = htmlIdGenerator(); const panelModel = useContext(PanelModelContext); const defaultIndex = useContext(DefaultIndexPatternContext); - const [fetchedIndex, setFetchedIndex] = useState(); const useKibanaIndices = Boolean(panelModel?.[USE_KIBANA_INDEXES_KEY]); const Component = useKibanaIndices ? ComboBoxSelect : FieldTextSelect; @@ -98,25 +96,6 @@ export const IndexPatternSelect = ({ }); }, [fetchedIndex]); - useEffect(() => { - async function fetchIndex() { - const { indexPatterns } = getDataStart(); - - setFetchedIndex( - value - ? await fetchIndexPattern(value, indexPatterns, { - fetchKibabaIndexForStringIndexes: true, - }) - : { - indexPattern: undefined, - indexPatternString: undefined, - } - ); - } - - fetchIndex(); - }, [value]); - if (!fetchedIndex) { return null; } diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js index ac4780e673e07a..70529be78567d0 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js @@ -8,7 +8,7 @@ import handlebars from 'handlebars/dist/handlebars'; import { isNumber } from 'lodash'; -import { DEFAULT_VALUE } from '../../../../common/get_last_value'; +import { isEmptyValue, DISPLAY_EMPTY_VALUE } from '../../../../common/last_value_utils'; import { inputFormats, outputFormats, isDuration } from '../lib/durations'; import { getFieldFormats } from '../../../services'; @@ -38,12 +38,12 @@ export const createTickFormatter = (format = '0,0.[00]', template, getConfig = n } } return (val) => { - let value; - - if (val === DEFAULT_VALUE) { - return val; + if (isEmptyValue(val)) { + return DISPLAY_EMPTY_VALUE; } + let value; + if (!isNumber(val)) { value = val; } else { diff --git a/src/plugins/vis_type_timeseries/public/application/components/palette_picker.tsx b/src/plugins/vis_type_timeseries/public/application/components/palette_picker.tsx index 20c0b40bb2e542..749d6ca62bfa93 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/palette_picker.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/palette_picker.tsx @@ -33,12 +33,12 @@ export function PalettePicker({ activePalette, palettes, setPalette, color }: Pa ...palettes .getAll() .filter(({ internal }) => !internal) - .map(({ id, title, getColors }) => { + .map(({ id, title, getCategoricalColors }) => { return { value: id, title, type: 'fixed' as const, - palette: getColors(10), + palette: getCategoricalColors(10), }; }), { @@ -49,7 +49,7 @@ export function PalettePicker({ activePalette, palettes, setPalette, color }: Pa type: 'fixed', palette: palettes .get('custom') - .getColors(10, { colors: [color, finalGradientColor], gradient: true }), + .getCategoricalColors(10, { colors: [color, finalGradientColor], gradient: true }), }, { value: PALETTES.RAINBOW, @@ -59,7 +59,7 @@ export function PalettePicker({ activePalette, palettes, setPalette, color }: Pa type: 'fixed', palette: palettes .get('custom') - .getColors(10, { colors: rainbowColors.slice(0, 10), gradient: false }), + .getCategoricalColors(10, { colors: rainbowColors.slice(0, 10), gradient: false }), }, ]} onChange={(newPalette) => { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js index a464771b01af32..6140726975cbd5 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js @@ -10,9 +10,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import { visWithSplits } from '../../vis_with_splits'; import { createTickFormatter } from '../../lib/tick_formatter'; -import _, { get, isUndefined, assign, includes } from 'lodash'; +import { get, isUndefined, assign, includes } from 'lodash'; import { Gauge } from '../../../visualizations/views/gauge'; -import { getLastValue } from '../../../../../common/get_last_value'; +import { getLastValue } from '../../../../../common/last_value_utils'; +import { getOperator, shouldOperate } from '../../../../../common/operators_utils'; function getColors(props) { const { model, visData } = props; @@ -21,9 +22,9 @@ function getColors(props) { let gauge; if (model.gauge_color_rules) { model.gauge_color_rules.forEach((rule) => { - if (rule.operator && rule.value != null) { - const value = (series[0] && getLastValue(series[0].data)) || 0; - if (_[rule.operator](value, rule.value)) { + if (rule.operator) { + const value = getLastValue(series[0]?.data); + if (shouldOperate(rule, value) && getOperator(rule.operator)(value, rule.value)) { gauge = rule.gauge; text = rule.text; } diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js index 3029bba04b4500..b35ee977d3e44d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js @@ -10,10 +10,11 @@ import PropTypes from 'prop-types'; import React from 'react'; import { visWithSplits } from '../../vis_with_splits'; import { createTickFormatter } from '../../lib/tick_formatter'; -import _, { get, isUndefined, assign, includes, pick } from 'lodash'; +import { get, isUndefined, assign, includes, pick } from 'lodash'; import { Metric } from '../../../visualizations/views/metric'; -import { getLastValue } from '../../../../../common/get_last_value'; +import { getLastValue } from '../../../../../common/last_value_utils'; import { isBackgroundInverted } from '../../../lib/set_is_reversed'; +import { getOperator, shouldOperate } from '../../../../../common/operators_utils'; function getColors(props) { const { model, visData } = props; @@ -22,9 +23,9 @@ function getColors(props) { let background; if (model.background_color_rules) { model.background_color_rules.forEach((rule) => { - if (rule.operator && rule.value != null) { - const value = (series[0] && getLastValue(series[0].data)) || 0; - if (_[rule.operator](value, rule.value)) { + if (rule.operator) { + const value = getLastValue(series[0]?.data); + if (shouldOperate(rule, value) && getOperator(rule.operator)(value, rule.value)) { background = rule.background_color; color = rule.color; } diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js index 41e6236cbc39b2..0b3a24615c0e30 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js @@ -9,13 +9,13 @@ import { getCoreStart } from '../../../../services'; import { createTickFormatter } from '../../lib/tick_formatter'; import { TopN } from '../../../visualizations/views/top_n'; -import { getLastValue } from '../../../../../common/get_last_value'; +import { getLastValue } from '../../../../../common/last_value_utils'; import { isBackgroundInverted } from '../../../lib/set_is_reversed'; import { replaceVars } from '../../lib/replace_vars'; import PropTypes from 'prop-types'; import React from 'react'; -import { sortBy, first, get, gt, gte, lt, lte } from 'lodash'; -const OPERATORS = { gt, gte, lt, lte }; +import { sortBy, first, get } from 'lodash'; +import { getOperator, shouldOperate } from '../../../../../common/operators_utils'; function sortByDirection(data, direction, fn) { if (direction === 'desc') { @@ -53,8 +53,8 @@ function TopNVisualization(props) { let color = item.color || seriesConfig.color; if (model.bar_color_rules) { model.bar_color_rules.forEach((rule) => { - if (rule.operator && rule.value != null && rule.bar_color) { - if (OPERATORS[rule.operator](value, rule.value)) { + if (shouldOperate(rule, value) && rule.operator && rule.bar_color) { + if (getOperator(rule.operator)(value, rule.value)) { color = rule.bar_color; } } diff --git a/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.ts b/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.ts index adcf1f3ad63cdb..028ce3d028997c 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.ts +++ b/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.ts @@ -58,7 +58,7 @@ export const getSplitByTermsColor = ({ } : seriesPalette.params; - const outputColor = palettesRegistry?.get(paletteName || 'default').getColor( + const outputColor = palettesRegistry?.get(paletteName || 'default').getCategoricalColor( [ { name: seriesName || emptyLabel, diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js index 31ea3412972e8f..000701c3a0764a 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js @@ -11,7 +11,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import classNames from 'classnames'; import { isBackgroundInverted, isBackgroundDark } from '../../lib/set_is_reversed'; -import { getLastValue } from '../../../../common/get_last_value'; +import { getLastValue } from '../../../../common/last_value_utils'; import { getValueBy } from '../lib/get_value_by'; import { GaugeVis } from './gauge_vis'; import reactcss from 'reactcss'; @@ -61,7 +61,7 @@ export class Gauge extends Component { render() { const { metric, type } = this.props; const { scale, translateX, translateY } = this.state; - const value = metric && getLastValue(metric.data); + const value = getLastValue(metric?.data); const max = (metric && getValueBy('max', metric.data)) || 1; const formatter = (metric && (metric.tickFormatter || metric.formatter)) || @@ -76,16 +76,13 @@ export class Gauge extends Component { left: this.state.left || 0, transform: `matrix(${scale}, 0, 0, ${scale}, ${translateX}, ${translateY})`, }, - }, - valueColor: { - value: { + valueColor: { color: this.props.valueColor, }, }, }, this.props ); - const gaugeProps = { value, reversed: isBackgroundDark(this.props.backgroundColor), @@ -114,7 +111,7 @@ export class Gauge extends Component {
{title}
-
+
{formatter(value)}
{additionalLabel} @@ -127,7 +124,7 @@ export class Gauge extends Component { ref={(el) => (this.inner = el)} style={styles.inner} > -
+
{formatter(value)}
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js index c8789f98969f88..30b7844a90fdac 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js @@ -12,6 +12,7 @@ import _ from 'lodash'; import reactcss from 'reactcss'; import { calculateCoordinates } from '../lib/calculate_coordinates'; import { COLORS } from '../constants/chart'; +import { isEmptyValue } from '../../../../common/last_value_utils'; export class GaugeVis extends Component { constructor(props) { @@ -55,10 +56,14 @@ export class GaugeVis extends Component { render() { const { type, value, max, color } = this.props; + + // if value is empty array, no metrics to display. + const formattedValue = isEmptyValue(value) ? 1 : value; + const { scale, translateX, translateY } = this.state; const size = 2 * Math.PI * 50; const sliceSize = type === 'half' ? 0.6 : 1; - const percent = value < max ? value / max : 1; + const percent = formattedValue < max ? formattedValue / max : 1; const styles = reactcss( { default: { @@ -161,6 +166,6 @@ GaugeVis.propTypes = { max: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), metric: PropTypes.object, reversed: PropTypes.bool, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]), type: PropTypes.oneOf(['half', 'circle']), }; diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js index 17cadb94457b6e..bc4230d0a15efe 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js @@ -11,7 +11,7 @@ import React, { Component } from 'react'; import _ from 'lodash'; import reactcss from 'reactcss'; -import { getLastValue } from '../../../../common/get_last_value'; +import { getLastValue } from '../../../../common/last_value_utils'; import { calculateCoordinates } from '../lib/calculate_coordinates'; export class Metric extends Component { @@ -58,7 +58,8 @@ export class Metric extends Component { const { metric, secondary } = this.props; const { scale, translateX, translateY } = this.state; const primaryFormatter = (metric && (metric.tickFormatter || metric.formatter)) || ((n) => n); - const primaryValue = primaryFormatter(getLastValue(metric && metric.data)); + const primaryValue = primaryFormatter(getLastValue(metric?.data)); + const styles = reactcss( { default: { @@ -120,7 +121,6 @@ export class Metric extends Component { if (this.props.reversed) { className += ' tvbVisMetric--reversed'; } - return (
(this.resize = el)} className="tvbVisMetric__resize"> diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js index 2559ed543e543d..0c43ab157fbbba 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { getLastValue } from '../../../../common/get_last_value'; +import { getLastValue, isEmptyValue } from '../../../../common/last_value_utils'; import { labelDateFormatter } from '../../components/lib/label_date_formatter'; import { emptyLabel } from '../../../../common/empty_label'; import reactcss from 'reactcss'; @@ -97,15 +97,16 @@ export class TopN extends Component { const renderMode = TopN.getRenderMode(min, max); const key = `${item.id || item.label}`; const lastValue = getLastValue(item.data); + // if result is empty, all bar need to be colored. + const lastValueFormatted = isEmptyValue(lastValue) ? 1 : lastValue; const formatter = item.tickFormatter || this.props.tickFormatter; - const isPositiveValue = lastValue >= 0; + const isPositiveValue = lastValueFormatted >= 0; const intervalLength = TopN.calcDomain(renderMode, min, max); // if both are 0, the division returns NaN causing unexpected behavior. // For this it defaults to 0 - const width = 100 * (Math.abs(lastValue) / intervalLength) || 0; + const width = 100 * (Math.abs(lastValueFormatted) / intervalLength) || 0; const label = item.labelFormatted ? labelDateFormatter(item.labelFormatted) : item.label; - const styles = reactcss( { default: { @@ -150,7 +151,7 @@ export class TopN extends Component { const intervalSettings = this.props.series.reduce( (acc, series, index) => { - const value = getLastValue(series.data); + const value = getLastValue(series.data) ?? 1; return { min: !index || value < acc.min ? value : acc.min, diff --git a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts index cb105d7b439ccf..5cdea62af95361 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts @@ -31,20 +31,22 @@ export async function getVisData( const indexPatternsService = await framework.getIndexPatternsService(requestContext); const esQueryConfig = await getEsQueryConfig(uiSettings); - const services: VisTypeTimeseriesRequestServices = { - esQueryConfig, - esShardTimeout, - indexPatternsService, - uiSettings, - searchStrategyRegistry: framework.searchStrategyRegistry, - cachedIndexPatternFetcher: getCachedIndexPatternFetcher(indexPatternsService), - }; - const promises = request.body.panels.map((panel) => { - if (panel.type === PANEL_TYPES.TABLE) { - return getTableData(requestContext, request, panel, services); - } - return getSeriesData(requestContext, request, panel, services); + const services: VisTypeTimeseriesRequestServices = { + esQueryConfig, + esShardTimeout, + indexPatternsService, + uiSettings, + searchStrategyRegistry: framework.searchStrategyRegistry, + cachedIndexPatternFetcher: getCachedIndexPatternFetcher( + indexPatternsService, + Boolean(panel.use_kibana_indexes) + ), + }; + + return panel.type === PANEL_TYPES.TABLE + ? getTableData(requestContext, request, panel, services) + : getSeriesData(requestContext, request, panel, services); }); return Promise.all(promises).then((res) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts index b03fa973e9da99..26ea191ab92173 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts @@ -11,7 +11,10 @@ import { getIndexPatternKey, fetchIndexPattern } from '../../../../common/index_ import type { IndexPatternsService } from '../../../../../data/server'; import type { IndexPatternValue, FetchedIndexPattern } from '../../../../common/types'; -export const getCachedIndexPatternFetcher = (indexPatternsService: IndexPatternsService) => { +export const getCachedIndexPatternFetcher = ( + indexPatternsService: IndexPatternsService, + fetchKibanaIndexForStringIndexes: boolean = false +) => { const cache = new Map(); return async (indexPatternValue: IndexPatternValue): Promise => { @@ -21,7 +24,9 @@ export const getCachedIndexPatternFetcher = (indexPatternsService: IndexPatterns return cache.get(key); } - const fetchedIndex = fetchIndexPattern(indexPatternValue, indexPatternsService); + const fetchedIndex = fetchIndexPattern(indexPatternValue, indexPatternsService, { + fetchKibanaIndexForStringIndexes, + }); cache.set(key, fetchedIndex); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/math.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/math.js index fd7f5a06cac560..5abfc3e26ffcdd 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/math.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/math.js @@ -8,9 +8,9 @@ import { mathAgg } from '../series/math'; -export function math(bucket, panel, series) { +export function math(bucket, panel, series, meta, extractFields) { return (next) => (results) => { - const mathFn = mathAgg({ aggregations: bucket }, panel, series); + const mathFn = mathAgg({ aggregations: bucket }, panel, series, meta, extractFields); return mathFn(next)(results); }; } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js index 9e3a2ac71ed025..88b06d7f7ffaa4 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js @@ -8,7 +8,7 @@ import { buildProcessorFunction } from '../build_processor_function'; import { processors } from '../response_processors/table'; -import { getLastValue } from '../../../../common/get_last_value'; +import { getLastValue } from '../../../../common/last_value_utils'; import { first, get } from 'lodash'; import { overwrite } from '../helpers'; import { getActiveSeries } from '../helpers/get_active_series'; diff --git a/src/plugins/vis_type_vega/public/components/vega_vis.scss b/src/plugins/vis_type_vega/public/components/vega_vis.scss index f0062869e00469..5b96eb9a560c76 100644 --- a/src/plugins/vis_type_vega/public/components/vega_vis.scss +++ b/src/plugins/vis_type_vega/public/components/vega_vis.scss @@ -18,7 +18,7 @@ z-index: 0; flex: 1 1 100%; - display: block; + //display determined by js max-width: 100%; max-height: 100%; width: 100%; diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index 8590b51d3b5ffe..255bd9774f9df8 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -147,7 +147,7 @@ export interface Bool { bool?: Bool; must?: DslQuery[]; filter?: Filter[]; - should?: never[]; + should?: Filter[]; must_not?: Filter[]; } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index b7f2b064cf9c22..54084c7476b6b5 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -83,9 +83,10 @@ export class VegaBaseView { return; } + const containerDisplay = this._parser.useResize ? 'flex' : 'block'; this._$container = $('
') // Force a height here because css is not loaded in mocha test - .css('height', '100%') + .css({ height: '100%', display: containerDisplay }) .appendTo(this._$parentEl); this._$controls = $( `
` diff --git a/src/plugins/vis_type_xy/public/vis_component.tsx b/src/plugins/vis_type_xy/public/vis_component.tsx index 5da5ffcc637c6c..dd88822f7f0f36 100644 --- a/src/plugins/vis_type_xy/public/vis_component.tsx +++ b/src/plugins/vis_type_xy/public/vis_component.tsx @@ -245,7 +245,7 @@ const VisComponent = (props: VisComponentProps) => { if (Object.keys(overwriteColors).includes(seriesName)) { return overwriteColors[seriesName]; } - const outputColor = palettesRegistry?.get(visParams.palette.name).getColor( + const outputColor = palettesRegistry?.get(visParams.palette.name).getCategoricalColor( [ { name: seriesName, diff --git a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts index 6f214745e12912..212c033a65c263 100644 --- a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts +++ b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts @@ -13,6 +13,7 @@ import { commonAddSupportOfDualIndexSelectionModeInTSVB, commonHideTSVBLastValueIndicator, commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel, + commonAddEmptyValueColorRule, } from '../migrations/visualization_common_migrations'; const byValueAddSupportOfDualIndexSelectionModeInTSVB = (state: SerializableState) => { @@ -36,6 +37,13 @@ const byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (state: Serial }; }; +const byValueAddEmptyValueColorRule = (state: SerializableState) => { + return { + ...state, + savedVis: commonAddEmptyValueColorRule(state.savedVis), + }; +}; + export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => { return { id: 'visualization', @@ -47,6 +55,7 @@ export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => { byValueHideTSVBLastValueIndicator, byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel )(state), + '7.14.0': (state) => flow(byValueAddEmptyValueColorRule)(state), }, }; }; diff --git a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts index 3f09f19d9ac63a..13b8d8c4a0f982 100644 --- a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts @@ -6,6 +6,9 @@ * Side Public License, v 1. */ +import { get, last } from 'lodash'; +import uuid from 'uuid'; + export const commonAddSupportOfDualIndexSelectionModeInTSVB = (visState: any) => { if (visState && visState.type === 'metrics') { const { params } = visState; @@ -42,3 +45,49 @@ export const commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (visStat return visState; }; + +export const commonAddEmptyValueColorRule = (visState: any) => { + if (visState && visState.type === 'metrics') { + const params: any = get(visState, 'params') || {}; + + const getRuleWithComparingToZero = (rules: any[] = []) => { + const compareWithEqualMethods = ['gte', 'lte']; + return last( + rules.filter((rule) => compareWithEqualMethods.includes(rule.operator) && rule.value === 0) + ); + }; + + const convertRuleToEmpty = (rule: any = {}) => ({ + ...rule, + id: uuid.v4(), + operator: 'empty', + value: null, + }); + + const addEmptyRuleToListIfNecessary = (rules: any[]) => { + const rule = getRuleWithComparingToZero(rules); + + if (rule) { + return [...rules, convertRuleToEmpty(rule)]; + } + + return rules; + }; + + const colorRules = { + bar_color_rules: addEmptyRuleToListIfNecessary(params.bar_color_rules), + background_color_rules: addEmptyRuleToListIfNecessary(params.background_color_rules), + gauge_color_rules: addEmptyRuleToListIfNecessary(params.gauge_color_rules), + }; + + return { + ...visState, + params: { + ...params, + ...colorRules, + }, + }; + } + + return visState; +}; diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts index dbe5482c442b7c..36e1635ad4730e 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts @@ -2017,4 +2017,101 @@ describe('migration visualization', () => { expect(params.use_kibana_indexes).toBeFalsy(); }); }); + + describe('7.14.0 tsvb - add empty value rule to savedObjects with less and greater then zero rules', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.14.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const rule1 = { value: 0, operator: 'lte', color: 'rgb(145, 112, 184)' }; + const rule2 = { value: 0, operator: 'gte', color: 'rgb(96, 146, 192)' }; + const rule3 = { value: 0, operator: 'gt', color: 'rgb(84, 179, 153)' }; + const rule4 = { value: 0, operator: 'lt', color: 'rgb(84, 179, 153)' }; + + const createTestDocWithType = (params: any) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: `{ + "type":"metrics", + "params": ${JSON.stringify(params)} + }`, + }, + }); + + const checkEmptyRuleIsAddedToArray = ( + rulesArrayProperty: string, + prevParams: any, + migratedParams: any, + rule: any + ) => { + expect(migratedParams).toHaveProperty(rulesArrayProperty); + expect(Array.isArray(migratedParams[rulesArrayProperty])).toBeTruthy(); + expect(migratedParams[rulesArrayProperty].length).toBe( + prevParams[rulesArrayProperty].length + 1 + ); + + const lastElementIndex = migratedParams[rulesArrayProperty].length - 1; + expect(migratedParams[rulesArrayProperty][lastElementIndex]).toHaveProperty('operator'); + expect(migratedParams[rulesArrayProperty][lastElementIndex].operator).toEqual('empty'); + expect(migratedParams[rulesArrayProperty][lastElementIndex].color).toEqual(rule.color); + }; + + const checkRuleIsNotAddedToArray = ( + rulesArrayProperty: string, + prevParams: any, + migratedParams: any, + rule: any + ) => { + expect(migratedParams).toHaveProperty(rulesArrayProperty); + expect(Array.isArray(migratedParams[rulesArrayProperty])).toBeTruthy(); + expect(migratedParams[rulesArrayProperty].length).toBe(prevParams[rulesArrayProperty].length); + // expects, that array contains one element... + expect(migratedParams[rulesArrayProperty][0].operator).toBe(rule.operator); + }; + + it('should add empty rule if operator = lte and value = 0', () => { + const params = { + bar_color_rules: [rule1], + background_color_rules: [rule1], + gauge_color_rules: [rule1], + }; + const migratedTestDoc = migrate(createTestDocWithType(params)); + const { params: migratedParams } = JSON.parse(migratedTestDoc.attributes.visState); + + checkEmptyRuleIsAddedToArray('bar_color_rules', params, migratedParams, rule1); + checkEmptyRuleIsAddedToArray('background_color_rules', params, migratedParams, rule1); + checkEmptyRuleIsAddedToArray('gauge_color_rules', params, migratedParams, rule1); + }); + + it('should add empty rule if operator = gte and value = 0', () => { + const params = { + bar_color_rules: [rule2], + background_color_rules: [rule2], + gauge_color_rules: [rule2], + }; + const migratedTestDoc = migrate(createTestDocWithType(params)); + const { params: migratedParams } = JSON.parse(migratedTestDoc.attributes.visState); + + checkEmptyRuleIsAddedToArray('bar_color_rules', params, migratedParams, rule2); + checkEmptyRuleIsAddedToArray('background_color_rules', params, migratedParams, rule2); + checkEmptyRuleIsAddedToArray('gauge_color_rules', params, migratedParams, rule2); + }); + + it('should not add empty rule if operator = gt or lt and value = any', () => { + const params = { + bar_color_rules: [rule3], + background_color_rules: [rule3], + gauge_color_rules: [rule4], + }; + const migratedTestDoc = migrate(createTestDocWithType(params)); + const { params: migratedParams } = JSON.parse(migratedTestDoc.attributes.visState); + + checkRuleIsNotAddedToArray('bar_color_rules', params, migratedParams, rule3); + checkRuleIsNotAddedToArray('background_color_rules', params, migratedParams, rule3); + checkRuleIsNotAddedToArray('gauge_color_rules', params, migratedParams, rule4); + }); + }); }); diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts index b9885588b6f760..c5050b4a6940b7 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts @@ -15,6 +15,7 @@ import { commonAddSupportOfDualIndexSelectionModeInTSVB, commonHideTSVBLastValueIndicator, commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel, + commonAddEmptyValueColorRule, } from './visualization_common_migrations'; const migrateIndexPattern: SavedObjectMigrationFn = (doc) => { @@ -966,6 +967,29 @@ const removeDefaultIndexPatternAndTimeFieldFromTSVBModel: SavedObjectMigrationFn }; }; +const addEmptyValueColorRule: SavedObjectMigrationFn = (doc) => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + const newVisState = commonAddEmptyValueColorRule(visState); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(newVisState), + }, + }; + } + return doc; +}; + export const visualizationSavedObjectTypeMigrations = { /** * We need to have this migration twice, once with a version prior to 7.0.0 once with a version @@ -1012,4 +1036,5 @@ export const visualizationSavedObjectTypeMigrations = { hideTSVBLastValueIndicator, removeDefaultIndexPatternAndTimeFieldFromTSVBModel ), + '7.14.0': flow(addEmptyValueColorRule), }; diff --git a/test/api_integration/apis/search/bsearch.ts b/test/api_integration/apis/search/bsearch.ts index f539d0f9e4544e..11fb74200d7ddf 100644 --- a/test/api_integration/apis/search/bsearch.ts +++ b/test/api_integration/apis/search/bsearch.ts @@ -8,15 +8,18 @@ import expect from '@kbn/expect'; import request from 'superagent'; +import { inflateResponse } from '../../../../src/plugins/bfetch/public/streaming'; import { FtrProviderContext } from '../../ftr_provider_context'; import { painlessErrReq } from './painless_err_req'; import { verifyErrorResponse } from './verify_error'; -function parseBfetchResponse(resp: request.Response): Array> { +function parseBfetchResponse(resp: request.Response, compressed: boolean = false) { return resp.text .trim() .split('\n') - .map((item) => JSON.parse(item)); + .map((item) => { + return JSON.parse(compressed ? inflateResponse(item) : item); + }); } export default function ({ getService }: FtrProviderContext) { @@ -26,29 +29,69 @@ export default function ({ getService }: FtrProviderContext) { describe('bsearch', () => { describe('post', () => { it('should return 200 a single response', async () => { - const resp = await supertest.post(`/internal/bsearch`).send({ - batch: [ - { - request: { - params: { - body: { - query: { - match_all: {}, + const resp = await supertest + .post(`/internal/bsearch`) + .set({ 'X-Chunk-Encoding': '' }) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, + }, }, }, }, + options: { + strategy: 'es', + }, }, - }, - ], - }); + ], + }); + + const jsonBody = parseBfetchResponse(resp); + + expect(resp.status).to.be(200); + expect(jsonBody[0].id).to.be(0); + expect(jsonBody[0].result.isPartial).to.be(false); + expect(jsonBody[0].result.isRunning).to.be(false); + expect(jsonBody[0].result).to.have.property('rawResponse'); + }); + + it('should return 200 a single response from compressed', async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set({ 'X-Chunk-Encoding': 'deflate' }) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, + }, + }, + }, + }, + options: { + strategy: 'es', + }, + }, + ], + }); - const jsonBody = JSON.parse(resp.text); + const jsonBody = parseBfetchResponse(resp, true); expect(resp.status).to.be(200); - expect(jsonBody.id).to.be(0); - expect(jsonBody.result).to.have.property('isPartial'); - expect(jsonBody.result).to.have.property('isRunning'); - expect(jsonBody.result).to.have.property('rawResponse'); + expect(jsonBody[0].id).to.be(0); + expect(jsonBody[0].result.isPartial).to.be(false); + expect(jsonBody[0].result.isRunning).to.be(false); + expect(jsonBody[0].result).to.have.property('rawResponse'); }); it('should return a batch of successful responses', async () => { @@ -57,6 +100,7 @@ export default function ({ getService }: FtrProviderContext) { { request: { params: { + index: '.kibana', body: { query: { match_all: {}, @@ -68,6 +112,7 @@ export default function ({ getService }: FtrProviderContext) { { request: { params: { + index: '.kibana', body: { query: { match_all: {}, @@ -95,6 +140,7 @@ export default function ({ getService }: FtrProviderContext) { { request: { params: { + index: '.kibana', body: { query: { match_all: {}, @@ -121,6 +167,7 @@ export default function ({ getService }: FtrProviderContext) { batch: [ { request: { + index: '.kibana', indexType: 'baad', params: { body: { diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts index aa201eb6a96ff5..ab3ca2e8dd3a7b 100644 --- a/test/api_integration/apis/ui_counters/ui_counters.ts +++ b/test/api_integration/apis/ui_counters/ui_counters.ts @@ -47,7 +47,8 @@ export default function ({ getService }: FtrProviderContext) { return savedObject; }; - describe('UI Counters API', () => { + // FLAKY: https://github.com/elastic/kibana/issues/98240 + describe.skip('UI Counters API', () => { const dayDate = moment().format('DDMMYYYY'); before(async () => await esArchiver.emptyKibanaIndex()); diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts index 275ac011820bed..ee60660ae4a9eb 100644 --- a/test/functional/apps/discover/_data_grid_context.ts +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -19,7 +19,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const filterBar = getService('filterBar'); const dataGrid = getService('dataGrid'); - const docTable = getService('docTable'); const PageObjects = getPageObjects([ 'common', 'discover', @@ -67,16 +66,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dataGrid.clickRowToggle({ rowIndex: 0 }); const rowActions = await dataGrid.getRowActions({ rowIndex: 0 }); await rowActions[1].click(); - // entering the context view (contains the legacy type) - const contextFields = await docTable.getFields(); + + const contextFields = await dataGrid.getFields(); const anchorTimestamp = contextFields[0][0]; + return anchorTimestamp === firstTimestamp; }); }); it('should open the context view with the same columns', async () => { - const columnNames = await docTable.getHeaderFields(); - expect(columnNames).to.eql(['Time', ...TEST_COLUMN_NAMES]); + const columnNames = await dataGrid.getHeaderFields(); + expect(columnNames).to.eql(['Time (@timestamp)', ...TEST_COLUMN_NAMES]); }); it('should open the context view with the filters disabled', async () => { @@ -111,7 +111,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await browser.getCurrentUrl()).to.contain('#/context'); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('document table has a length of 6', async () => { - const nrOfDocs = (await docTable.getBodyRows()).length; + const nrOfDocs = (await dataGrid.getBodyRows()).length; return nrOfDocs === 6; }); }); diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index df148a35aaf24e..6f298a364abfa7 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -10,6 +10,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); const log = getService('log'); const retry = getService('retry'); const esArchiver = getService('esArchiver'); @@ -59,6 +60,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); + describe('classic table in window 900x700', async function () { + before(async () => { + await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); + await browser.setWindowSize(900, 700); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + }); + + it(`should load up to ${rowsHardLimit} rows when scrolling at the end of the table with `, async function () { + const initialRows = await testSubjects.findAll('docTableRow'); + await testSubjects.scrollIntoView('discoverBackToTop'); + // now count the rows + await retry.waitFor('next batch of documents to be displayed', async () => { + const actual = await testSubjects.findAll('docTableRow'); + log.debug(`initial doc nr: ${initialRows.length}, actual doc nr: ${actual.length}`); + return actual.length > initialRows.length; + }); + }); + }); + + describe('classic table in window 600x700', async function () { + before(async () => { + await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); + await browser.setWindowSize(600, 700); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + }); + + it(`should load up to ${rowsHardLimit} rows when scrolling at the end of the table with `, async function () { + const initialRows = await testSubjects.findAll('docTableRow'); + await testSubjects.scrollIntoView('discoverBackToTop'); + // now count the rows + await retry.waitFor('next batch of documents to be displayed', async () => { + const actual = await testSubjects.findAll('docTableRow'); + log.debug(`initial doc nr: ${initialRows.length}, actual doc nr: ${actual.length}`); + return actual.length > initialRows.length; + }); + }); + }); + describe('legacy', async function () { before(async () => { await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index d3b6edaffdbd32..4d0930c3ff932d 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -30,9 +30,9 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo const testSubjects = getService('testSubjects'); const { header } = getPageObjects(['header']); const kibanaServer = getService('kibanaServer'); - const MenuToggle = getService('MenuToggle'); + const menuToggle = getService('menuToggle'); - const quickSelectTimeMenuToggle = new MenuToggle({ + const quickSelectTimeMenuToggle = menuToggle.create({ name: 'QuickSelectTime Menu', menuTestSubject: 'superDatePickerQuickMenu', toggleButtonTestSubject: 'superDatePickerToggleQuickMenuButton', diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index a00587c9789778..f66b0fe5b9e794 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -135,6 +135,7 @@ export class DataGridService extends FtrService { if (!table) { return []; } + const cells = await table.findAllByCssSelector('.euiDataGridRowCell'); const rows: WebElementWrapper[][] = []; @@ -173,14 +174,13 @@ export class DataGridService extends FtrService { } public async getHeaderFields(): Promise { - const result = await this.find.allByCssSelector('.euiDataGridHeaderCell__content'); + const result = await this.find.allByCssSelector( + '.euiDataGridHeaderCell__button > .euiDataGridHeaderCell__content' + ); + const textArr = []; - let idx = 0; for (const cell of result) { - if (idx > 1) { - textArr.push(await cell.getVisibleText()); - } - idx++; + textArr.push(await cell.getVisibleText()); } return Promise.resolve(textArr); } diff --git a/test/functional/services/embedding.ts b/test/functional/services/embedding.ts index f3d5340e41fa6a..e394aff19ab8b6 100644 --- a/test/functional/services/embedding.ts +++ b/test/functional/services/embedding.ts @@ -6,24 +6,20 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; -export function EmbeddingProvider({ getService, getPageObjects }: FtrProviderContext) { - const browser = getService('browser'); - const log = getService('log'); - const PageObjects = getPageObjects(['header']); +export class EmbeddingService extends FtrService { + private readonly browser = this.ctx.getService('browser'); + private readonly log = this.ctx.getService('log'); + private readonly PageObjects = this.ctx.getPageObjects(['header']); - class Embedding { - /** - * Opens current page in embeded mode - */ - public async openInEmbeddedMode(): Promise { - const currentUrl = await browser.getCurrentUrl(); - log.debug(`Opening in embedded mode: ${currentUrl}`); - await browser.get(`${currentUrl}&embed=true`); - await PageObjects.header.waitUntilLoadingHasFinished(); - } + /** + * Opens current page in embeded mode + */ + public async openInEmbeddedMode(): Promise { + const currentUrl = await this.browser.getCurrentUrl(); + this.log.debug(`Opening in embedded mode: ${currentUrl}`); + await this.browser.get(`${currentUrl}&embed=true`); + await this.PageObjects.header.waitUntilLoadingHasFinished(); } - - return new Embedding(); } diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 03c43ffc302146..a509141390f676 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -26,16 +26,16 @@ import { DashboardVisualizationsService, } from './dashboard'; import { DocTableService } from './doc_table'; -import { EmbeddingProvider } from './embedding'; +import { EmbeddingService } from './embedding'; import { FilterBarService } from './filter_bar'; import { FlyoutService } from './flyout'; import { GlobalNavService } from './global_nav'; import { InspectorService } from './inspector'; import { FieldEditorService } from './field_editor'; import { ManagementMenuService } from './management'; -import { QueryBarProvider } from './query_bar'; +import { QueryBarService } from './query_bar'; import { RemoteProvider } from './remote'; -import { RenderableProvider } from './renderable'; +import { RenderableService } from './renderable'; import { ToastsService } from './toasts'; import { DataGridService } from './data_grid'; import { @@ -44,9 +44,9 @@ import { VegaDebugInspectorViewService, } from './visualizations'; import { ListingTableService } from './listing_table'; -import { SavedQueryManagementComponentProvider } from './saved_query_management_component'; +import { SavedQueryManagementComponentService } from './saved_query_management_component'; import { KibanaSupertestProvider } from './supertest'; -import { MenuToggleProvider } from './menu_toggle'; +import { MenuToggleService } from './menu_toggle'; import { MonacoEditorProvider } from './monaco_editor'; export const services = { @@ -54,7 +54,7 @@ export const services = { __webdriver__: RemoteProvider, filterBar: FilterBarService, - queryBar: QueryBarProvider, + queryBar: QueryBarService, find: FindProvider, testSubjects: TestSubjects, docTable: DocTableService, @@ -70,8 +70,8 @@ export const services = { flyout: FlyoutService, comboBox: ComboBoxService, dataGrid: DataGridService, - embedding: EmbeddingProvider, - renderable: RenderableProvider, + embedding: EmbeddingService, + renderable: RenderableService, browser: BrowserProvider, pieChart: PieChartService, inspector: InspectorService, @@ -80,10 +80,10 @@ export const services = { appsMenu: AppsMenuService, globalNav: GlobalNavService, toasts: ToastsService, - savedQueryManagementComponent: SavedQueryManagementComponentProvider, + savedQueryManagementComponent: SavedQueryManagementComponentService, elasticChart: ElasticChartService, supertest: KibanaSupertestProvider, managementMenu: ManagementMenuService, monacoEditor: MonacoEditorProvider, - MenuToggle: MenuToggleProvider, + menuToggle: MenuToggleService, }; diff --git a/test/functional/services/menu_toggle.ts b/test/functional/services/menu_toggle.ts index 866d73bd9df251..4de66a5697775d 100644 --- a/test/functional/services/menu_toggle.ts +++ b/test/functional/services/menu_toggle.ts @@ -6,61 +6,56 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; -export function MenuToggleProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - const retry = getService('retry'); - const testSubjects = getService('testSubjects'); - - interface Options { - name: string; - menuTestSubject: string; - toggleButtonTestSubject: string; - } - - return class MenuToggle { - private readonly name: string; - private readonly menuTestSubject: string; - private readonly toggleButtonTestSubject: string; +interface Options { + name: string; + menuTestSubject: string; + toggleButtonTestSubject: string; +} - constructor(options: Options) { - this.name = options.name; - this.menuTestSubject = options.menuTestSubject; - this.toggleButtonTestSubject = options.toggleButtonTestSubject; - } +export class MenuToggleService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly retry = this.ctx.getService('retry'); + private readonly testSubjects = this.ctx.getService('testSubjects'); - async open() { - await this.setState(true); - } + create(options: Options) { + const { log, retry, testSubjects } = this; + const { name, menuTestSubject, toggleButtonTestSubject } = options; - async close() { - await this.setState(false); - } - - private async setState(expectedState: boolean) { + async function setState(expectedState: boolean) { log.debug( - `setting menu open state [name=${this.name}] [state=${expectedState ? 'open' : 'closed'}]` + `setting menu open state [name=${name}] [state=${expectedState ? 'open' : 'closed'}]` ); await retry.try(async () => { // if the menu is clearly in the expected state already, bail out quickly if so - const isOpen = await testSubjects.exists(this.menuTestSubject, { timeout: 1000 }); + const isOpen = await testSubjects.exists(menuTestSubject, { timeout: 1000 }); if (isOpen === expectedState) { return; } // toggle the view state by clicking the button - await testSubjects.click(this.toggleButtonTestSubject); + await testSubjects.click(toggleButtonTestSubject); if (expectedState === true) { // wait for up to 10 seconds for the menu to show up, otherwise fail and retry - await testSubjects.existOrFail(this.menuTestSubject, { timeout: 10000 }); + await testSubjects.existOrFail(menuTestSubject, { timeout: 10000 }); } else { // wait for the form to hide, otherwise fail and retry - await testSubjects.waitForDeleted(this.menuTestSubject); + await testSubjects.waitForDeleted(menuTestSubject); } }); } - }; + + return { + async open() { + await setState(true); + }, + + async close() { + await setState(false); + }, + }; + } } diff --git a/test/functional/services/query_bar.ts b/test/functional/services/query_bar.ts index 2c4cd3b8db131f..31586d92d92a9d 100644 --- a/test/functional/services/query_bar.ts +++ b/test/functional/services/query_bar.ts @@ -7,82 +7,76 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; -export function QueryBarProvider({ getService, getPageObjects }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - const retry = getService('retry'); - const log = getService('log'); - const PageObjects = getPageObjects(['header', 'common']); - const find = getService('find'); - const browser = getService('browser'); +export class QueryBarService extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly retry = this.ctx.getService('retry'); + private readonly log = this.ctx.getService('log'); + private readonly PageObjects = this.ctx.getPageObjects(['header', 'common']); + private readonly find = this.ctx.getService('find'); + private readonly browser = this.ctx.getService('browser'); - class QueryBar { - async getQueryString(): Promise { - return await testSubjects.getAttribute('queryInput', 'value'); - } + async getQueryString(): Promise { + return await this.testSubjects.getAttribute('queryInput', 'value'); + } - public async setQuery(query: string): Promise { - log.debug(`QueryBar.setQuery(${query})`); - // Extra caution used because of flaky test here: https://github.com/elastic/kibana/issues/16978 doesn't seem - // to be actually setting the query in the query input based off - await retry.try(async () => { - await testSubjects.click('queryInput'); + public async setQuery(query: string): Promise { + this.log.debug(`QueryBar.setQuery(${query})`); + // Extra caution used because of flaky test here: https://github.com/elastic/kibana/issues/16978 doesn't seem + // to be actually setting the query in the query input based off + await this.retry.try(async () => { + await this.testSubjects.click('queryInput'); - // testSubjects.setValue uses input.clearValue which wasn't working, but input.clearValueWithKeyboard does. - // So the following lines do the same thing as input.setValue but with input.clearValueWithKeyboard instead. - const input = await find.activeElement(); - await input.clearValueWithKeyboard(); - await input.type(query); - const currentQuery = await this.getQueryString(); - if (currentQuery !== query) { - throw new Error( - `Failed to set query input to ${query}, instead query is ${currentQuery}` - ); - } - }); - } - - public async clearQuery(): Promise { - await this.setQuery(''); - await PageObjects.common.pressTabKey(); // move outside of input into language switcher - await PageObjects.common.pressTabKey(); // move outside of language switcher so time picker appears - } + // this.testSubjects.setValue uses input.clearValue which wasn't working, but input.clearValueWithKeyboard does. + // So the following lines do the same thing as input.setValue but with input.clearValueWithKeyboard instead. + const input = await this.find.activeElement(); + await input.clearValueWithKeyboard(); + await input.type(query); + const currentQuery = await this.getQueryString(); + if (currentQuery !== query) { + throw new Error(`Failed to set query input to ${query}, instead query is ${currentQuery}`); + } + }); + } - public async submitQuery(): Promise { - log.debug('QueryBar.submitQuery'); - await testSubjects.click('queryInput'); - await PageObjects.common.pressEnterKey(); - await PageObjects.header.waitUntilLoadingHasFinished(); - } + public async clearQuery(): Promise { + await this.setQuery(''); + await this.PageObjects.common.pressTabKey(); // move outside of input into language switcher + await this.PageObjects.common.pressTabKey(); // move outside of language switcher so time picker appears + } - public async clickQuerySubmitButton(): Promise { - await testSubjects.click('querySubmitButton'); - } + public async submitQuery(): Promise { + this.log.debug('QueryBar.submitQuery'); + await this.testSubjects.click('queryInput'); + await this.PageObjects.common.pressEnterKey(); + await this.PageObjects.header.waitUntilLoadingHasFinished(); + } - public async switchQueryLanguage(lang: 'kql' | 'lucene'): Promise { - await testSubjects.click('switchQueryLanguageButton'); - const kqlToggle = await testSubjects.find('languageToggle'); - const currentLang = - (await kqlToggle.getAttribute('aria-checked')) === 'true' ? 'kql' : 'lucene'; - if (lang !== currentLang) { - await kqlToggle.click(); - } + public async clickQuerySubmitButton(): Promise { + await this.testSubjects.click('querySubmitButton'); + } - await browser.pressKeys(browser.keys.ESCAPE); // close popover - await this.expectQueryLanguageOrFail(lang); // make sure lang is switched + public async switchQueryLanguage(lang: 'kql' | 'lucene'): Promise { + await this.testSubjects.click('switchQueryLanguageButton'); + const kqlToggle = await this.testSubjects.find('languageToggle'); + const currentLang = + (await kqlToggle.getAttribute('aria-checked')) === 'true' ? 'kql' : 'lucene'; + if (lang !== currentLang) { + await kqlToggle.click(); } - public async expectQueryLanguageOrFail(lang: 'kql' | 'lucene'): Promise { - const queryLanguageButton = await testSubjects.find('switchQueryLanguageButton'); - expect((await queryLanguageButton.getVisibleText()).toLowerCase()).to.eql(lang); - } + await this.browser.pressKeys(this.browser.keys.ESCAPE); // close popover + await this.expectQueryLanguageOrFail(lang); // make sure lang is switched + } - public async getSuggestions() { - const suggestions = await testSubjects.findAll('autoCompleteSuggestionText'); - return Promise.all(suggestions.map((suggestion) => suggestion.getVisibleText())); - } + public async expectQueryLanguageOrFail(lang: 'kql' | 'lucene'): Promise { + const queryLanguageButton = await this.testSubjects.find('switchQueryLanguageButton'); + expect((await queryLanguageButton.getVisibleText()).toLowerCase()).to.eql(lang); } - return new QueryBar(); + public async getSuggestions() { + const suggestions = await this.testSubjects.findAll('autoCompleteSuggestionText'); + return Promise.all(suggestions.map((suggestion) => suggestion.getVisibleText())); + } } diff --git a/test/functional/services/renderable.ts b/test/functional/services/renderable.ts index 42ea808bd64ba4..da298b6ec0343e 100644 --- a/test/functional/services/renderable.ts +++ b/test/functional/services/renderable.ts @@ -6,49 +6,45 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; const RENDER_COMPLETE_SELECTOR = '[data-render-complete="true"]'; const RENDER_COMPLETE_PENDING_SELECTOR = '[data-render-complete="false"]'; const DATA_LOADING_SELECTOR = '[data-loading]'; -export function RenderableProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - const retry = getService('retry'); - const find = getService('find'); +export class RenderableService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly retry = this.ctx.getService('retry'); + private readonly find = this.ctx.getService('find'); - class Renderable { - /** - * This method waits for a certain number of objects to finish rendering and loading, which is indicated - * by a couple tags. The RENDER_COMPLETE_SELECTOR indicates that it's done initially loading up. Some - * visualizations also add a DATA_LOADING_SELECTOR when the internal data is loading. This test will not - * return if any of those tags are found. - * @param count {Number} Number of RENDER_COMPLETE_SELECTORs to wait for. - */ - public async waitForRender(count: number = 1): Promise { - log.debug(`Renderable.waitForRender for ${count} elements`); - await retry.try(async () => { - const completedElements = await find.allByCssSelector(RENDER_COMPLETE_SELECTOR); - if (completedElements.length < count) { - const pendingElements = await find.allByCssSelector(RENDER_COMPLETE_PENDING_SELECTOR); - const pendingElementNames = []; - for (const pendingElement of pendingElements) { - const title = await pendingElement.getAttribute('data-title'); - pendingElementNames.push(title); - } - throw new Error(`${ - completedElements.length - } elements completed rendering, still waiting on a total of ${count} - specifically:\n${pendingElementNames.join('\n')}`); + /** + * This method waits for a certain number of objects to finish rendering and loading, which is indicated + * by a couple tags. The RENDER_COMPLETE_SELECTOR indicates that it's done initially loading up. Some + * visualizations also add a DATA_LOADING_SELECTOR when the internal data is loading. This test will not + * return if any of those tags are found. + * @param count {Number} Number of RENDER_COMPLETE_SELECTORs to wait for. + */ + public async waitForRender(count: number = 1): Promise { + this.log.debug(`Renderable.waitForRender for ${count} elements`); + await this.retry.try(async () => { + const completedElements = await this.find.allByCssSelector(RENDER_COMPLETE_SELECTOR); + if (completedElements.length < count) { + const pendingElements = await this.find.allByCssSelector(RENDER_COMPLETE_PENDING_SELECTOR); + const pendingElementNames = []; + for (const pendingElement of pendingElements) { + const title = await pendingElement.getAttribute('data-title'); + pendingElementNames.push(title); } + throw new Error(`${ + completedElements.length + } elements completed rendering, still waiting on a total of ${count} + specifically:\n${pendingElementNames.join('\n')}`); + } - const stillLoadingElements = await find.allByCssSelector(DATA_LOADING_SELECTOR, 1000); - if (stillLoadingElements.length > 0) { - throw new Error(`${stillLoadingElements.length} elements still loading contents`); - } - }); - } + const stillLoadingElements = await this.find.allByCssSelector(DATA_LOADING_SELECTOR, 1000); + if (stillLoadingElements.length > 0) { + throw new Error(`${stillLoadingElements.length} elements still loading contents`); + } + }); } - - return new Renderable(); } diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index d4fa34f224547c..aabe8c0aebb0c6 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -7,210 +7,205 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; - -export function SavedQueryManagementComponentProvider({ - getService, - getPageObjects, -}: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - const queryBar = getService('queryBar'); - const retry = getService('retry'); - const config = getService('config'); - const PageObjects = getPageObjects(['common']); - - class SavedQueryManagementComponent { - public async getCurrentlyLoadedQueryID() { - await this.openSavedQueryManagementComponent(); - try { - return await testSubjects.getVisibleText('~saved-query-list-item-selected'); - } catch { - return undefined; - } - } - - public async saveNewQuery( - name: string, - description: string, - includeFilters: boolean, - includeTimeFilter: boolean - ) { - await this.openSaveCurrentQueryModal(); - await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter); +import { FtrService } from '../ftr_provider_context'; + +export class SavedQueryManagementComponentService extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly queryBar = this.ctx.getService('queryBar'); + private readonly retry = this.ctx.getService('retry'); + private readonly config = this.ctx.getService('config'); + private readonly PageObjects = this.ctx.getPageObjects(['common']); + + public async getCurrentlyLoadedQueryID() { + await this.openSavedQueryManagementComponent(); + try { + return await this.testSubjects.getVisibleText('~saved-query-list-item-selected'); + } catch { + return undefined; } + } - public async saveNewQueryWithNameError(name?: string) { - await this.openSaveCurrentQueryModal(); - if (name) { - await testSubjects.setValue('saveQueryFormTitle', name); - } - - // Form input validation only happens onBlur. Clicking the save button should de-focus the - // input element and the validation should prevent a save from actually happening if there's - // an error. - await testSubjects.click('savedQueryFormSaveButton'); - - await retry.waitForWithTimeout('save button to be disabled', 1000, async () => { - const saveQueryFormSaveButtonStatus = await testSubjects.isEnabled( - 'savedQueryFormSaveButton' - ); - return saveQueryFormSaveButtonStatus === false; - }); + public async saveNewQuery( + name: string, + description: string, + includeFilters: boolean, + includeTimeFilter: boolean + ) { + await this.openSaveCurrentQueryModal(); + await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter); + } - await testSubjects.click('savedQueryFormCancelButton'); + public async saveNewQueryWithNameError(name?: string) { + await this.openSaveCurrentQueryModal(); + if (name) { + await this.testSubjects.setValue('saveQueryFormTitle', name); } - public async saveCurrentlyLoadedAsNewQuery( - name: string, - description: string, - includeFilters: boolean, - includeTimeFilter: boolean - ) { - await this.openSavedQueryManagementComponent(); - await testSubjects.click('saved-query-management-save-as-new-button'); - await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter); - } + // Form input validation only happens onBlur. Clicking the save button should de-focus the + // input element and the validation should prevent a save from actually happening if there's + // an error. + await this.testSubjects.click('savedQueryFormSaveButton'); - public async updateCurrentlyLoadedQuery( - description: string, - includeFilters: boolean, - includeTimeFilter: boolean - ) { - await this.openSavedQueryManagementComponent(); - await testSubjects.click('saved-query-management-save-changes-button'); - await this.submitSaveQueryForm(null, description, includeFilters, includeTimeFilter); - } + await this.retry.waitForWithTimeout('save button to be disabled', 1000, async () => { + const saveQueryFormSaveButtonStatus = await this.testSubjects.isEnabled( + 'savedQueryFormSaveButton' + ); + return saveQueryFormSaveButtonStatus === false; + }); - public async loadSavedQuery(title: string) { - await this.openSavedQueryManagementComponent(); - await testSubjects.click(`~load-saved-query-${title}-button`); - await retry.try(async () => { - await this.openSavedQueryManagementComponent(); - const selectedSavedQueryText = await testSubjects.getVisibleText( - '~saved-query-list-item-selected' - ); - expect(selectedSavedQueryText).to.eql(title); - }); - await this.closeSavedQueryManagementComponent(); - } + await this.testSubjects.click('savedQueryFormCancelButton'); + } - public async deleteSavedQuery(title: string) { - await this.openSavedQueryManagementComponent(); - await testSubjects.click(`~delete-saved-query-${title}-button`); - await PageObjects.common.clickConfirmOnModal(); - } + public async saveCurrentlyLoadedAsNewQuery( + name: string, + description: string, + includeFilters: boolean, + includeTimeFilter: boolean + ) { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.click('saved-query-management-save-as-new-button'); + await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter); + } - async clearCurrentlyLoadedQuery() { + public async updateCurrentlyLoadedQuery( + description: string, + includeFilters: boolean, + includeTimeFilter: boolean + ) { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.click('saved-query-management-save-changes-button'); + await this.submitSaveQueryForm(null, description, includeFilters, includeTimeFilter); + } + + public async loadSavedQuery(title: string) { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.click(`~load-saved-query-${title}-button`); + await this.retry.try(async () => { await this.openSavedQueryManagementComponent(); - await testSubjects.click('saved-query-management-clear-button'); - await this.closeSavedQueryManagementComponent(); - const queryString = await queryBar.getQueryString(); - expect(queryString).to.be.empty(); - } + const selectedSavedQueryText = await this.testSubjects.getVisibleText( + '~saved-query-list-item-selected' + ); + expect(selectedSavedQueryText).to.eql(title); + }); + await this.closeSavedQueryManagementComponent(); + } - async submitSaveQueryForm( - title: string | null, - description: string, - includeFilters: boolean, - includeTimeFilter: boolean - ) { - if (title) { - await testSubjects.setValue('saveQueryFormTitle', title); - } - await testSubjects.setValue('saveQueryFormDescription', description); - - const currentIncludeFiltersValue = - (await testSubjects.getAttribute('saveQueryFormIncludeFiltersOption', 'aria-checked')) === - 'true'; - if (currentIncludeFiltersValue !== includeFilters) { - await testSubjects.click('saveQueryFormIncludeFiltersOption'); - } - - const currentIncludeTimeFilterValue = - (await testSubjects.getAttribute( - 'saveQueryFormIncludeTimeFilterOption', - 'aria-checked' - )) === 'true'; - if (currentIncludeTimeFilterValue !== includeTimeFilter) { - await testSubjects.click('saveQueryFormIncludeTimeFilterOption'); - } - - await testSubjects.click('savedQueryFormSaveButton'); - } + public async deleteSavedQuery(title: string) { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.click(`~delete-saved-query-${title}-button`); + await this.PageObjects.common.clickConfirmOnModal(); + } - async savedQueryExist(title: string) { - await this.openSavedQueryManagementComponent(); - const exists = testSubjects.exists(`~load-saved-query-${title}-button`); - await this.closeSavedQueryManagementComponent(); - return exists; - } + async clearCurrentlyLoadedQuery() { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.click('saved-query-management-clear-button'); + await this.closeSavedQueryManagementComponent(); + const queryString = await this.queryBar.getQueryString(); + expect(queryString).to.be.empty(); + } - async savedQueryExistOrFail(title: string) { - await this.openSavedQueryManagementComponent(); - await testSubjects.existOrFail(`~load-saved-query-${title}-button`); + async submitSaveQueryForm( + title: string | null, + description: string, + includeFilters: boolean, + includeTimeFilter: boolean + ) { + if (title) { + await this.testSubjects.setValue('saveQueryFormTitle', title); } + await this.testSubjects.setValue('saveQueryFormDescription', description); - async savedQueryTextExist(text: string) { - await this.openSavedQueryManagementComponent(); - const queryString = await queryBar.getQueryString(); - expect(queryString).to.eql(text); + const currentIncludeFiltersValue = + (await this.testSubjects.getAttribute( + 'saveQueryFormIncludeFiltersOption', + 'aria-checked' + )) === 'true'; + if (currentIncludeFiltersValue !== includeFilters) { + await this.testSubjects.click('saveQueryFormIncludeFiltersOption'); } - async savedQueryMissingOrFail(title: string) { - await retry.try(async () => { - await this.openSavedQueryManagementComponent(); - await testSubjects.missingOrFail(`~load-saved-query-${title}-button`); - }); - await this.closeSavedQueryManagementComponent(); + const currentIncludeTimeFilterValue = + (await this.testSubjects.getAttribute( + 'saveQueryFormIncludeTimeFilterOption', + 'aria-checked' + )) === 'true'; + if (currentIncludeTimeFilterValue !== includeTimeFilter) { + await this.testSubjects.click('saveQueryFormIncludeTimeFilterOption'); } - async openSavedQueryManagementComponent() { - const isOpenAlready = await testSubjects.exists('saved-query-management-popover'); - if (isOpenAlready) return; + await this.testSubjects.click('savedQueryFormSaveButton'); + } - await retry.waitFor('saved query management popover to have any text', async () => { - await testSubjects.click('saved-query-management-popover-button'); - const queryText = await testSubjects.getVisibleText('saved-query-management-popover'); - return queryText.length > 0; - }); - } + async savedQueryExist(title: string) { + await this.openSavedQueryManagementComponent(); + const exists = this.testSubjects.exists(`~load-saved-query-${title}-button`); + await this.closeSavedQueryManagementComponent(); + return exists; + } - async closeSavedQueryManagementComponent() { - const isOpenAlready = await testSubjects.exists('saved-query-management-popover'); - if (!isOpenAlready) return; + async savedQueryExistOrFail(title: string) { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.existOrFail(`~load-saved-query-${title}-button`); + } - await retry.try(async () => { - await testSubjects.click('saved-query-management-popover-button'); - await testSubjects.missingOrFail('saved-query-management-popover'); - }); - } + async savedQueryTextExist(text: string) { + await this.openSavedQueryManagementComponent(); + const queryString = await this.queryBar.getQueryString(); + expect(queryString).to.eql(text); + } - async openSaveCurrentQueryModal() { + async savedQueryMissingOrFail(title: string) { + await this.retry.try(async () => { await this.openSavedQueryManagementComponent(); + await this.testSubjects.missingOrFail(`~load-saved-query-${title}-button`); + }); + await this.closeSavedQueryManagementComponent(); + } - await retry.try(async () => { - await testSubjects.click('saved-query-management-save-button'); - await testSubjects.existOrFail('saveQueryForm', { - timeout: config.get('timeouts.waitForExists'), - }); - }); - } + async openSavedQueryManagementComponent() { + const isOpenAlready = await this.testSubjects.exists('saved-query-management-popover'); + if (isOpenAlready) return; - async saveNewQueryMissingOrFail() { - await this.openSavedQueryManagementComponent(); - await testSubjects.missingOrFail('saved-query-management-save-button'); - } + await this.retry.waitFor('saved query management popover to have any text', async () => { + await this.testSubjects.click('saved-query-management-popover-button'); + const queryText = await this.testSubjects.getVisibleText('saved-query-management-popover'); + return queryText.length > 0; + }); + } - async updateCurrentlyLoadedQueryMissingOrFail() { - await this.openSavedQueryManagementComponent(); - await testSubjects.missingOrFail('saved-query-management-save-changes-button'); - } + async closeSavedQueryManagementComponent() { + const isOpenAlready = await this.testSubjects.exists('saved-query-management-popover'); + if (!isOpenAlready) return; - async deleteSavedQueryMissingOrFail(title: string) { - await this.openSavedQueryManagementComponent(); - await testSubjects.missingOrFail(`delete-saved-query-${title}-button`); - } + await this.retry.try(async () => { + await this.testSubjects.click('saved-query-management-popover-button'); + await this.testSubjects.missingOrFail('saved-query-management-popover'); + }); + } + + async openSaveCurrentQueryModal() { + await this.openSavedQueryManagementComponent(); + + await this.retry.try(async () => { + await this.testSubjects.click('saved-query-management-save-button'); + await this.testSubjects.existOrFail('saveQueryForm', { + timeout: this.config.get('timeouts.waitForExists'), + }); + }); + } + + async saveNewQueryMissingOrFail() { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.missingOrFail('saved-query-management-save-button'); } - return new SavedQueryManagementComponent(); + async updateCurrentlyLoadedQueryMissingOrFail() { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.missingOrFail('saved-query-management-save-changes-button'); + } + + async deleteSavedQueryMissingOrFail(title: string) { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.missingOrFail(`delete-saved-query-${title}-button`); + } } diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts index 38565b1e2c0a85..65a2ce02aa0a45 100644 --- a/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts @@ -18,6 +18,7 @@ async function getDeprecations({ message: `CorePluginDeprecationsPlugin is a deprecated feature for testing.`, documentationUrl: 'test-url', level: 'warning', + deprecationType: 'feature', correctiveActions: { manualSteps: ['Step a', 'Step b'], }, diff --git a/test/plugin_functional/test_suites/core/deprecations.ts b/test/plugin_functional/test_suites/core/deprecations.ts index a78527d0d82e25..99b1a79fb51e34 100644 --- a/test/plugin_functional/test_suites/core/deprecations.ts +++ b/test/plugin_functional/test_suites/core/deprecations.ts @@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const PageObjects = getPageObjects(['common']); const browser = getService('browser'); - const CorePluginDeprecationsPluginDeprecations = [ + const CorePluginDeprecationsPluginDeprecations: DomainDeprecationDetails[] = [ { level: 'critical', message: @@ -27,6 +27,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide 'Replace "corePluginDeprecations.oldProperty" with "corePluginDeprecations.newProperty" in the Kibana config file, CLI flag, or environment variable (in Docker only).', ], }, + deprecationType: 'config', domainId: 'corePluginDeprecations', }, { @@ -37,6 +38,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide 'Remove "corePluginDeprecations.noLongerUsed" from the Kibana config file, CLI flag, or environment variable (in Docker only)', ], }, + deprecationType: 'config', domainId: 'corePluginDeprecations', }, { @@ -45,6 +47,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide 'Kibana plugin functional tests will no longer allow corePluginDeprecations.secret config to be set to anything except 42.', correctiveActions: {}, documentationUrl: 'config-secret-doc-url', + deprecationType: 'config', domainId: 'corePluginDeprecations', }, { @@ -54,6 +57,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide correctiveActions: { manualSteps: ['Step a', 'Step b'], }, + deprecationType: 'feature', domainId: 'corePluginDeprecations', }, { diff --git a/x-pack/plugins/alerting/server/config.test.ts b/x-pack/plugins/alerting/server/config.test.ts index 069c41605ccfbc..a8befe5210752e 100644 --- a/x-pack/plugins/alerting/server/config.test.ts +++ b/x-pack/plugins/alerting/server/config.test.ts @@ -8,10 +8,11 @@ import { configSchema } from './config'; describe('config validation', () => { - test('alerts defaults', () => { + test('alerting defaults', () => { const config: Record = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { + "enableImportExport": false, "healthCheck": Object { "interval": "60m", }, diff --git a/x-pack/plugins/alerting/server/config.ts b/x-pack/plugins/alerting/server/config.ts index e42955b385bf1e..d50917fd135785 100644 --- a/x-pack/plugins/alerting/server/config.ts +++ b/x-pack/plugins/alerting/server/config.ts @@ -16,6 +16,7 @@ export const configSchema = schema.object({ interval: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }), removalDelay: schema.string({ validate: validateDurationSchema, defaultValue: '1h' }), }), + enableImportExport: schema.boolean({ defaultValue: false }), }); export type AlertsConfig = TypeOf; diff --git a/x-pack/plugins/alerting/server/health/get_state.test.ts b/x-pack/plugins/alerting/server/health/get_state.test.ts index 643d966d1fad0b..96627e10fb3bdf 100644 --- a/x-pack/plugins/alerting/server/health/get_state.test.ts +++ b/x-pack/plugins/alerting/server/health/get_state.test.ts @@ -72,6 +72,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { interval: '5m', removalDelay: '1h', }, + enableImportExport: false, }), pollInterval ).subscribe(); @@ -107,6 +108,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { interval: '5m', removalDelay: '1h', }, + enableImportExport: false, }), pollInterval, retryDelay @@ -152,6 +154,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { interval: '5m', removalDelay: '1h', }, + enableImportExport: false, }) ).toPromise(); @@ -182,6 +185,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { interval: '5m', removalDelay: '1h', }, + enableImportExport: false, }) ).toPromise(); @@ -212,6 +216,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { interval: '5m', removalDelay: '1h', }, + enableImportExport: false, }) ).toPromise(); @@ -239,6 +244,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { interval: '5m', removalDelay: '1h', }, + enableImportExport: false, }), retryDelay ).subscribe((status) => { @@ -269,6 +275,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { interval: '5m', removalDelay: '1h', }, + enableImportExport: false, }), retryDelay ).subscribe((status) => { @@ -305,6 +312,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { interval: '5m', removalDelay: '1h', }, + enableImportExport: false, }) ).toPromise(); diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index ec4b7095d67f7f..4e9249944a6bf9 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -18,6 +18,7 @@ import { AlertsConfig } from './config'; import { AlertType } from './types'; import { eventLogMock } from '../../event_log/server/mocks'; import { actionsMock } from '../../actions/server/mocks'; +import mappings from './saved_objects/mappings.json'; describe('Alerting Plugin', () => { describe('setup()', () => { @@ -25,6 +26,8 @@ describe('Alerting Plugin', () => { let coreSetup: ReturnType; let pluginsSetup: jest.Mocked; + beforeEach(() => jest.clearAllMocks()); + it('should log warning when Encrypted Saved Objects plugin is missing encryption key', async () => { const context = coreMock.createPluginInitializerContext({ healthCheck: { @@ -34,6 +37,7 @@ describe('Alerting Plugin', () => { interval: '5m', removalDelay: '1h', }, + enableImportExport: false, }); plugin = new AlertingPlugin(context); @@ -57,6 +61,72 @@ describe('Alerting Plugin', () => { ); }); + it('should register saved object with no management capability if enableImportExport is false', async () => { + const context = coreMock.createPluginInitializerContext({ + healthCheck: { + interval: '5m', + }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + enableImportExport: false, + }); + plugin = new AlertingPlugin(context); + + const setupMocks = coreMock.createSetup(); + await plugin.setup(setupMocks, { + licensing: licensingMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(), + taskManager: taskManagerMock.createSetup(), + eventLog: eventLogServiceMock.create(), + actions: actionsMock.createSetup(), + statusService: statusServiceMock.createSetupContract(), + }); + + expect(setupMocks.savedObjects.registerType).toHaveBeenCalledTimes(2); + const registerAlertingSavedObject = setupMocks.savedObjects.registerType.mock.calls[0][0]; + expect(registerAlertingSavedObject.name).toEqual('alert'); + expect(registerAlertingSavedObject.hidden).toBe(true); + expect(registerAlertingSavedObject.mappings).toEqual(mappings.alert); + expect(registerAlertingSavedObject.management).toBeUndefined(); + }); + + it('should register saved object with import/export capability if enableImportExport is true', async () => { + const context = coreMock.createPluginInitializerContext({ + healthCheck: { + interval: '5m', + }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + enableImportExport: true, + }); + plugin = new AlertingPlugin(context); + + const setupMocks = coreMock.createSetup(); + await plugin.setup(setupMocks, { + licensing: licensingMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(), + taskManager: taskManagerMock.createSetup(), + eventLog: eventLogServiceMock.create(), + actions: actionsMock.createSetup(), + statusService: statusServiceMock.createSetupContract(), + }); + + expect(setupMocks.savedObjects.registerType).toHaveBeenCalledTimes(2); + const registerAlertingSavedObject = setupMocks.savedObjects.registerType.mock.calls[0][0]; + expect(registerAlertingSavedObject.name).toEqual('alert'); + expect(registerAlertingSavedObject.hidden).toBe(true); + expect(registerAlertingSavedObject.mappings).toEqual(mappings.alert); + expect(registerAlertingSavedObject.management).not.toBeUndefined(); + expect(registerAlertingSavedObject.management?.importableAndExportable).toBe(true); + expect(registerAlertingSavedObject.management?.getTitle).not.toBeUndefined(); + expect(registerAlertingSavedObject.management?.onImport).not.toBeUndefined(); + expect(registerAlertingSavedObject.management?.onExport).not.toBeUndefined(); + }); + describe('registerType()', () => { let setup: PluginSetupContract; const sampleAlertType: AlertType = { @@ -119,6 +189,7 @@ describe('Alerting Plugin', () => { interval: '5m', removalDelay: '1h', }, + enableImportExport: false, }); const plugin = new AlertingPlugin(context); @@ -158,6 +229,7 @@ describe('Alerting Plugin', () => { interval: '5m', removalDelay: '1h', }, + enableImportExport: false, }); const plugin = new AlertingPlugin(context); @@ -211,6 +283,7 @@ describe('Alerting Plugin', () => { interval: '5m', removalDelay: '1h', }, + enableImportExport: false, }); const plugin = new AlertingPlugin(context); diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 990733c320dfe8..769243b8feaf6a 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -190,7 +190,7 @@ export class AlertingPlugin { event: { provider: EVENT_LOG_PROVIDER }, }); - setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); + setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects, this.config); this.eventLogService = plugins.eventLog; plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index 6b76fd97dc53b7..c339183eeedcdb 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -16,6 +16,7 @@ import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objec import { transformRulesForExport } from './transform_rule_for_export'; import { RawAlert } from '../types'; import { getImportWarnings } from './get_import_warnings'; +import { AlertsConfig } from '../config'; export { partiallyUpdateAlert } from './partially_update_alert'; export const AlertAttributesExcludedFromAAD = [ @@ -41,59 +42,66 @@ export type AlertAttributesExcludedFromAADType = export function setupSavedObjects( savedObjects: SavedObjectsServiceSetup, - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, + alertingConfig: Promise ) { - savedObjects.registerType({ - name: 'alert', - hidden: true, - namespaceType: 'single', - migrations: getMigrations(encryptedSavedObjects), - mappings: mappings.alert, - management: { - importableAndExportable: true, - getTitle(ruleSavedObject: SavedObject) { - return `Rule: [${ruleSavedObject.attributes.name}]`; - }, - onImport(ruleSavedObjects) { - return { - warnings: getImportWarnings(ruleSavedObjects), - }; - }, - onExport( - context: SavedObjectsExportTransformContext, - objects: Array> - ) { - return transformRulesForExport(objects); - }, - }, - }); + alertingConfig.then((config: AlertsConfig) => { + savedObjects.registerType({ + name: 'alert', + hidden: true, + namespaceType: 'single', + migrations: getMigrations(encryptedSavedObjects), + mappings: mappings.alert, + ...(config.enableImportExport + ? { + management: { + importableAndExportable: true, + getTitle(ruleSavedObject: SavedObject) { + return `Rule: [${ruleSavedObject.attributes.name}]`; + }, + onImport(ruleSavedObjects) { + return { + warnings: getImportWarnings(ruleSavedObjects), + }; + }, + onExport( + context: SavedObjectsExportTransformContext, + objects: Array> + ) { + return transformRulesForExport(objects); + }, + }, + } + : {}), + }); - savedObjects.registerType({ - name: 'api_key_pending_invalidation', - hidden: true, - namespaceType: 'agnostic', - mappings: { - properties: { - apiKeyId: { - type: 'keyword', - }, - createdAt: { - type: 'date', + savedObjects.registerType({ + name: 'api_key_pending_invalidation', + hidden: true, + namespaceType: 'agnostic', + mappings: { + properties: { + apiKeyId: { + type: 'keyword', + }, + createdAt: { + type: 'date', + }, }, }, - }, - }); + }); - // Encrypted attributes - encryptedSavedObjects.registerType({ - type: 'alert', - attributesToEncrypt: new Set(['apiKey']), - attributesToExcludeFromAAD: new Set(AlertAttributesExcludedFromAAD), - }); + // Encrypted attributes + encryptedSavedObjects.registerType({ + type: 'alert', + attributesToEncrypt: new Set(['apiKey']), + attributesToExcludeFromAAD: new Set(AlertAttributesExcludedFromAAD), + }); - // Encrypted attributes - encryptedSavedObjects.registerType({ - type: 'api_key_pending_invalidation', - attributesToEncrypt: new Set(['apiKeyId']), + // Encrypted attributes + encryptedSavedObjects.registerType({ + type: 'api_key_pending_invalidation', + attributesToEncrypt: new Set(['apiKeyId']), + }); }); } diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 120ab6de296dd8..78d74b78c99ba6 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -180,7 +180,8 @@ test('enqueues execution per selected action', async () => { `); expect(jest.requireMock('./inject_action_params').injectActionParams).toHaveBeenCalledWith({ - alertId: '1', + ruleId: '1', + spaceId: 'default', actionTypeId: 'test', actionParams: { alertVal: 'My 1 name-of-alert default tag-A,tag-B 2 goes here', diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 2ecf5404856954..93cced2043d5e1 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -138,7 +138,8 @@ export function createExecutionHandler< .map((action) => ({ ...action, params: injectActionParams({ - alertId, + ruleId: alertId, + spaceId, actionParams: action.params, actionTypeId: action.actionTypeId, }), diff --git a/x-pack/plugins/alerting/server/task_runner/inject_action_params.test.ts b/x-pack/plugins/alerting/server/task_runner/inject_action_params.test.ts index 62d834eb91da0d..0416a3c4d1214a 100644 --- a/x-pack/plugins/alerting/server/task_runner/inject_action_params.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/inject_action_params.test.ts @@ -14,7 +14,8 @@ describe('injectActionParams', () => { }; const result = injectActionParams({ actionParams, - alertId: '1', + ruleId: '1', + spaceId: 'the-space', actionTypeId: '.server-log', }); expect(result).toMatchInlineSnapshot(` @@ -32,7 +33,8 @@ describe('injectActionParams', () => { }; const result = injectActionParams({ actionParams, - alertId: '1', + ruleId: '1', + spaceId: 'default', actionTypeId: '.email', }); expect(result).toMatchInlineSnapshot(` @@ -41,8 +43,58 @@ describe('injectActionParams', () => { "message": "State: \\"{{state.value}}\\", Context: \\"{{context.value}}\\"", }, "kibanaFooterLink": Object { - "path": "/app/management/insightsAndAlerting/triggersActions/alert/1", - "text": "View alert in Kibana", + "path": "/app/management/insightsAndAlerting/triggersActions/rule/1", + "text": "View rule in Kibana", + }, + } + `); + }); + + test('injects viewInKibanaPath and viewInKibanaText when actionTypeId is .email and spaceId is undefined', () => { + const actionParams = { + body: { + message: 'State: "{{state.value}}", Context: "{{context.value}}"', + }, + }; + const result = injectActionParams({ + actionParams, + ruleId: '1', + spaceId: undefined, + actionTypeId: '.email', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "body": Object { + "message": "State: \\"{{state.value}}\\", Context: \\"{{context.value}}\\"", + }, + "kibanaFooterLink": Object { + "path": "/app/management/insightsAndAlerting/triggersActions/rule/1", + "text": "View rule in Kibana", + }, + } + `); + }); + + test('injects viewInKibanaPath with space ID and viewInKibanaText when actionTypeId is .email', () => { + const actionParams = { + body: { + message: 'State: "{{state.value}}", Context: "{{context.value}}"', + }, + }; + const result = injectActionParams({ + actionParams, + ruleId: '1', + spaceId: 'not-the-default', + actionTypeId: '.email', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "body": Object { + "message": "State: \\"{{state.value}}\\", Context: \\"{{context.value}}\\"", + }, + "kibanaFooterLink": Object { + "path": "/s/not-the-default/app/management/insightsAndAlerting/triggersActions/rule/1", + "text": "View rule in Kibana", }, } `); diff --git a/x-pack/plugins/alerting/server/task_runner/inject_action_params.ts b/x-pack/plugins/alerting/server/task_runner/inject_action_params.ts index 177622867565cd..11ac3f92d1071b 100644 --- a/x-pack/plugins/alerting/server/task_runner/inject_action_params.ts +++ b/x-pack/plugins/alerting/server/task_runner/inject_action_params.ts @@ -9,25 +9,29 @@ import { i18n } from '@kbn/i18n'; import { AlertActionParams } from '../types'; export interface InjectActionParamsOpts { - alertId: string; + ruleId: string; + spaceId: string | undefined; actionTypeId: string; actionParams: AlertActionParams; } export function injectActionParams({ - alertId, + ruleId, + spaceId, actionTypeId, actionParams, }: InjectActionParamsOpts) { // Inject kibanaFooterLink if action type is email. This is used by the email action type // to inject a "View alert in Kibana" with a URL in the email's footer. if (actionTypeId === '.email') { + const spacePrefix = + spaceId && spaceId.length > 0 && spaceId !== 'default' ? `/s/${spaceId}` : ''; return { ...actionParams, kibanaFooterLink: { - path: `/app/management/insightsAndAlerting/triggersActions/alert/${alertId}`, + path: `${spacePrefix}/app/management/insightsAndAlerting/triggersActions/rule/${ruleId}`, text: i18n.translate('xpack.alerting.injectActionParams.email.kibanaFooterLinkText', { - defaultMessage: 'View alert in Kibana', + defaultMessage: 'View rule in Kibana', }), }, }; diff --git a/x-pack/plugins/ingest_pipelines/server/services/index.ts b/x-pack/plugins/apm/common/index_pattern_constants.ts similarity index 77% rename from x-pack/plugins/ingest_pipelines/server/services/index.ts rename to x-pack/plugins/apm/common/index_pattern_constants.ts index b006d728052574..4b67bba1fef910 100644 --- a/x-pack/plugins/ingest_pipelines/server/services/index.ts +++ b/x-pack/plugins/apm/common/index_pattern_constants.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { License } from './license'; +export const APM_STATIC_INDEX_PATTERN_ID = 'apm_static_index_pattern_id'; diff --git a/src/plugins/apm_oss/public/assets/apm.png b/x-pack/plugins/apm/public/assets/apm.png similarity index 100% rename from src/plugins/apm_oss/public/assets/apm.png rename to x-pack/plugins/apm/public/assets/apm.png diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts index 8954adf2c18c71..c998964b864003 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts @@ -22,7 +22,7 @@ import { SYMBOLIZE_AS_TYPES, } from '../../../../../../maps/common/constants'; -import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; +import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../common/index_pattern_constants'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts index 7a40880eb90536..e989577ac15aaa 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts @@ -17,7 +17,7 @@ import { USER_AGENT_OS, } from '../../../../../common/elasticsearch_fieldnames'; -import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; +import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../common/index_pattern_constants'; const getWildcardFilter = (field: string, value: string): Filter => { return { diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx index 4343d504c33739..919d140c54c1eb 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import rison, { RisonValue } from 'rison-node'; import url from 'url'; -import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; +import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../common/index_pattern_constants'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { getTimepickerRisonData } from '../rison_helpers'; diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts index e627e9ed1d6cf6..607a7e6227a9d6 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts @@ -6,10 +6,8 @@ */ import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; -import { - apmIndexPattern, - APM_STATIC_INDEX_PATTERN_ID, -} from '../../../../../../src/plugins/apm_oss/server'; +import { APM_STATIC_INDEX_PATTERN_ID } from '../../../common/index_pattern_constants'; +import apmIndexPattern from '../../tutorial/index_pattern.json'; import { hasHistoricalAgentData } from '../services/get_services/has_historical_agent_data'; import { Setup } from '../helpers/setup_request'; import { APMRouteHandlerResources } from '../../routes/typings'; diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 8d83f762e2023e..824eba9bce0b08 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import { combineLatest } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { @@ -32,7 +31,6 @@ import { createApmAgentConfigurationIndex } from './lib/settings/agent_configura import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices'; import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_custom_link_index'; import { apmIndices, apmTelemetry } from './saved_objects'; -import { createElasticCloudInstructions } from './tutorial/elastic_cloud'; import { uiSettings } from './ui_settings'; import type { ApmPluginRequestHandlerContext, @@ -51,6 +49,7 @@ import { SERVICE_NAME, TRANSACTION_TYPE, } from '../common/elasticsearch_fieldnames'; +import { tutorialProvider } from './tutorial'; export class APMPlugin implements @@ -103,28 +102,20 @@ export class APMPlugin }); } - const ossTutorialProvider = plugins.apmOss.getRegisteredTutorialProvider(); - plugins.home?.tutorials.unregisterTutorial(ossTutorialProvider); - plugins.home?.tutorials.registerTutorial(() => { - const ossPart = ossTutorialProvider({}); - if (this.currentConfig!['xpack.apm.ui.enabled'] && ossPart.artifacts) { - // @ts-expect-error ossPart.artifacts.application is readonly - ossPart.artifacts.application = { - path: '/app/apm', - label: i18n.translate( - 'xpack.apm.tutorial.specProvider.artifacts.application.label', - { - defaultMessage: 'Launch APM', - } - ), - }; - } - - return { - ...ossPart, - elasticCloud: createElasticCloudInstructions(plugins.cloud), - }; - }); + plugins.home?.tutorials.registerTutorial( + tutorialProvider({ + isEnabled: this.currentConfig['xpack.apm.ui.enabled'], + indexPatternTitle: this.currentConfig['apm_oss.indexPattern'], + cloud: plugins.cloud, + indices: { + errorIndices: this.currentConfig['apm_oss.errorIndices'], + metricsIndices: this.currentConfig['apm_oss.metricsIndices'], + onboardingIndices: this.currentConfig['apm_oss.onboardingIndices'], + sourcemapIndices: this.currentConfig['apm_oss.sourcemapIndices'], + transactionIndices: this.currentConfig['apm_oss.transactionIndices'], + }, + }) + ); plugins.features.registerKibanaFeature(APM_FEATURE); diff --git a/x-pack/plugins/apm/server/tutorial/elastic_cloud.ts b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts similarity index 94% rename from x-pack/plugins/apm/server/tutorial/elastic_cloud.ts rename to x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts index 08e1ff75d43242..c6afd6a592fff2 100644 --- a/x-pack/plugins/apm/server/tutorial/elastic_cloud.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from '../../../../../src/plugins/home/server'; +import { INSTRUCTION_VARIANT } from '../../../../../../src/plugins/home/server'; import { createNodeAgentInstructions, @@ -19,8 +19,8 @@ import { createJavaAgentInstructions, createDotNetAgentInstructions, createPhpAgentInstructions, -} from '../../../../../src/plugins/apm_oss/server'; -import { CloudSetup } from '../../../cloud/server'; +} from '../instructions/apm_agent_instructions'; +import { CloudSetup } from '../../../../cloud/server'; export function createElasticCloudInstructions(cloudSetup?: CloudSetup) { const apmServerUrl = cloudSetup?.apm.url; diff --git a/src/plugins/apm_oss/server/tutorial/envs/on_prem.ts b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts similarity index 52% rename from src/plugins/apm_oss/server/tutorial/envs/on_prem.ts rename to x-pack/plugins/apm/server/tutorial/envs/on_prem.ts index 7d261abb0cc018..a0e96f563381cf 100644 --- a/src/plugins/apm_oss/server/tutorial/envs/on_prem.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts @@ -1,13 +1,12 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from '../../../../home/server'; +import { INSTRUCTION_VARIANT } from '../../../../../../src/plugins/home/server'; import { createWindowsServerInstructions, createEditConfig, @@ -50,31 +49,46 @@ export function onPremInstructions({ return { instructionSets: [ { - title: i18n.translate('apmOss.tutorial.apmServer.title', { + title: i18n.translate('xpack.apm.tutorial.apmServer.title', { defaultMessage: 'APM Server', }), callOut: { - title: i18n.translate('apmOss.tutorial.apmServer.callOut.title', { + title: i18n.translate('xpack.apm.tutorial.apmServer.callOut.title', { defaultMessage: 'Important: Updating to 7.0 or higher', }), - message: i18n.translate('apmOss.tutorial.apmServer.callOut.message', { - defaultMessage: `Please make sure your APM Server is updated to 7.0 or higher. \ + message: i18n.translate( + 'xpack.apm.tutorial.apmServer.callOut.message', + { + defaultMessage: `Please make sure your APM Server is updated to 7.0 or higher. \ You can also migrate your 6.x data with the migration assistant found in Kibana's management section.`, - }), + } + ), iconType: 'alert', }, instructionVariants: [ { id: INSTRUCTION_VARIANT.OSX, - instructions: [createDownloadServerOsx(), EDIT_CONFIG, START_SERVER_UNIX], + instructions: [ + createDownloadServerOsx(), + EDIT_CONFIG, + START_SERVER_UNIX, + ], }, { id: INSTRUCTION_VARIANT.DEB, - instructions: [createDownloadServerDeb(), EDIT_CONFIG, START_SERVER_UNIX_SYSV], + instructions: [ + createDownloadServerDeb(), + EDIT_CONFIG, + START_SERVER_UNIX_SYSV, + ], }, { id: INSTRUCTION_VARIANT.RPM, - instructions: [createDownloadServerRpm(), EDIT_CONFIG, START_SERVER_UNIX_SYSV], + instructions: [ + createDownloadServerRpm(), + EDIT_CONFIG, + START_SERVER_UNIX_SYSV, + ], }, { id: INSTRUCTION_VARIANT.WINDOWS, @@ -82,23 +96,38 @@ export function onPremInstructions({ }, ], statusCheck: { - title: i18n.translate('apmOss.tutorial.apmServer.statusCheck.title', { - defaultMessage: 'APM Server status', - }), - text: i18n.translate('apmOss.tutorial.apmServer.statusCheck.text', { - defaultMessage: - 'Make sure APM Server is running before you start implementing the APM agents.', - }), - btnLabel: i18n.translate('apmOss.tutorial.apmServer.statusCheck.btnLabel', { - defaultMessage: 'Check APM Server status', - }), - success: i18n.translate('apmOss.tutorial.apmServer.statusCheck.successMessage', { - defaultMessage: 'You have correctly setup APM Server', - }), - error: i18n.translate('apmOss.tutorial.apmServer.statusCheck.errorMessage', { - defaultMessage: - 'No APM Server detected. Please make sure it is running and you have updated to 7.0 or higher.', - }), + title: i18n.translate( + 'xpack.apm.tutorial.apmServer.statusCheck.title', + { + defaultMessage: 'APM Server status', + } + ), + text: i18n.translate( + 'xpack.apm.tutorial.apmServer.statusCheck.text', + { + defaultMessage: + 'Make sure APM Server is running before you start implementing the APM agents.', + } + ), + btnLabel: i18n.translate( + 'xpack.apm.tutorial.apmServer.statusCheck.btnLabel', + { + defaultMessage: 'Check APM Server status', + } + ), + success: i18n.translate( + 'xpack.apm.tutorial.apmServer.statusCheck.successMessage', + { + defaultMessage: 'You have correctly setup APM Server', + } + ), + error: i18n.translate( + 'xpack.apm.tutorial.apmServer.statusCheck.errorMessage', + { + defaultMessage: + 'No APM Server detected. Please make sure it is running and you have updated to 7.0 or higher.', + } + ), esHitsCheck: { index: onboardingIndices, query: { @@ -113,7 +142,7 @@ export function onPremInstructions({ }, }, { - title: i18n.translate('apmOss.tutorial.apmAgents.title', { + title: i18n.translate('xpack.apm.tutorial.apmAgents.title', { defaultMessage: 'APM Agents', }), instructionVariants: [ @@ -159,30 +188,56 @@ export function onPremInstructions({ }, ], statusCheck: { - title: i18n.translate('apmOss.tutorial.apmAgents.statusCheck.title', { - defaultMessage: 'Agent status', - }), - text: i18n.translate('apmOss.tutorial.apmAgents.statusCheck.text', { - defaultMessage: - 'Make sure your application is running and the agents are sending data.', - }), - btnLabel: i18n.translate('apmOss.tutorial.apmAgents.statusCheck.btnLabel', { - defaultMessage: 'Check agent status', - }), - success: i18n.translate('apmOss.tutorial.apmAgents.statusCheck.successMessage', { - defaultMessage: 'Data successfully received from one or more agents', - }), - error: i18n.translate('apmOss.tutorial.apmAgents.statusCheck.errorMessage', { - defaultMessage: 'No data has been received from agents yet', - }), + title: i18n.translate( + 'xpack.apm.tutorial.apmAgents.statusCheck.title', + { + defaultMessage: 'Agent status', + } + ), + text: i18n.translate( + 'xpack.apm.tutorial.apmAgents.statusCheck.text', + { + defaultMessage: + 'Make sure your application is running and the agents are sending data.', + } + ), + btnLabel: i18n.translate( + 'xpack.apm.tutorial.apmAgents.statusCheck.btnLabel', + { + defaultMessage: 'Check agent status', + } + ), + success: i18n.translate( + 'xpack.apm.tutorial.apmAgents.statusCheck.successMessage', + { + defaultMessage: + 'Data successfully received from one or more agents', + } + ), + error: i18n.translate( + 'xpack.apm.tutorial.apmAgents.statusCheck.errorMessage', + { + defaultMessage: 'No data has been received from agents yet', + } + ), esHitsCheck: { - index: [errorIndices, transactionIndices, metricsIndices, sourcemapIndices], + index: [ + errorIndices, + transactionIndices, + metricsIndices, + sourcemapIndices, + ], query: { bool: { filter: [ { terms: { - 'processor.event': ['error', 'transaction', 'metric', 'sourcemap'], + 'processor.event': [ + 'error', + 'transaction', + 'metric', + 'sourcemap', + ], }, }, { range: { 'observer.version_major': { gte: 7 } } }, diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts new file mode 100644 index 00000000000000..d678677a4b7514 --- /dev/null +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { onPremInstructions } from './envs/on_prem'; +import { createElasticCloudInstructions } from './envs/elastic_cloud'; +import apmIndexPattern from './index_pattern.json'; +import { CloudSetup } from '../../../cloud/server'; +import { + ArtifactsSchema, + TutorialsCategory, +} from '../../../../../src/plugins/home/server'; +import { APM_STATIC_INDEX_PATTERN_ID } from '../../common/index_pattern_constants'; + +const apmIntro = i18n.translate('xpack.apm.tutorial.introduction', { + defaultMessage: + 'Collect in-depth performance metrics and errors from inside your applications.', +}); +const moduleName = 'apm'; + +export const tutorialProvider = ({ + isEnabled, + indexPatternTitle, + indices, + cloud, +}: { + isEnabled: boolean; + indexPatternTitle: string; + cloud?: CloudSetup; + indices: { + errorIndices: string; + transactionIndices: string; + metricsIndices: string; + sourcemapIndices: string; + onboardingIndices: string; + }; +}) => () => { + const savedObjects = [ + { + ...apmIndexPattern, + id: APM_STATIC_INDEX_PATTERN_ID, + attributes: { + ...apmIndexPattern.attributes, + title: indexPatternTitle, + }, + }, + ]; + + const artifacts: ArtifactsSchema = { + dashboards: [ + { + id: '8d3ed660-7828-11e7-8c47-65b845b5cfb3', + linkLabel: i18n.translate( + 'xpack.apm.tutorial.specProvider.artifacts.dashboards.linkLabel', + { + defaultMessage: 'APM dashboard', + } + ), + isOverview: true, + }, + ], + }; + + if (isEnabled) { + // @ts-expect-error artifacts.application is readonly + artifacts.application = { + path: '/app/apm', + label: i18n.translate( + 'xpack.apm.tutorial.specProvider.artifacts.application.label', + { + defaultMessage: 'Launch APM', + } + ), + }; + } + + return { + id: 'apm', + name: i18n.translate('xpack.apm.tutorial.specProvider.name', { + defaultMessage: 'APM', + }), + moduleName, + category: TutorialsCategory.OTHER, + shortDescription: apmIntro, + longDescription: i18n.translate( + 'xpack.apm.tutorial.specProvider.longDescription', + { + defaultMessage: + 'Application Performance Monitoring (APM) collects in-depth \ +performance metrics and errors from inside your application. \ +It allows you to monitor the performance of thousands of applications in real time. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: + '{config.docs.base_url}guide/en/apm/get-started/{config.docs.version}/index.html', + }, + } + ), + euiIconType: 'apmApp', + artifacts, + onPrem: onPremInstructions(indices), + elasticCloud: createElasticCloudInstructions(cloud), + previewImagePath: '/plugins/apm/assets/apm.png', + savedObjects, + savedObjectsInstallMsg: i18n.translate( + 'xpack.apm.tutorial.specProvider.savedObjectsInstallMsg', + { + defaultMessage: + 'An APM index pattern is required for some features in the APM UI.', + } + ), + }; +}; diff --git a/src/plugins/apm_oss/server/tutorial/index_pattern.json b/x-pack/plugins/apm/server/tutorial/index_pattern.json similarity index 100% rename from src/plugins/apm_oss/server/tutorial/index_pattern.json rename to x-pack/plugins/apm/server/tutorial/index_pattern.json diff --git a/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts b/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts new file mode 100644 index 00000000000000..a25021fac5d006 --- /dev/null +++ b/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts @@ -0,0 +1,931 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 createNodeAgentInstructions = ( + apmServerUrl = '', + secretToken = '' +) => [ + { + title: i18n.translate('xpack.apm.tutorial.nodeClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('xpack.apm.tutorial.nodeClient.install.textPre', { + defaultMessage: + 'Install the APM agent for Node.js as a dependency to your application.', + }), + commands: ['npm install elastic-apm-node --save'], + }, + { + title: i18n.translate('xpack.apm.tutorial.nodeClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('xpack.apm.tutorial.nodeClient.configure.textPre', { + defaultMessage: + 'Agents are libraries that run inside of your application process. \ +APM services are created programmatically based on the `serviceName`. \ +This agent supports a variety of frameworks but can also be used with your custom stack.', + }), + commands: `// ${i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.commands.addThisToTheFileTopComment', + { + defaultMessage: + 'Add this to the VERY top of the first file loaded in your app', + } + )} +var apm = require('elastic-apm-node').start({curlyOpen} + + // ${i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.commands.setRequiredServiceNameComment', + { + defaultMessage: 'Override the service name from package.json', + } + )} + // ${i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.commands.allowedCharactersComment', + { + defaultMessage: 'Allowed characters: a-z, A-Z, 0-9, -, _, and space', + } + )} + serviceName: '', + + // ${i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.commands.useIfApmRequiresTokenComment', + { + defaultMessage: 'Use if APM Server requires a secret token', + } + )} + secretToken: '${secretToken}', + + // ${i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.commands.setCustomApmServerUrlComment', + { + defaultMessage: + 'Set the custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + } + )} + serverUrl: '${apmServerUrl}', + + // ${i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.commands.setCustomServiceEnvironmentComment', + { + defaultMessage: 'Set the service environment', + } + )} + environment: 'production' +{curlyClose})`.split('\n'), + textPost: i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.textPost', + { + defaultMessage: + 'See [the documentation]({documentationLink}) for advanced usage, including how to use with \ +[Babel/ES Modules]({babelEsModulesLink}).', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/agent/nodejs/current/index.html', + babelEsModulesLink: + '{config.docs.base_url}guide/en/apm/agent/nodejs/current/advanced-setup.html#es-modules', + }, + } + ), + }, +]; + +export const createDjangoAgentInstructions = ( + apmServerUrl = '', + secretToken = '' +) => [ + { + title: i18n.translate('xpack.apm.tutorial.djangoClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('xpack.apm.tutorial.djangoClient.install.textPre', { + defaultMessage: 'Install the APM agent for Python as a dependency.', + }), + commands: ['$ pip install elastic-apm'], + }, + { + title: i18n.translate('xpack.apm.tutorial.djangoClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.textPre', + { + defaultMessage: + 'Agents are libraries that run inside of your application process. \ +APM services are created programmatically based on the `SERVICE_NAME`.', + } + ), + commands: `# ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.addAgentComment', + { + defaultMessage: 'Add the agent to the installed apps', + } + )} +INSTALLED_APPS = ( + 'elasticapm.contrib.django', + # ... +) + +ELASTIC_APM = {curlyOpen} + # ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment', + { + defaultMessage: 'Set the required service name. Allowed characters:', + } + )} + # ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.allowedCharactersComment', + { + defaultMessage: 'a-z, A-Z, 0-9, -, _, and space', + } + )} + 'SERVICE_NAME': '', + + # ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment', + { + defaultMessage: 'Use if APM Server requires a secret token', + } + )} + 'SECRET_TOKEN': '${secretToken}', + + # ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment', + { + defaultMessage: + 'Set the custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + } + )} + 'SERVER_URL': '${apmServerUrl}', + + # ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.setServiceEnvironmentComment', + { + defaultMessage: 'Set the service environment', + } + )} + 'ENVIRONMENT': 'production', +{curlyClose} + +# ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment', + { + defaultMessage: + 'To send performance metrics, add our tracing middleware:', + } + )} +MIDDLEWARE = ( + 'elasticapm.contrib.django.middleware.TracingMiddleware', + #... +)`.split('\n'), + textPost: i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.textPost', + { + defaultMessage: + 'See the [documentation]({documentationLink}) for advanced usage.', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/agent/python/current/django-support.html', + }, + } + ), + }, +]; + +export const createFlaskAgentInstructions = ( + apmServerUrl = '', + secretToken = '' +) => [ + { + title: i18n.translate('xpack.apm.tutorial.flaskClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('xpack.apm.tutorial.flaskClient.install.textPre', { + defaultMessage: 'Install the APM agent for Python as a dependency.', + }), + commands: ['$ pip install elastic-apm[flask]'], + }, + { + title: i18n.translate('xpack.apm.tutorial.flaskClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.textPre', + { + defaultMessage: + 'Agents are libraries that run inside of your application process. \ +APM services are created programmatically based on the `SERVICE_NAME`.', + } + ), + commands: `# ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment', + { + defaultMessage: 'initialize using environment variables', + } + )} +from elasticapm.contrib.flask import ElasticAPM +app = Flask(__name__) +apm = ElasticAPM(app) + +# ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.configureElasticApmComment', + { + defaultMessage: + "or configure to use ELASTIC_APM in your application's settings", + } + )} +from elasticapm.contrib.flask import ElasticAPM +app.config['ELASTIC_APM'] = {curlyOpen} + # ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment', + { + defaultMessage: 'Set the required service name. Allowed characters:', + } + )} + # ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.allowedCharactersComment', + { + defaultMessage: 'a-z, A-Z, 0-9, -, _, and space', + } + )} + 'SERVICE_NAME': '', + + # ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment', + { + defaultMessage: 'Use if APM Server requires a secret token', + } + )} + 'SECRET_TOKEN': '${secretToken}', + + # ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment', + { + defaultMessage: + 'Set the custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + } + )} + 'SERVER_URL': '${apmServerUrl}', + + # ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.setServiceEnvironmentComment', + { + defaultMessage: 'Set the service environment', + } + )} + 'ENVIRONMENT': 'production', +{curlyClose} + +apm = ElasticAPM(app)`.split('\n'), + textPost: i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.textPost', + { + defaultMessage: + 'See the [documentation]({documentationLink}) for advanced usage.', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/agent/python/current/flask-support.html', + }, + } + ), + }, +]; + +export const createRailsAgentInstructions = ( + apmServerUrl = '', + secretToken = '' +) => [ + { + title: i18n.translate('xpack.apm.tutorial.railsClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('xpack.apm.tutorial.railsClient.install.textPre', { + defaultMessage: 'Add the agent to your Gemfile.', + }), + commands: [`gem 'elastic-apm'`], + }, + { + title: i18n.translate('xpack.apm.tutorial.railsClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate( + 'xpack.apm.tutorial.railsClient.configure.textPre', + { + defaultMessage: + 'APM is automatically started when your app boots. Configure the agent, by creating the config file {configFile}', + values: { configFile: '`config/elastic_apm.yml`' }, + } + ), + commands: `# config/elastic_apm.yml: + +# Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space +# Defaults to the name of your Rails app +service_name: 'my-service' + +# Use if APM Server requires a secret token +secret_token: '${secretToken}' + +# Set the custom APM Server URL (default: http://localhost:8200) +server_url: '${apmServerUrl || 'http://localhost:8200'}' + +# Set the service environment +environment: 'production'`.split('\n'), + textPost: i18n.translate( + 'xpack.apm.tutorial.railsClient.configure.textPost', + { + defaultMessage: + 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/agent/ruby/current/index.html', + }, + } + ), + }, +]; + +export const createRackAgentInstructions = ( + apmServerUrl = '', + secretToken = '' +) => [ + { + title: i18n.translate('xpack.apm.tutorial.rackClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('xpack.apm.tutorial.rackClient.install.textPre', { + defaultMessage: 'Add the agent to your Gemfile.', + }), + commands: [`gem 'elastic-apm'`], + }, + { + title: i18n.translate('xpack.apm.tutorial.rackClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('xpack.apm.tutorial.rackClient.configure.textPre', { + defaultMessage: + 'For Rack or a compatible framework (e.g. Sinatra), include the middleware in your app and start the agent.', + }), + commands: `# config.ru + require 'sinatra/base' + + class MySinatraApp < Sinatra::Base + use ElasticAPM::Middleware + + # ... + end + + ElasticAPM.start( + app: MySinatraApp, # ${i18n.translate( + 'xpack.apm.tutorial.rackClient.configure.commands.requiredComment', + { + defaultMessage: 'required', + } + )} + config_file: '' # ${i18n.translate( + 'xpack.apm.tutorial.rackClient.configure.commands.optionalComment', + { + defaultMessage: 'optional, defaults to config/elastic_apm.yml', + } + )} + ) + + run MySinatraApp + + at_exit {curlyOpen} ElasticAPM.stop {curlyClose}`.split('\n'), + }, + { + title: i18n.translate('xpack.apm.tutorial.rackClient.createConfig.title', { + defaultMessage: 'Create config file', + }), + textPre: i18n.translate( + 'xpack.apm.tutorial.rackClient.createConfig.textPre', + { + defaultMessage: 'Create a config file {configFile}:', + values: { configFile: '`config/elastic_apm.yml`' }, + } + ), + commands: `# config/elastic_apm.yml: + +# ${i18n.translate( + 'xpack.apm.tutorial.rackClient.createConfig.commands.setServiceNameComment', + { + defaultMessage: + 'Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space', + } + )} +# ${i18n.translate( + 'xpack.apm.tutorial.rackClient.createConfig.commands.defaultsToTheNameOfRackAppClassComment', + { + defaultMessage: "Defaults to the name of your Rack app's class.", + } + )} +service_name: 'my-service' + +# ${i18n.translate( + 'xpack.apm.tutorial.rackClient.createConfig.commands.useIfApmServerRequiresTokenComment', + { + defaultMessage: 'Use if APM Server requires a token', + } + )} +secret_token: '${secretToken}' + +# ${i18n.translate( + 'xpack.apm.tutorial.rackClient.createConfig.commands.setCustomApmServerComment', + { + defaultMessage: + 'Set custom APM Server URL (default: {defaultServerUrl})', + values: { defaultServerUrl: 'http://localhost:8200' }, + } + )} +server_url: '${apmServerUrl || 'http://localhost:8200'}', + +# ${i18n.translate( + 'xpack.apm.tutorial.rackClient.createConfig.commands.setServiceEnvironment', + { + defaultMessage: 'Set the service environment', + } + )} +environment: 'production'`.split('\n'), + textPost: i18n.translate( + 'xpack.apm.tutorial.rackClient.createConfig.textPost', + { + defaultMessage: + 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/agent/ruby/current/index.html', + }, + } + ), + }, +]; + +export const createJsAgentInstructions = (apmServerUrl = '') => [ + { + title: i18n.translate( + 'xpack.apm.tutorial.jsClient.enableRealUserMonitoring.title', + { + defaultMessage: 'Enable Real User Monitoring support in APM Server', + } + ), + textPre: i18n.translate( + 'xpack.apm.tutorial.jsClient.enableRealUserMonitoring.textPre', + { + defaultMessage: + 'APM Server disables RUM support by default. See the [documentation]({documentationLink}) \ +for details on how to enable RUM support.', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/server/{config.docs.version}/configuration-rum.html', + }, + } + ), + }, + { + title: i18n.translate( + 'xpack.apm.tutorial.jsClient.installDependency.title', + { + defaultMessage: 'Set up the Agent as a dependency', + } + ), + textPre: i18n.translate( + 'xpack.apm.tutorial.jsClient.installDependency.textPre', + { + defaultMessage: + 'You can install the Agent as a dependency to your application with \ +`npm install @elastic/apm-rum --save`.\n\n\ +The Agent can then be initialized and configured in your application like this:', + } + ), + commands: `import {curlyOpen} init as initApm {curlyClose} from '@elastic/apm-rum' +var apm = initApm({curlyOpen} + + // ${i18n.translate( + 'xpack.apm.tutorial.jsClient.installDependency.commands.setRequiredServiceNameComment', + { + defaultMessage: + 'Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)', + } + )} + serviceName: 'your-app-name', + + // ${i18n.translate( + 'xpack.apm.tutorial.jsClient.installDependency.commands.setCustomApmServerUrlComment', + { + defaultMessage: + 'Set custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + } + )} + serverUrl: '${apmServerUrl}', + + // ${i18n.translate( + 'xpack.apm.tutorial.jsClient.installDependency.commands.setServiceVersionComment', + { + defaultMessage: + 'Set the service version (required for source map feature)', + } + )} + serviceVersion: '', + + // ${i18n.translate( + 'xpack.apm.tutorial.jsClient.installDependency.commands.setServiceEnvironmentComment', + { + defaultMessage: 'Set the service environment', + } + )} + environment: 'production' +{curlyClose})`.split('\n'), + textPost: i18n.translate( + 'xpack.apm.tutorial.jsClient.installDependency.textPost', + { + defaultMessage: + 'Framework integrations, like React or Angular, have custom dependencies. \ +See the [integration documentation]({docLink}) for more information.', + values: { + docLink: + '{config.docs.base_url}guide/en/apm/agent/rum-js/current/framework-integrations.html', + }, + } + ), + }, + { + title: i18n.translate('xpack.apm.tutorial.jsClient.scriptTags.title', { + defaultMessage: 'Set up the Agent with Script Tags', + }), + textPre: i18n.translate('xpack.apm.tutorial.jsClient.scriptTags.textPre', { + defaultMessage: + "Alternatively, you can use Script tags to set up and configure the Agent. \ +Add a ` + +`.split('\n'), + }, +]; + +export const createGoAgentInstructions = ( + apmServerUrl = '', + secretToken = '' +) => [ + { + title: i18n.translate('xpack.apm.tutorial.goClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('xpack.apm.tutorial.goClient.install.textPre', { + defaultMessage: 'Install the APM agent packages for Go.', + }), + commands: ['go get go.elastic.co/apm'], + }, + { + title: i18n.translate('xpack.apm.tutorial.goClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('xpack.apm.tutorial.goClient.configure.textPre', { + defaultMessage: + 'Agents are libraries that run inside of your application process. \ +APM services are created programmatically based on the executable \ +file name, or the `ELASTIC_APM_SERVICE_NAME` environment variable.', + }), + commands: `# ${i18n.translate( + 'xpack.apm.tutorial.goClient.configure.commands.initializeUsingEnvironmentVariablesComment', + { + defaultMessage: 'Initialize using environment variables:', + } + )} + +# ${i18n.translate( + 'xpack.apm.tutorial.goClient.configure.commands.setServiceNameComment', + { + defaultMessage: + 'Set the service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space.', + } + )} +# ${i18n.translate( + 'xpack.apm.tutorial.goClient.configure.commands.usedExecutableNameComment', + { + defaultMessage: + 'If ELASTIC_APM_SERVICE_NAME is not specified, the executable name will be used.', + } + )} +export ELASTIC_APM_SERVICE_NAME= + +# ${i18n.translate( + 'xpack.apm.tutorial.goClient.configure.commands.setCustomApmServerUrlComment', + { + defaultMessage: + 'Set custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + } + )} +export ELASTIC_APM_SERVER_URL=${apmServerUrl} + +# ${i18n.translate( + 'xpack.apm.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment', + { + defaultMessage: 'Use if APM Server requires a secret token', + } + )} +export ELASTIC_APM_SECRET_TOKEN=${secretToken} + +# ${i18n.translate( + 'xpack.apm.tutorial.goClient.configure.commands.setServiceEnvironment', + { + defaultMessage: 'Set the service environment', + } + )} +export ELASTIC_APM_ENVIRONMENT= +`.split('\n'), + textPost: i18n.translate('xpack.apm.tutorial.goClient.configure.textPost', { + defaultMessage: + 'See the [documentation]({documentationLink}) for advanced configuration.', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/agent/go/current/configuration.html', + }, + }), + }, + { + title: i18n.translate('xpack.apm.tutorial.goClient.instrument.title', { + defaultMessage: 'Instrument your application', + }), + textPre: i18n.translate('xpack.apm.tutorial.goClient.instrument.textPre', { + defaultMessage: + 'Instrument your Go application by using one of the provided instrumentation modules or \ +by using the tracer API directly.', + }), + commands: `\ +import ( + "net/http" + + "go.elastic.co/apm/module/apmhttp" +) + +func main() {curlyOpen} + mux := http.NewServeMux() + ... + http.ListenAndServe(":8080", apmhttp.Wrap(mux)) +{curlyClose} +`.split('\n'), + textPost: i18n.translate( + 'xpack.apm.tutorial.goClient.instrument.textPost', + { + defaultMessage: + 'See the [documentation]({documentationLink}) for a detailed \ +guide to instrumenting Go source code.', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/agent/go/current/instrumenting-source.html', + }, + } + ), + }, +]; + +export const createJavaAgentInstructions = ( + apmServerUrl = '', + secretToken = '' +) => [ + { + title: i18n.translate('xpack.apm.tutorial.javaClient.download.title', { + defaultMessage: 'Download the APM agent', + }), + textPre: i18n.translate('xpack.apm.tutorial.javaClient.download.textPre', { + defaultMessage: + 'Download the agent jar from [Maven Central]({mavenCentralLink}). \ +Do **not** add the agent as a dependency to your application.', + values: { + mavenCentralLink: + 'http://search.maven.org/#search%7Cga%7C1%7Ca%3Aelastic-apm-agent', + }, + }), + }, + { + title: i18n.translate( + 'xpack.apm.tutorial.javaClient.startApplication.title', + { + defaultMessage: 'Start your application with the javaagent flag', + } + ), + textPre: i18n.translate( + 'xpack.apm.tutorial.javaClient.startApplication.textPre', + { + defaultMessage: + 'Add the `-javaagent` flag and configure the agent with system properties.\n\n \ +* Set the required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)\n \ +* Set the custom APM Server URL (default: {customApmServerUrl})\n \ +* Set the APM Server secret token\n \ +* Set the service environment\n \ +* Set the base package of your application', + values: { customApmServerUrl: 'http://localhost:8200' }, + } + ), + commands: `java -javaagent:/path/to/elastic-apm-agent-.jar \\ + -Delastic.apm.service_name=my-application \\ + -Delastic.apm.server_urls=${apmServerUrl || 'http://localhost:8200'} \\ + -Delastic.apm.secret_token=${secretToken} \\ + -Delastic.apm.environment=production \\ + -Delastic.apm.application_packages=org.example \\ + -jar my-application.jar`.split('\n'), + textPost: i18n.translate( + 'xpack.apm.tutorial.javaClient.startApplication.textPost', + { + defaultMessage: + 'See the [documentation]({documentationLink}) for configuration options and advanced \ +usage.', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/agent/java/current/index.html', + }, + } + ), + }, +]; + +export const createDotNetAgentInstructions = ( + apmServerUrl = '', + secretToken = '' +) => [ + { + title: i18n.translate('xpack.apm.tutorial.dotNetClient.download.title', { + defaultMessage: 'Download the APM agent', + }), + textPre: i18n.translate( + 'xpack.apm.tutorial.dotNetClient.download.textPre', + { + defaultMessage: + 'Add the the agent package(s) from [NuGet]({allNuGetPackagesLink}) to your .NET application. There are multiple \ + NuGet packages available for different use cases. \n\nFor an ASP.NET Core application with Entity Framework \ + Core download the [Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}) package. This package will automatically add every \ + agent component to your application. \n\n In case you would like to minimize the dependencies, you can use the \ + [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) package for just \ + ASP.NET Core monitoring or the [Elastic.Apm.EfCore]({efCorePackageLink}) package for just Entity Framework Core monitoring. \n\n \ + In case you only want to use the public Agent API for manual instrumentation use the [Elastic.Apm]({elasticApmPackageLink}) package.', + values: { + allNuGetPackagesLink: 'https://www.nuget.org/packages?q=Elastic.apm', + netCoreAllApmPackageLink: + 'https://www.nuget.org/packages/Elastic.Apm.NetCoreAll', + aspNetCorePackageLink: + 'https://www.nuget.org/packages/Elastic.Apm.AspNetCore', + efCorePackageLink: + 'https://www.nuget.org/packages/Elastic.Apm.EntityFrameworkCore', + elasticApmPackageLink: 'https://www.nuget.org/packages/Elastic.Apm', + }, + } + ), + }, + { + title: i18n.translate( + 'xpack.apm.tutorial.dotNetClient.configureApplication.title', + { + defaultMessage: 'Add the agent to the application', + } + ), + textPre: i18n.translate( + 'xpack.apm.tutorial.dotNetClient.configureApplication.textPre', + { + defaultMessage: + 'In case of ASP.NET Core with the `Elastic.Apm.NetCoreAll` package, call the `UseAllElasticApm` \ + method in the `Configure` method within the `Startup.cs` file.', + } + ), + commands: `public class Startup +{curlyOpen} + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + {curlyOpen} + app.UseAllElasticApm(Configuration); + //…rest of the method + {curlyClose} + //…rest of the class +{curlyClose}`.split('\n'), + textPost: i18n.translate( + 'xpack.apm.tutorial.dotNetClient.configureApplication.textPost', + { + defaultMessage: + 'Passing an `IConfiguration` instance is optional and by doing so, the agent will read config settings through this \ + `IConfiguration` instance (e.g. from the `appsettings.json` file).', + } + ), + }, + { + title: i18n.translate( + 'xpack.apm.tutorial.dotNetClient.configureAgent.title', + { + defaultMessage: 'Sample appsettings.json file:', + } + ), + commands: `{curlyOpen} + "ElasticApm": {curlyOpen} + "SecretToken": "${secretToken}", + "ServerUrls": "${ + apmServerUrl || 'http://localhost:8200' + }", //Set custom APM Server URL (default: http://localhost:8200) + "ServiceName": "MyApp", //allowed characters: a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application + "Environment": "production", // Set the service environment + {curlyClose} +{curlyClose}`.split('\n'), + textPost: i18n.translate( + 'xpack.apm.tutorial.dotNetClient.configureAgent.textPost', + { + defaultMessage: + 'In case you don’t pass an `IConfiguration` instance to the agent (e.g. in case of non ASP.NET Core applications) \ + you can also configure the agent through environment variables. \n \ + See [the documentation]({documentationLink}) for advanced usage.', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/agent/dotnet/current/configuration.html', + }, + } + ), + }, +]; + +export const createPhpAgentInstructions = ( + apmServerUrl = '', + secretToken = '' +) => [ + { + title: i18n.translate('xpack.apm.tutorial.phpClient.download.title', { + defaultMessage: 'Download the APM agent', + }), + textPre: i18n.translate('xpack.apm.tutorial.phpClient.download.textPre', { + defaultMessage: + 'Download the package corresponding to your platform from [GitHub releases]({githubReleasesLink}).', + values: { + githubReleasesLink: 'https://github.com/elastic/apm-agent-php/releases', + }, + }), + }, + { + title: i18n.translate('xpack.apm.tutorial.phpClient.installPackage.title', { + defaultMessage: 'Install the downloaded package', + }), + textPre: i18n.translate( + 'xpack.apm.tutorial.phpClient.installPackage.textPre', + { + defaultMessage: 'For example on Alpine Linux using APK package:', + } + ), + commands: ['apk add --allow-untrusted .apk'], + textPost: i18n.translate( + 'xpack.apm.tutorial.phpClient.installPackage.textPost', + { + defaultMessage: + 'See the [documentation]({documentationLink}) for installation commands on other supported platforms and advanced installation.', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/agent/php/current/setup.html', + }, + } + ), + }, + { + title: i18n.translate('xpack.apm.tutorial.phpClient.configureAgent.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate( + 'xpack.apm.tutorial.phpClient.configureAgent.textPre', + { + defaultMessage: + 'APM is automatically started when your app boots. Configure the agent either via `php.ini` file:', + } + ), + commands: `elastic_apm.server_url=http://localhost:8200 +elastic_apm.service_name="My service" +`.split('\n'), + textPost: i18n.translate( + 'xpack.apm.tutorial.phpClient.configure.textPost', + { + defaultMessage: + 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/agent/php/current/configuration.html', + }, + } + ), + }, +]; diff --git a/src/plugins/apm_oss/server/tutorial/instructions/apm_server_instructions.ts b/x-pack/plugins/apm/server/tutorial/instructions/apm_server_instructions.ts similarity index 66% rename from src/plugins/apm_oss/server/tutorial/instructions/apm_server_instructions.ts rename to x-pack/plugins/apm/server/tutorial/instructions/apm_server_instructions.ts index eee93d8dc9fd1a..c2fc7b1774f65f 100644 --- a/src/plugins/apm_oss/server/tutorial/instructions/apm_server_instructions.ts +++ b/x-pack/plugins/apm/server/tutorial/instructions/apm_server_instructions.ts @@ -1,18 +1,17 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { i18n } from '@kbn/i18n'; export const createEditConfig = () => ({ - title: i18n.translate('apmOss.tutorial.editConfig.title', { + title: i18n.translate('xpack.apm.tutorial.editConfig.title', { defaultMessage: 'Edit the configuration', }), - textPre: i18n.translate('apmOss.tutorial.editConfig.textPre', { + textPre: i18n.translate('xpack.apm.tutorial.editConfig.textPre', { defaultMessage: "If you're using an X-Pack secured version of Elastic Stack, you must specify \ credentials in the `apm-server.yml` config file.", @@ -26,10 +25,10 @@ credentials in the `apm-server.yml` config file.", }); const createStartServer = () => ({ - title: i18n.translate('apmOss.tutorial.startServer.title', { + title: i18n.translate('xpack.apm.tutorial.startServer.title', { defaultMessage: 'Start APM Server', }), - textPre: i18n.translate('apmOss.tutorial.startServer.textPre', { + textPre: i18n.translate('xpack.apm.tutorial.startServer.textPre', { defaultMessage: 'The server processes and stores application performance metrics in Elasticsearch.', }), @@ -56,7 +55,7 @@ export function createStartServerUnix() { } const createDownloadServerTitle = () => - i18n.translate('apmOss.tutorial.downloadServer.title', { + i18n.translate('xpack.apm.tutorial.downloadServer.title', { defaultMessage: 'Download and unpack APM Server', }); @@ -75,8 +74,9 @@ export const createDownloadServerDeb = () => ({ 'curl -L -O https://artifacts.elastic.co/downloads/apm-server/apm-server-{config.kibana.version}-amd64.deb', 'sudo dpkg -i apm-server-{config.kibana.version}-amd64.deb', ], - textPost: i18n.translate('apmOss.tutorial.downloadServerTitle', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({downloadPageLink}).', + textPost: i18n.translate('xpack.apm.tutorial.downloadServerTitle', { + defaultMessage: + 'Looking for the 32-bit packages? See the [Download page]({downloadPageLink}).', values: { downloadPageLink: '{config.docs.base_url}downloads/apm/apm-server', }, @@ -89,8 +89,9 @@ export const createDownloadServerRpm = () => ({ 'curl -L -O https://artifacts.elastic.co/downloads/apm-server/apm-server-{config.kibana.version}-x86_64.rpm', 'sudo rpm -vi apm-server-{config.kibana.version}-x86_64.rpm', ], - textPost: i18n.translate('apmOss.tutorial.downloadServerRpm', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({downloadPageLink}).', + textPost: i18n.translate('xpack.apm.tutorial.downloadServerRpm', { + defaultMessage: + 'Looking for the 32-bit packages? See the [Download page]({downloadPageLink}).', values: { downloadPageLink: '{config.docs.base_url}downloads/apm/apm-server', }, @@ -103,32 +104,41 @@ export function createWindowsServerInstructions() { return [ { title: createDownloadServerTitle(), - textPre: i18n.translate('apmOss.tutorial.windowsServerInstructions.textPre', { - defaultMessage: - '1. Download the APM Server Windows zip file from the \ + textPre: i18n.translate( + 'xpack.apm.tutorial.windowsServerInstructions.textPre', + { + defaultMessage: + '1. Download the APM Server Windows zip file from the \ [Download page]({downloadPageLink}).\n2. Extract the contents of \ the zip file into {zipFileExtractFolder}.\n3. Rename the {apmServerDirectory} \ directory to `APM-Server`.\n4. Open a PowerShell prompt as an Administrator \ (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install \ PowerShell.\n5. From the PowerShell prompt, run the following commands to install APM Server as a Windows service:', - values: { - downloadPageLink: 'https://www.elastic.co/downloads/apm/apm-server', - zipFileExtractFolder: '`C:\\Program Files`', - apmServerDirectory: '`apm-server-{config.kibana.version}-windows`', - }, - }), - commands: [`cd 'C:\\Program Files\\APM-Server'`, `.\\install-service-apm-server.ps1`], - textPost: i18n.translate('apmOss.tutorial.windowsServerInstructions.textPost', { - defaultMessage: - 'Note: If script execution is disabled on your system, \ + values: { + downloadPageLink: 'https://www.elastic.co/downloads/apm/apm-server', + zipFileExtractFolder: '`C:\\Program Files`', + apmServerDirectory: '`apm-server-{config.kibana.version}-windows`', + }, + } + ), + commands: [ + `cd 'C:\\Program Files\\APM-Server'`, + `.\\install-service-apm-server.ps1`, + ], + textPost: i18n.translate( + 'xpack.apm.tutorial.windowsServerInstructions.textPost', + { + defaultMessage: + 'Note: If script execution is disabled on your system, \ you need to set the execution policy for the current session \ to allow the script to run. For example: {command}.', - values: { - command: - '`PowerShell.exe -ExecutionPolicy UnRestricted -File .\\install-service-apm-server.ps1`', - }, - }), + values: { + command: + '`PowerShell.exe -ExecutionPolicy UnRestricted -File .\\install-service-apm-server.ps1`', + }, + } + ), }, createEditConfig(), { diff --git a/x-pack/plugins/canvas/public/functions/pie.test.js b/x-pack/plugins/canvas/public/functions/pie.test.js index 915d8525079dbf..b1c17463408928 100644 --- a/x-pack/plugins/canvas/public/functions/pie.test.js +++ b/x-pack/plugins/canvas/public/functions/pie.test.js @@ -18,7 +18,7 @@ describe('pie', () => { const fn = functionWrapper( pieFunctionFactory({ get: () => ({ - getColors: () => ['red', 'black'], + getCategoricalColors: () => ['red', 'black'], }), }) ); @@ -59,7 +59,7 @@ describe('pie', () => { const mockedFn = functionWrapper( pieFunctionFactory({ get: () => ({ - getColors: mockedColors, + getCategoricalColors: mockedColors, }), }) ); diff --git a/x-pack/plugins/canvas/public/functions/pie.ts b/x-pack/plugins/canvas/public/functions/pie.ts index 0840667302ebef..a91dc16b770c9f 100644 --- a/x-pack/plugins/canvas/public/functions/pie.ts +++ b/x-pack/plugins/canvas/public/functions/pie.ts @@ -173,7 +173,7 @@ export function pieFunctionFactory( canvas: false, colors: paletteService .get(palette.name || 'custom') - .getColors(data.length, palette.params), + .getCategoricalColors(data.length, palette.params), legend: getLegendConfig(legend, data.length), grid: { show: false, diff --git a/x-pack/plugins/canvas/public/functions/plot.test.js b/x-pack/plugins/canvas/public/functions/plot.test.js index 849752d2c984ba..5ed858961d7980 100644 --- a/x-pack/plugins/canvas/public/functions/plot.test.js +++ b/x-pack/plugins/canvas/public/functions/plot.test.js @@ -21,7 +21,7 @@ describe('plot', () => { const fn = functionWrapper( plotFunctionFactory({ get: () => ({ - getColors: () => ['red', 'black'], + getCategoricalColors: () => ['red', 'black'], }), }) ); @@ -121,7 +121,7 @@ describe('plot', () => { const mockedFn = functionWrapper( plotFunctionFactory({ get: () => ({ - getColors: mockedColors, + getCategoricalColors: mockedColors, }), }) ); diff --git a/x-pack/plugins/canvas/public/functions/plot/index.ts b/x-pack/plugins/canvas/public/functions/plot/index.ts index c0c73c3a21bc6c..477c7041901461 100644 --- a/x-pack/plugins/canvas/public/functions/plot/index.ts +++ b/x-pack/plugins/canvas/public/functions/plot/index.ts @@ -144,7 +144,7 @@ export function plotFunctionFactory( canvas: false, colors: paletteService .get(args.palette.name || 'custom') - .getColors(data.length, args.palette.params), + .getCategoricalColors(data.length, args.palette.params), legend: getLegendConfig(args.legend, data.length), grid: gridConfig, xaxis: getFlotAxisConfig('x', args.xaxis, { diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index de13077cd1b099..66f2bf78e0c9c3 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -12,9 +12,16 @@ import fetch from 'node-fetch'; const { Response } = jest.requireActual('node-fetch'); +jest.mock('@kbn/utils', () => ({ + kibanaPackageJson: { version: '1.0.0' }, +})); + import { loggingSystemMock } from 'src/core/server/mocks'; -import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; +import { + callEnterpriseSearchConfigAPI, + warnMismatchedVersions, +} from './enterprise_search_config_api'; describe('callEnterpriseSearchConfigAPI', () => { const mockConfig = { @@ -218,4 +225,22 @@ describe('callEnterpriseSearchConfigAPI', () => { "Exceeded 200ms timeout while checking http://localhost:3002. Please consider increasing your enterpriseSearch.accessCheckTimeout value so that users aren't prevented from accessing Enterprise Search plugins due to slow responses." ); }); + + describe('warnMismatchedVersions', () => { + it("logs a warning when Enterprise Search and Kibana's versions are not the same", () => { + warnMismatchedVersions('1.1.0', mockDependencies.log); + + expect(mockDependencies.log.warn).toHaveBeenCalledWith( + expect.stringContaining( + 'Your Kibana instance (v1.0.0) is not the same version as your Enterprise Search instance (v1.1.0)' + ) + ); + }); + + it("does not log a warning when Enterprise Search and Kibana's versions are the same", () => { + warnMismatchedVersions('1.0.0', mockDependencies.log); + + expect(mockDependencies.log.warn).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index ebe718dfebd309..0f2faf1fd8a3ab 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -8,6 +8,8 @@ import AbortController from 'abort-controller'; import fetch from 'node-fetch'; +import { kibanaPackageJson } from '@kbn/utils'; + import { KibanaRequest, Logger } from 'src/core/server'; import { stripTrailingSlash } from '../../common/strip_slashes'; @@ -58,6 +60,8 @@ export const callEnterpriseSearchConfigAPI = async ({ }); const data = await response.json(); + warnMismatchedVersions(data?.version?.number, log); + return { access: { hasAppSearchAccess: !!data?.current_user?.access?.app_search, @@ -135,3 +139,13 @@ export const callEnterpriseSearchConfigAPI = async ({ clearTimeout(timeout); } }; + +export const warnMismatchedVersions = (enterpriseSearchVersion: string, log: Logger) => { + const kibanaVersion = kibanaPackageJson.version; + + if (enterpriseSearchVersion !== kibanaVersion) { + log.warn( + `Your Kibana instance (v${kibanaVersion}) is not the same version as your Enterprise Search instance (v${enterpriseSearchVersion}), which may cause unexpected behavior. Use matching versions for the best experience.` + ); + } +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/hosts_input.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/hosts_input.test.tsx new file mode 100644 index 00000000000000..27bf5af72fb61d --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/hosts_input.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, act } from '@testing-library/react'; + +import { createTestRendererMock } from '../../mock'; + +import { HostsInput } from './hosts_input'; + +function renderInput(value = ['http://host1.com']) { + const renderer = createTestRendererMock(); + const mockOnChange = jest.fn(); + + const utils = renderer.render( + + ); + + return { utils, mockOnChange }; +} + +test('it should allow to add a new host', async () => { + const { utils, mockOnChange } = renderInput(); + + const addRowEl = await utils.findByText('Add row'); + fireEvent.click(addRowEl); + expect(mockOnChange).toHaveBeenCalledWith(['http://host1.com', '']); +}); + +test('it should allow to remove an host', async () => { + const { utils, mockOnChange } = renderInput(['http://host1.com', 'http://host2.com']); + + await act(async () => { + const deleteRowEl = await utils.container.querySelector('[aria-label="Delete host"]'); + if (!deleteRowEl) { + throw new Error('Delete host button not found'); + } + fireEvent.click(deleteRowEl); + }); + + expect(mockOnChange).toHaveBeenCalledWith(['http://host2.com']); +}); + +test('it should allow to update existing host with single host', async () => { + const { utils, mockOnChange } = renderInput(['http://host1.com']); + + const inputEl = await utils.findByDisplayValue('http://host1.com'); + fireEvent.change(inputEl, { target: { value: 'http://newhost.com' } }); + expect(mockOnChange).toHaveBeenCalledWith(['http://newhost.com']); +}); + +test('it should allow to update existing host with multiple hosts', async () => { + const { utils, mockOnChange } = renderInput(['http://host1.com', 'http://host2.com']); + + const inputEl = await utils.findByDisplayValue('http://host1.com'); + fireEvent.change(inputEl, { target: { value: 'http://newhost.com' } }); + expect(mockOnChange).toHaveBeenCalledWith(['http://newhost.com', 'http://host2.com']); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/hosts_input.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/hosts_input.tsx new file mode 100644 index 00000000000000..0e5f9a5e028b5e --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/hosts_input.tsx @@ -0,0 +1,247 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, useState } from 'react'; +import type { ReactNode, FunctionComponent, ChangeEvent } from 'react'; +import sytled, { useTheme } from 'styled-components'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiFormRow, + EuiFieldText, + EuiDragDropContext, + EuiDroppable, + EuiDraggable, + EuiIcon, + EuiButtonIcon, + EuiSpacer, + EuiFormHelpText, + euiDragDropReorder, + EuiFormErrorText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import type { EuiTheme } from '../../../../../../../../src/plugins/kibana_react/common'; + +interface Props { + id: string; + value: string[]; + onChange: (newValue: string[]) => void; + label: string; + helpText: ReactNode; + errors?: Array<{ message: string; index?: number }>; + isInvalid?: boolean; +} + +interface SortableTextFieldProps { + id: string; + index: number; + value: string; + onChange: (e: ChangeEvent) => void; + onDelete: (index: number) => void; + errors?: string[]; + autoFocus?: boolean; +} + +const DraggableDiv = sytled.div` + margin: ${(props) => props.theme.eui.euiSizeS}; +`; + +function displayErrors(errors?: string[]) { + return errors?.length + ? errors.map((error, errorIndex) => ( + {error} + )) + : null; +} + +const SortableTextField: FunctionComponent = React.memo( + ({ id, index, value, onChange, onDelete, autoFocus, errors }) => { + const onDeleteHandler = useCallback(() => { + onDelete(index); + }, [onDelete, index]); + + const isInvalid = (errors?.length ?? 0) > 0; + const theme = useTheme() as EuiTheme; + + return ( + + {(provided, state) => ( + + + + + + + + + {displayErrors(errors)} + + + + + + )} + + ); + } +); + +export const HostsInput: FunctionComponent = ({ + id, + value, + onChange, + helpText, + label, + isInvalid, + errors, +}) => { + const [autoFocus, setAutoFocus] = useState(false); + const rows = useMemo( + () => + value.map((host, idx) => ({ + value: host, + onChange: (e: ChangeEvent) => { + const newValue = [...value]; + newValue[idx] = e.target.value; + + onChange(newValue); + }, + })), + [value, onChange] + ); + + const onDelete = useCallback( + (idx: number) => { + onChange([...value.slice(0, idx), ...value.slice(idx + 1)]); + }, + [value, onChange] + ); + + const addRowHandler = useCallback(() => { + setAutoFocus(true); + onChange([...value, '']); + }, [value, onChange]); + + const onDragEndHandler = useCallback( + ({ source, destination }) => { + if (source && destination) { + const items = euiDragDropReorder(value, source.index, destination.index); + + onChange(items); + } + }, + [value, onChange] + ); + + const globalErrors = useMemo(() => { + return errors && errors.filter((err) => err.index === undefined).map(({ message }) => message); + }, [errors]); + + const indexedErrors = useMemo(() => { + if (!errors) { + return []; + } + return errors.reduce((acc, err) => { + if (err.index === undefined) { + return acc; + } + + if (!acc[err.index]) { + acc[err.index] = []; + } + + acc[err.index].push(err.message); + + return acc; + }, [] as string[][]); + }, [errors]); + + const isSortable = rows.length > 1; + return ( + + <> + {helpText} + + + + {rows.map((row, idx) => ( + + {isSortable ? ( + + ) : ( + <> + + {displayErrors(indexedErrors[idx])} + + )} + + ))} + + + {displayErrors(globalErrors)} + + + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx index b8028547910097..ea2e795d5fabbb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx @@ -20,10 +20,10 @@ import { EuiFlyoutFooter, EuiForm, EuiFormRow, - EuiComboBox, EuiCode, EuiCodeEditor, EuiLink, + EuiPanel, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText } from '@elastic/eui'; @@ -41,6 +41,7 @@ import { isDiffPathProtocol } from '../../../../../common/'; import { SettingsConfirmModal } from './confirm_modal'; import type { SettingsConfirmModalProps } from './confirm_modal'; +import { HostsInput } from './hosts_input'; import 'brace/mode/yaml'; import 'brace/theme/textmate'; @@ -59,37 +60,60 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { const [isLoading, setIsloading] = React.useState(false); const { notifications } = useStartServices(); - const fleetServerHostsInput = useComboInput([], (value) => { + const fleetServerHostsInput = useComboInput('fleetServerHostsComboBox', [], (value) => { if (value.length === 0) { return [ - i18n.translate('xpack.fleet.settings.fleetServerHostsEmptyError', { - defaultMessage: 'At least one URL is required', - }), + { + message: i18n.translate('xpack.fleet.settings.fleetServerHostsEmptyError', { + defaultMessage: 'At least one URL is required', + }), + }, ]; } - if (value.some((v) => !v.match(URL_REGEX))) { - return [ - i18n.translate('xpack.fleet.settings.fleetServerHostsError', { - defaultMessage: 'Invalid URL', - }), - ]; + + const res: Array<{ message: string; index: number }> = []; + value.forEach((val, idx) => { + if (!val.match(URL_REGEX)) { + res.push({ + message: i18n.translate('xpack.fleet.settings.fleetServerHostsError', { + defaultMessage: 'Invalid URL', + }), + index: idx, + }); + } + }); + if (res.length) { + return res; } + if (value.length && isDiffPathProtocol(value)) { return [ - i18n.translate('xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError', { - defaultMessage: 'Protocol and path must be the same for each URL', - }), + { + message: i18n.translate( + 'xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError', + { + defaultMessage: 'Protocol and path must be the same for each URL', + } + ), + }, ]; } }); - const elasticsearchUrlInput = useComboInput([], (value) => { - if (value.some((v) => !v.match(URL_REGEX))) { - return [ - i18n.translate('xpack.fleet.settings.elasticHostError', { - defaultMessage: 'Invalid URL', - }), - ]; + const elasticsearchUrlInput = useComboInput('esHostsComboxBox', [], (value) => { + const res: Array<{ message: string; index: number }> = []; + value.forEach((val, idx) => { + if (!val.match(URL_REGEX)) { + res.push({ + message: i18n.translate('xpack.fleet.settings.elasticHostError', { + defaultMessage: 'Invalid URL', + }), + index: idx, + }); + } + }); + if (res.length) { + return res; } }); @@ -264,91 +288,72 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { /> - - - - - ), - }} - /> - } - {...inputs.fleetServerHosts.formRowProps} - > - - - + + + + + ), + }} + /> + } + /> + - - - - ), + + + + + + + - } - {...inputs.elasticsearchUrl.formRowProps} - > - - - - - - + + ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_input.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_input.ts index 6314fbeb0c72eb..e4a517dbae9c8f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_input.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_input.ts @@ -5,11 +5,12 @@ * 2.0. */ -import React from 'react'; +import { useState, useCallback } from 'react'; +import type React from 'react'; export function useInput(defaultValue = '', validate?: (value: string) => string[] | undefined) { - const [value, setValue] = React.useState(defaultValue); - const [errors, setErrors] = React.useState(); + const [value, setValue] = useState(defaultValue); + const [errors, setErrors] = useState(); const onChange = (e: React.ChangeEvent) => { const newValue = e.target.value; @@ -50,31 +51,31 @@ export function useInput(defaultValue = '', validate?: (value: string) => string } export function useComboInput( + id: string, defaultValue = [], - validate?: (value: string[]) => string[] | undefined + validate?: (value: string[]) => Array<{ message: string; index?: number }> | undefined ) { - const [value, setValue] = React.useState(defaultValue); - const [errors, setErrors] = React.useState(); + const [value, setValue] = useState(defaultValue); + const [errors, setErrors] = useState | undefined>(); const isInvalid = errors !== undefined; + const onChange = useCallback( + (newValues: string[]) => { + setValue(newValues); + if (errors && validate && validate(newValues) === undefined) { + setErrors(undefined); + } + }, + [validate, errors] + ); + return { props: { - selectedOptions: value.map((val: string) => ({ label: val })), - onCreateOption: (newVal: any) => { - setValue([...value, newVal]); - }, - onChange: (newSelectedOptions: any[]) => { - const newValues = newSelectedOptions.map((option) => option.label); - setValue(newValues); - if (errors && validate && validate(newValues) === undefined) { - setErrors(undefined); - } - }, - isInvalid, - }, - formRowProps: { - error: errors, + id, + value, + onChange, + errors, isInvalid, }, value, diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index cbf311cac4e3bf..3735cfffeaa715 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -82,6 +82,7 @@ const PackagePolicyBaseSchema = { export const NewPackagePolicySchema = schema.object({ ...PackagePolicyBaseSchema, + id: schema.maybe(schema.string()), force: schema.maybe(schema.boolean()), }); diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json index 5dcff0ba942e12..cd29e7b9ee1cdc 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["home", "licensing", "management", "features", "share"], + "requiredPlugins": ["home", "management", "features", "share"], "optionalPlugins": ["security", "usageCollection", "fleet"], "configPath": ["xpack", "index_management"], "requiredBundles": [ diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index d2ef5a7eed6e7a..35d25eb452b84d 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -5,11 +5,9 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import { CoreSetup, Plugin, - Logger, PluginInitializerContext, ILegacyCustomClusterClient, } from 'src/core/server'; @@ -17,7 +15,7 @@ import { import { PLUGIN } from '../common/constants/plugin'; import { Dependencies } from './types'; import { ApiRoutes } from './routes'; -import { License, IndexDataEnricher } from './services'; +import { IndexDataEnricher } from './services'; import { isEsError, handleEsError, parseEsError } from './shared_imports'; import { elasticsearchJsPlugin } from './client/elasticsearch'; import type { IndexManagementRequestHandlerContext } from './types'; @@ -36,38 +34,20 @@ async function getCustomEsClient(getStartServices: CoreSetup['getStartServices'] export class IndexMgmtServerPlugin implements Plugin { private readonly apiRoutes: ApiRoutes; - private readonly license: License; - private readonly logger: Logger; private readonly indexDataEnricher: IndexDataEnricher; private dataManagementESClient?: ILegacyCustomClusterClient; constructor(initContext: PluginInitializerContext) { - this.logger = initContext.logger.get(); this.apiRoutes = new ApiRoutes(); - this.license = new License(); this.indexDataEnricher = new IndexDataEnricher(); } setup( { http, getStartServices }: CoreSetup, - { features, licensing, security }: Dependencies + { features, security }: Dependencies ): IndexManagementPluginSetup { const router = http.createRouter(); - this.license.setup( - { - pluginId: PLUGIN.id, - minimumLicenseType: PLUGIN.minimumLicenseType, - defaultErrorMessage: i18n.translate('xpack.idxMgmt.licenseCheckErrorMessage', { - defaultMessage: 'License check failed', - }), - }, - { - licensing, - logger: this.logger, - } - ); - features.registerElasticsearchFeature({ id: PLUGIN.id, management: { @@ -97,7 +77,6 @@ export class IndexMgmtServerPlugin implements Plugin security !== undefined && security.license.isEnabled(), }, diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts index a6c0592e035e79..4bf05b44c43a70 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts @@ -12,11 +12,7 @@ import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { componentTemplateSchema } from './schema_validation'; -export const registerCreateRoute = ({ - router, - license, - lib: { isEsError }, -}: RouteDependencies): void => { +export const registerCreateRoute = ({ router, lib: { isEsError } }: RouteDependencies): void => { router.post( { path: addBasePath('/component_templates'), @@ -24,7 +20,7 @@ export const registerCreateRoute = ({ body: componentTemplateSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; const serializedComponentTemplate = serializeComponentTemplate(req.body); @@ -73,6 +69,6 @@ export const registerCreateRoute = ({ throw error; } - }) + } ); }; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts index abf04da638cafc..d30f54f6e44adf 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts @@ -14,7 +14,7 @@ const paramsSchema = schema.object({ names: schema.string(), }); -export const registerDeleteRoute = ({ router, license }: RouteDependencies): void => { +export const registerDeleteRoute = ({ router }: RouteDependencies): void => { router.delete( { path: addBasePath('/component_templates/{names}'), @@ -22,7 +22,7 @@ export const registerDeleteRoute = ({ router, license }: RouteDependencies): voi params: paramsSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; const { names } = req.params; const componentNames = names.split(','); @@ -48,6 +48,6 @@ export const registerDeleteRoute = ({ router, license }: RouteDependencies): voi ); return res.ok({ body: response }); - }) + } ); }; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts index 552aa5a9a2888c..a5d70e65f870a9 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts @@ -19,11 +19,11 @@ const paramsSchema = schema.object({ name: schema.string(), }); -export function registerGetAllRoute({ router, license, lib: { isEsError } }: RouteDependencies) { +export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDependencies) { // Get all component templates router.get( { path: addBasePath('/component_templates'), validate: false }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; try { @@ -56,7 +56,7 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou throw error; } - }) + } ); // Get single component template @@ -67,7 +67,7 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou params: paramsSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; const { name } = req.params; @@ -96,6 +96,6 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou throw error; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts index 62fb228a34cedf..eccf2d945785f2 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts @@ -8,7 +8,6 @@ import { httpServerMock, httpServiceMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; -import { License } from '../../../services/license'; import { IndexDataEnricher } from '../../../services/index_data_enricher'; import { registerPrivilegesRoute } from './privileges'; @@ -47,9 +46,6 @@ describe('GET privileges', () => { registerPrivilegesRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, config: { isSecurityEnabled: () => true, }, @@ -118,9 +114,6 @@ describe('GET privileges', () => { registerPrivilegesRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, config: { isSecurityEnabled: () => false, }, diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts index 1ed6555eb38067..62ad93453091ec 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts @@ -17,13 +17,13 @@ const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } = return privileges; }, []); -export const registerPrivilegesRoute = ({ license, router, config }: RouteDependencies) => { +export const registerPrivilegesRoute = ({ router, config }: RouteDependencies) => { router.get( { path: addBasePath('/component_templates/privileges'), validate: false, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const privilegesResult: Privileges = { hasAllPrivileges: true, missingPrivileges: { @@ -66,6 +66,6 @@ export const registerPrivilegesRoute = ({ license, router, config }: RouteDepend } catch (e) { throw e; } - }) + } ); }; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts index 42b53ab6ee25b3..ee94b8f2b0082d 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts @@ -15,11 +15,7 @@ const paramsSchema = schema.object({ name: schema.string(), }); -export const registerUpdateRoute = ({ - router, - license, - lib: { isEsError }, -}: RouteDependencies): void => { +export const registerUpdateRoute = ({ router, lib: { isEsError } }: RouteDependencies): void => { router.put( { path: addBasePath('/component_templates/{name}'), @@ -28,7 +24,7 @@ export const registerUpdateRoute = ({ params: paramsSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; const { name } = req.params; const { template, version, _meta } = req.body; @@ -57,6 +53,6 @@ export const registerUpdateRoute = ({ throw error; } - }) + } ); }; diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts index 476228cfce2fcf..49166f4823a026 100644 --- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts @@ -15,13 +15,13 @@ const bodySchema = schema.object({ dataStreams: schema.arrayOf(schema.string()), }); -export function registerDeleteRoute({ router, license }: RouteDependencies) { +export function registerDeleteRoute({ router }: RouteDependencies) { router.post( { path: addBasePath('/delete_data_streams'), validate: { body: bodySchema }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; const { dataStreams } = req.body as TypeOf; @@ -48,6 +48,6 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) { ); return res.ok({ body: response }); - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts index 9573b9cc6436f2..1ce7c14f0a2093 100644 --- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts @@ -103,18 +103,13 @@ const getDataStreamsPrivileges = (client: ElasticsearchClient, names: string[]) }); }; -export function registerGetAllRoute({ - router, - license, - lib: { handleEsError }, - config, -}: RouteDependencies) { +export function registerGetAllRoute({ router, lib: { handleEsError }, config }: RouteDependencies) { const querySchema = schema.object({ includeStats: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), }); router.get( { path: addBasePath('/data_streams'), validate: { query: querySchema } }, - license.guardApiRoute(async (ctx, req, response) => { + async (ctx, req, response) => { const { asCurrentUser } = ctx.core.elasticsearch.client; const includeStats = (req.query as TypeOf).includeStats === 'true'; @@ -151,16 +146,11 @@ export function registerGetAllRoute({ } catch (error) { return handleEsError({ error, response }); } - }) + } ); } -export function registerGetOneRoute({ - router, - license, - lib: { handleEsError }, - config, -}: RouteDependencies) { +export function registerGetOneRoute({ router, lib: { handleEsError }, config }: RouteDependencies) { const paramsSchema = schema.object({ name: schema.string(), }); @@ -169,7 +159,7 @@ export function registerGetOneRoute({ path: addBasePath('/data_streams/{name}'), validate: { params: paramsSchema }, }, - license.guardApiRoute(async (ctx, req, response) => { + async (ctx, req, response) => { const { name } = req.params as TypeOf; const { asCurrentUser } = ctx.core.elasticsearch.client; try { @@ -207,6 +197,6 @@ export function registerGetOneRoute({ } catch (error) { return handleEsError({ error, response }); } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts index 2f5da4b1d8957a..593f0cda6886e3 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts @@ -14,10 +14,10 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerClearCacheRoute({ router, license, lib }: RouteDependencies) { +export function registerClearCacheRoute({ router, lib }: RouteDependencies) { router.post( { path: addBasePath('/indices/clear_cache'), validate: { body: bodySchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const payload = req.body as typeof bodySchema.type; const { indices = [] } = payload; @@ -40,6 +40,6 @@ export function registerClearCacheRoute({ router, license, lib }: RouteDependenc // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts index 1a0babfc3a5b1f..777adcd0557095 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts @@ -14,10 +14,10 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerCloseRoute({ router, license, lib }: RouteDependencies) { +export function registerCloseRoute({ router, lib }: RouteDependencies) { router.post( { path: addBasePath('/indices/close'), validate: { body: bodySchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const payload = req.body as typeof bodySchema.type; const { indices = [] } = payload; @@ -40,6 +40,6 @@ export function registerCloseRoute({ router, license, lib }: RouteDependencies) // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts index 9a022d4595d1c0..914835089a4381 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts @@ -14,10 +14,10 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerDeleteRoute({ router, license, lib }: RouteDependencies) { +export function registerDeleteRoute({ router, lib }: RouteDependencies) { router.post( { path: addBasePath('/indices/delete'), validate: { body: bodySchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const body = req.body as typeof bodySchema.type; const { indices = [] } = body; @@ -40,6 +40,6 @@ export function registerDeleteRoute({ router, license, lib }: RouteDependencies) // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts index b064f3520004a0..bb1759a034cc7e 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts @@ -14,10 +14,10 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerFlushRoute({ router, license, lib }: RouteDependencies) { +export function registerFlushRoute({ router, lib }: RouteDependencies) { router.post( { path: addBasePath('/indices/flush'), validate: { body: bodySchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const body = req.body as typeof bodySchema.type; const { indices = [] } = body; @@ -40,6 +40,6 @@ export function registerFlushRoute({ router, license, lib }: RouteDependencies) // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts index 1c14f660b98c67..6f0e8f0fec5673 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts @@ -15,7 +15,7 @@ const bodySchema = schema.object({ maxNumSegments: schema.maybe(schema.number()), }); -export function registerForcemergeRoute({ router, license, lib }: RouteDependencies) { +export function registerForcemergeRoute({ router, lib }: RouteDependencies) { router.post( { path: addBasePath('/indices/forcemerge'), @@ -23,7 +23,7 @@ export function registerForcemergeRoute({ router, license, lib }: RouteDependenc body: bodySchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { maxNumSegments, indices = [] } = req.body as typeof bodySchema.type; const params = { expandWildcards: 'none', @@ -47,6 +47,6 @@ export function registerForcemergeRoute({ router, license, lib }: RouteDependenc // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts index b669d78f2ba597..4b1281e0f21219 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts @@ -14,10 +14,10 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerFreezeRoute({ router, license, lib }: RouteDependencies) { +export function registerFreezeRoute({ router, lib }: RouteDependencies) { router.post( { path: addBasePath('/indices/freeze'), validate: { body: bodySchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const body = req.body as typeof bodySchema.type; const { indices = [] } = body; @@ -42,6 +42,6 @@ export function registerFreezeRoute({ router, license, lib }: RouteDependencies) // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts index 0b253b9fe66c97..47c454e96c8e2a 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts @@ -9,26 +9,23 @@ import { fetchIndices } from '../../../lib/fetch_indices'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; -export function registerListRoute({ router, license, indexDataEnricher, lib }: RouteDependencies) { - router.get( - { path: addBasePath('/indices'), validate: false }, - license.guardApiRoute(async (ctx, req, res) => { - try { - const indices = await fetchIndices( - ctx.core.elasticsearch.legacy.client.callAsCurrentUser, - indexDataEnricher - ); - return res.ok({ body: indices }); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; +export function registerListRoute({ router, indexDataEnricher, lib }: RouteDependencies) { + router.get({ path: addBasePath('/indices'), validate: false }, async (ctx, req, res) => { + try { + const indices = await fetchIndices( + ctx.core.elasticsearch.legacy.client.callAsCurrentUser, + indexDataEnricher + ); + return res.ok({ body: indices }); + } catch (e) { + if (lib.isEsError(e)) { + return res.customError({ + statusCode: e.statusCode, + body: e, + }); } - }) - ); + // Case: default + throw e; + } + }); } diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts index a35ddfcf4d91b2..cad57ce60de654 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts @@ -14,10 +14,10 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerOpenRoute({ router, license, lib }: RouteDependencies) { +export function registerOpenRoute({ router, lib }: RouteDependencies) { router.post( { path: addBasePath('/indices/open'), validate: { body: bodySchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const body = req.body as typeof bodySchema.type; const { indices = [] } = body; @@ -40,6 +40,6 @@ export function registerOpenRoute({ router, license, lib }: RouteDependencies) { // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts index f69d2d90a5b8fc..e2c0155e280865 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts @@ -14,10 +14,10 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerRefreshRoute({ router, license, lib }: RouteDependencies) { +export function registerRefreshRoute({ router, lib }: RouteDependencies) { router.post( { path: addBasePath('/indices/refresh'), validate: { body: bodySchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const body = req.body as typeof bodySchema.type; const { indices = [] } = body; @@ -40,6 +40,6 @@ export function registerRefreshRoute({ router, license, lib }: RouteDependencies // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts index 04b7d760fc1d62..8d83cd21f427dd 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts @@ -17,15 +17,10 @@ const bodySchema = schema.maybe( }) ); -export function registerReloadRoute({ - router, - license, - indexDataEnricher, - lib, -}: RouteDependencies) { +export function registerReloadRoute({ router, indexDataEnricher, lib }: RouteDependencies) { router.post( { path: addBasePath('/indices/reload'), validate: { body: bodySchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { indexNames = [] } = (req.body as typeof bodySchema.type) ?? {}; try { @@ -45,6 +40,6 @@ export function registerReloadRoute({ // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts index 3cda4d6b5f1682..45102f4874129a 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts @@ -14,10 +14,10 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerUnfreezeRoute({ router, license, lib }: RouteDependencies) { +export function registerUnfreezeRoute({ router, lib }: RouteDependencies) { router.post( { path: addBasePath('/indices/unfreeze'), validate: { body: bodySchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { indices = [] } = req.body as typeof bodySchema.type; const params = { path: `/${encodeURIComponent(indices.join(','))}/_unfreeze`, @@ -37,6 +37,6 @@ export function registerUnfreezeRoute({ router, license, lib }: RouteDependencie // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts b/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts index f0b62bacdee426..406ceba16c8bda 100644 --- a/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts @@ -21,10 +21,10 @@ function formatHit(hit: { [key: string]: { mappings: any } }, indexName: string) }; } -export function registerMappingRoute({ router, license, lib }: RouteDependencies) { +export function registerMappingRoute({ router, lib }: RouteDependencies) { router.get( { path: addBasePath('/mapping/{indexName}'), validate: { params: paramsSchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { indexName } = req.params as typeof paramsSchema.type; const params = { expand_wildcards: 'none', @@ -48,6 +48,6 @@ export function registerMappingRoute({ router, license, lib }: RouteDependencies // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts b/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts index 7a661a9e9e4f49..276b326929e8f9 100644 --- a/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts @@ -21,10 +21,10 @@ function formatHit(hit: { [key: string]: {} }) { return hit[key]; } -export function registerLoadRoute({ router, license, lib }: RouteDependencies) { +export function registerLoadRoute({ router, lib }: RouteDependencies) { router.get( { path: addBasePath('/settings/{indexName}'), validate: { params: paramsSchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { indexName } = req.params as typeof paramsSchema.type; const params = { expandWildcards: 'none', @@ -50,6 +50,6 @@ export function registerLoadRoute({ router, license, lib }: RouteDependencies) { // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts index 4c153d6293a79f..b4f12b91083df9 100644 --- a/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts @@ -16,13 +16,13 @@ const paramsSchema = schema.object({ indexName: schema.string(), }); -export function registerUpdateRoute({ router, license, lib }: RouteDependencies) { +export function registerUpdateRoute({ router, lib }: RouteDependencies) { router.put( { path: addBasePath('/settings/{indexName}'), validate: { body: bodySchema, params: paramsSchema }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { indexName } = req.params as typeof paramsSchema.type; const params = { ignoreUnavailable: true, @@ -48,6 +48,6 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts b/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts index f8385711b55fe2..42a3012ea8e174 100644 --- a/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts @@ -23,10 +23,10 @@ function formatHit(hit: { _shards: any; indices: { [key: string]: any } }, index }; } -export function registerStatsRoute({ router, license, lib }: RouteDependencies) { +export function registerStatsRoute({ router, lib }: RouteDependencies) { router.get( { path: addBasePath('/stats/{indexName}'), validate: { params: paramsSchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { indexName } = req.params as typeof paramsSchema.type; const params = { expand_wildcards: 'none', @@ -49,6 +49,6 @@ export function registerStatsRoute({ router, license, lib }: RouteDependencies) // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts index 97e3c380e13ecd..d8a236bdebd155 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts @@ -15,10 +15,10 @@ import { saveTemplate, doesTemplateExist } from './lib'; const bodySchema = templateSchema; -export function registerCreateRoute({ router, license, lib }: RouteDependencies) { +export function registerCreateRoute({ router, lib }: RouteDependencies) { router.post( { path: addBasePath('/index_templates'), validate: { body: bodySchema } }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; const template = req.body as TemplateDeserialized; const { @@ -64,6 +64,6 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts index e258fafa534e31..083964dec9edcc 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts @@ -22,13 +22,13 @@ const bodySchema = schema.object({ ), }); -export function registerDeleteRoute({ router, license }: RouteDependencies) { +export function registerDeleteRoute({ router }: RouteDependencies) { router.post( { path: addBasePath('/delete_index_templates'), validate: { body: bodySchema }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; const { templates } = req.body as TypeOf; const response: { templatesDeleted: Array; errors: any[] } = { @@ -60,6 +60,6 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) { ); return res.ok({ body: response }); - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index 006532cfd4dbe1..bd000186d91c40 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -17,32 +17,29 @@ import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_template import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; -export function registerGetAllRoute({ router, license }: RouteDependencies) { - router.get( - { path: addBasePath('/index_templates'), validate: false }, - license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.dataManagement!.client; - const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); - - const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate'); - const { index_templates: templatesEs } = await callAsCurrentUser( - 'dataManagement.getComposableIndexTemplates' - ); - - const legacyTemplates = deserializeLegacyTemplateList( - legacyTemplatesEs, - cloudManagedTemplatePrefix - ); - const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); - - const body = { - templates, - legacyTemplates, - }; - - return res.ok({ body }); - }) - ); +export function registerGetAllRoute({ router }: RouteDependencies) { + router.get({ path: addBasePath('/index_templates'), validate: false }, async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); + + const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate'); + const { index_templates: templatesEs } = await callAsCurrentUser( + 'dataManagement.getComposableIndexTemplates' + ); + + const legacyTemplates = deserializeLegacyTemplateList( + legacyTemplatesEs, + cloudManagedTemplatePrefix + ); + const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); + + const body = { + templates, + legacyTemplates, + }; + + return res.ok({ body }); + }); } const paramsSchema = schema.object({ @@ -54,13 +51,13 @@ const querySchema = schema.object({ legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), }); -export function registerGetOneRoute({ router, license, lib }: RouteDependencies) { +export function registerGetOneRoute({ router, lib }: RouteDependencies) { router.get( { path: addBasePath('/index_templates/{name}'), validate: { params: paramsSchema, query: querySchema }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { name } = req.params as TypeOf; const { callAsCurrentUser } = ctx.dataManagement!.client; @@ -106,6 +103,6 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts index f4554bd2fb1fa6..0c3d8faea628cf 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts @@ -12,13 +12,13 @@ import { addBasePath } from '../index'; const bodySchema = schema.object({}, { unknowns: 'allow' }); -export function registerSimulateRoute({ router, license, lib }: RouteDependencies) { +export function registerSimulateRoute({ router, lib }: RouteDependencies) { router.post( { path: addBasePath('/index_templates/simulate'), validate: { body: bodySchema }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; const template = req.body as TypeOf; @@ -42,6 +42,6 @@ export function registerSimulateRoute({ router, license, lib }: RouteDependencie // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts index f0070408768cbf..07a7d457f04737 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts @@ -18,13 +18,13 @@ const paramsSchema = schema.object({ name: schema.string(), }); -export function registerUpdateRoute({ router, license, lib }: RouteDependencies) { +export function registerUpdateRoute({ router, lib }: RouteDependencies) { router.put( { path: addBasePath('/index_templates/{name}'), validate: { body: bodySchema, params: paramsSchema }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; const { name } = req.params as typeof paramsSchema.type; const template = req.body as TemplateDeserialized; @@ -58,6 +58,6 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) // Case: default throw e; } - }) + } ); } diff --git a/x-pack/plugins/index_management/server/services/index.ts b/x-pack/plugins/index_management/server/services/index.ts index 3af5117e2b7aaa..576d7c46fa086a 100644 --- a/x-pack/plugins/index_management/server/services/index.ts +++ b/x-pack/plugins/index_management/server/services/index.ts @@ -5,6 +5,4 @@ * 2.0. */ -export { License } from './license'; - export { IndexDataEnricher, Enricher } from './index_data_enricher'; diff --git a/x-pack/plugins/index_management/server/services/license.ts b/x-pack/plugins/index_management/server/services/license.ts deleted file mode 100644 index 0898c01a1e4669..00000000000000 --- a/x-pack/plugins/index_management/server/services/license.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Logger } from 'src/core/server'; -import type { KibanaRequest, KibanaResponseFactory, RequestHandler } from 'kibana/server'; - -import { LicensingPluginSetup } from '../../../licensing/server'; -import { LicenseType } from '../../../licensing/common/types'; -import type { IndexManagementRequestHandlerContext } from '../types'; - -export interface LicenseStatus { - isValid: boolean; - message?: string; -} - -interface SetupSettings { - pluginId: string; - minimumLicenseType: LicenseType; - defaultErrorMessage: string; -} - -export class License { - private licenseStatus: LicenseStatus = { - isValid: false, - message: 'Invalid License', - }; - - setup( - { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, - { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } - ) { - licensing.license$.subscribe((license) => { - const { state, message } = license.check(pluginId, minimumLicenseType); - const hasRequiredLicense = state === 'valid'; - - if (hasRequiredLicense) { - this.licenseStatus = { isValid: true }; - } else { - this.licenseStatus = { - isValid: false, - message: message || defaultErrorMessage, - }; - if (message) { - logger.info(message); - } - } - }); - } - - guardApiRoute( - handler: RequestHandler - ) { - const license = this; - - return function licenseCheck( - ctx: Context, - request: KibanaRequest, - response: KibanaResponseFactory - ) { - const licenseStatus = license.getStatus(); - - if (!licenseStatus.isValid) { - return response.customError({ - body: { - message: licenseStatus.message || '', - }, - statusCode: 403, - }); - } - - return handler(ctx, request, response); - }; - } - - getStatus() { - return this.licenseStatus; - } -} diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts index 0854733d5db595..c980279d5bf306 100644 --- a/x-pack/plugins/index_management/server/types.ts +++ b/x-pack/plugins/index_management/server/types.ts @@ -14,7 +14,7 @@ import type { import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; -import { License, IndexDataEnricher } from './services'; +import { IndexDataEnricher } from './services'; import { isEsError, parseEsError, handleEsError } from './shared_imports'; export interface Dependencies { @@ -25,7 +25,6 @@ export interface Dependencies { export interface RouteDependencies { router: IndexManagementRouter; - license: License; config: { isSecurityEnabled: () => boolean; }; diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx index 901a4b6a8383e3..7f1636b00d24e3 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx @@ -113,7 +113,7 @@ export const Template = (args) => ; The purpose of this component is to allow you, the developer, to have your very own Log Stream in your plugin. -The component is exposed through `infra/public`. Since Kibana uses relative paths is up to you to find how to import it (sorry). +The component is exposed through `infra/public`. Since Kibana uses relative paths, it is up to you to find how to import it (sorry). ```tsx import { LogStream } from '../../../../../../infra/public'; @@ -124,8 +124,9 @@ import { LogStream } from '../../../../../../infra/public'; To use the component your plugin needs to follow certain criteria: -- Ensure `"infra"` is specified as a `requiredPlugins` in your plugin's `kibana.json`. -- Ensure the `` component is mounted inside the hiearchy of a [`kibana-react` provider](https://github.com/elastic/kibana/blob/b2d0aa7b7fae1c89c8f9e8854ae73e71be64e765/src/plugins/kibana_react/README.md#L45). +- Ensure `"infra"` and `"data"` are specified as a `requiredPlugins` in your plugin's `kibana.json`. +- Ensure the `` component is mounted inside the hierachy of a [`kibana-react` provider](https://github.com/elastic/kibana/blob/b2d0aa7b7fae1c89c8f9e8854ae73e71be64e765/src/plugins/kibana_react/README.md#L45). At a minimum, the kibana-react provider must pass `http` (from core start services) and `data` (from core plugin start dependencies). +- Ensure the `` component is mounted inside the hierachy of a [`EuiThemeProvider`](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_react/common/eui_styled_components.tsx). ## Usage @@ -354,25 +355,30 @@ The infra plugin has the concept of a "source configuration", a collection of se The `` component will use the `"default"` source configuration. If you want to use your own configuration, you need to first create it when you initialize your plugin, and then specify it in the `` component with the `sourceId` prop. ```tsx -// Your `plugin/init.ts` +// Your `server/plugin.ts` class MyPlugin { // ... setup(core, plugins) { plugins.infra.defineInternalSourceConfiguration( 'my_source', // ID for your source configuration { - logAlias: 'some-index-*', // Optional. what ES index to query. + name: 'some-name', + description: 'some description', + logIndices: { // Also accepts an `index_pattern` type with `indexPatternId` + type: 'index_name', + indexName: 'some-index', + }, logColumns: [ - { timestampColumn: { id: '...uuid4' }, // The `@timestamp` column. - { fieldColumn: { id: '...uuid4', field: 'some_field' }}, // Any column(s) you want. - { messageColumn: { id: '...uuid' }} // The `message` column. + { timestampColumn: { id: '...uuid4' }, // The `@timestamp` column. `id` is an arbitrary string identifier. + { fieldColumn: { id: '...uuid4', field: 'some_field' }}, // Any column(s) you want. `id` is an arbitrary string identifier. + { messageColumn: { id: '...uuid' }} // The `message` column. `id` is an arbitrary string identifier. ] } ); } } -// Somewhere else on your code +// Somewhere else in your client-side code { - private readonly logger: Logger; - private readonly license: License; private readonly apiRoutes: ApiRoutes; - constructor({ logger }: PluginInitializerContext) { - this.logger = logger.get(); - this.license = new License(); + constructor() { this.apiRoutes = new ApiRoutes(); } - public setup({ http }: CoreSetup, { licensing, security, features }: Dependencies) { - this.logger.debug('ingest_pipelines: setup'); - + public setup({ http }: CoreSetup, { security, features }: Dependencies) { const router = http.createRouter(); - this.license.setup( - { - pluginId: PLUGIN_ID, - minimumLicenseType: PLUGIN_MIN_LICENSE_TYPE, - defaultErrorMessage: i18n.translate('xpack.ingestPipelines.licenseCheckErrorMessage', { - defaultMessage: 'License check failed', - }), - }, - { - licensing, - logger: this.logger, - } - ); - features.registerElasticsearchFeature({ id: 'ingest_pipelines', management: { @@ -61,7 +36,6 @@ export class IngestPipelinesPlugin implements Plugin { this.apiRoutes.setup({ router, - license: this.license, config: { isSecurityEnabled: () => security !== undefined && security.license.isEnabled(), }, diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts index 388c82aa34b3d1..b078ca051a2724 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -20,7 +20,6 @@ const bodySchema = schema.object({ export const registerCreateRoute = ({ router, - license, lib: { handleEsError }, }: RouteDependencies): void => { router.post( @@ -30,7 +29,7 @@ export const registerCreateRoute = ({ body: bodySchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { client: clusterClient } = ctx.core.elasticsearch; const pipeline = req.body as Pipeline; @@ -74,6 +73,6 @@ export const registerCreateRoute = ({ } catch (error) { return handleEsError({ error, response: res }); } - }) + } ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts index 8cc7d7044ad08e..1ffa5adabd83b3 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts @@ -14,7 +14,7 @@ const paramsSchema = schema.object({ names: schema.string(), }); -export const registerDeleteRoute = ({ router, license }: RouteDependencies): void => { +export const registerDeleteRoute = ({ router }: RouteDependencies): void => { router.delete( { path: `${API_BASE_PATH}/{names}`, @@ -22,7 +22,7 @@ export const registerDeleteRoute = ({ router, license }: RouteDependencies): voi params: paramsSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { client: clusterClient } = ctx.core.elasticsearch; const { names } = req.params; const pipelineNames = names.split(','); @@ -48,6 +48,6 @@ export const registerDeleteRoute = ({ router, license }: RouteDependencies): voi ); return res.ok({ body: response }); - }) + } ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts index 324bcdd3edb462..6f7233c70dbfe8 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts @@ -17,7 +17,6 @@ const paramsSchema = schema.object({ export const registerDocumentsRoute = ({ router, - license, lib: { handleEsError }, }: RouteDependencies): void => { router.get( @@ -27,7 +26,7 @@ export const registerDocumentsRoute = ({ params: paramsSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { client: clusterClient } = ctx.core.elasticsearch; const { index, id } = req.params; @@ -46,6 +45,6 @@ export const registerDocumentsRoute = ({ } catch (error) { return handleEsError({ error, response: res }); } - }) + } ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts index 853bd1c7dde238..b512ebda5ecdb2 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts @@ -15,32 +15,25 @@ const paramsSchema = schema.object({ name: schema.string(), }); -export const registerGetRoutes = ({ - router, - license, - lib: { handleEsError }, -}: RouteDependencies): void => { +export const registerGetRoutes = ({ router, lib: { handleEsError } }: RouteDependencies): void => { // Get all pipelines - router.get( - { path: API_BASE_PATH, validate: false }, - license.guardApiRoute(async (ctx, req, res) => { - const { client: clusterClient } = ctx.core.elasticsearch; + router.get({ path: API_BASE_PATH, validate: false }, async (ctx, req, res) => { + const { client: clusterClient } = ctx.core.elasticsearch; - try { - const { body: pipelines } = await clusterClient.asCurrentUser.ingest.getPipeline(); + try { + const { body: pipelines } = await clusterClient.asCurrentUser.ingest.getPipeline(); - return res.ok({ body: deserializePipelines(pipelines) }); - } catch (error) { - const esErrorResponse = handleEsError({ error, response: res }); - if (esErrorResponse.status === 404) { - // ES returns 404 when there are no pipelines - // Instead, we return an empty array and 200 status back to the client - return res.ok({ body: [] }); - } - return esErrorResponse; + return res.ok({ body: deserializePipelines(pipelines) }); + } catch (error) { + const esErrorResponse = handleEsError({ error, response: res }); + if (esErrorResponse.status === 404) { + // ES returns 404 when there are no pipelines + // Instead, we return an empty array and 200 status back to the client + return res.ok({ body: [] }); } - }) - ); + return esErrorResponse; + } + }); // Get single pipeline router.get( @@ -50,7 +43,7 @@ export const registerGetRoutes = ({ params: paramsSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params; @@ -68,6 +61,6 @@ export const registerGetRoutes = ({ } catch (error) { return handleEsError({ error, response: res }); } - }) + } ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts index e1e4b2d3d28866..5368b59b35a41a 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts @@ -17,13 +17,13 @@ const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } = return privileges; }, []); -export const registerPrivilegesRoute = ({ license, router, config }: RouteDependencies) => { +export const registerPrivilegesRoute = ({ router, config }: RouteDependencies) => { router.get( { path: `${API_BASE_PATH}/privileges`, validate: false, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const privilegesResult: Privileges = { hasAllPrivileges: true, missingPrivileges: { @@ -51,6 +51,6 @@ export const registerPrivilegesRoute = ({ license, router, config }: RouteDepend privilegesResult.hasAllPrivileges = hasAllPrivileges; return res.ok({ body: privilegesResult }); - }) + } ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts index a1d0a4ec2e3d32..f697a38e21561e 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts @@ -19,7 +19,6 @@ const bodySchema = schema.object({ export const registerSimulateRoute = ({ router, - license, lib: { handleEsError }, }: RouteDependencies): void => { router.post( @@ -29,7 +28,7 @@ export const registerSimulateRoute = ({ body: bodySchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { client: clusterClient } = ctx.core.elasticsearch; const { pipeline, documents, verbose } = req.body; @@ -47,6 +46,6 @@ export const registerSimulateRoute = ({ } catch (error) { return handleEsError({ error, response: res }); } - }) + } ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts index 0d3e2a37795273..35af1395f5e37b 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -19,7 +19,6 @@ const paramsSchema = schema.object({ export const registerUpdateRoute = ({ router, - license, lib: { handleEsError }, }: RouteDependencies): void => { router.put( @@ -30,7 +29,7 @@ export const registerUpdateRoute = ({ params: paramsSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { + async (ctx, req, res) => { const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -54,6 +53,6 @@ export const registerUpdateRoute = ({ } catch (error) { return handleEsError({ error, response: res }); } - }) + } ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/services/license.ts b/x-pack/plugins/ingest_pipelines/server/services/license.ts deleted file mode 100644 index 05878b170ff1d0..00000000000000 --- a/x-pack/plugins/ingest_pipelines/server/services/license.ts +++ /dev/null @@ -1,84 +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 { Logger } from 'src/core/server'; -import { - KibanaRequest, - KibanaResponseFactory, - RequestHandler, - RequestHandlerContext, -} from 'kibana/server'; - -import { LicensingPluginSetup } from '../../../licensing/server'; -import { LicenseType } from '../../../licensing/common/types'; - -export interface LicenseStatus { - isValid: boolean; - message?: string; -} - -interface SetupSettings { - pluginId: string; - minimumLicenseType: LicenseType; - defaultErrorMessage: string; -} - -export class License { - private licenseStatus: LicenseStatus = { - isValid: false, - message: 'Invalid License', - }; - - setup( - { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, - { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } - ) { - licensing.license$.subscribe((license) => { - const { state, message } = license.check(pluginId, minimumLicenseType); - const hasRequiredLicense = state === 'valid'; - - if (hasRequiredLicense) { - this.licenseStatus = { isValid: true }; - } else { - this.licenseStatus = { - isValid: false, - message: message || defaultErrorMessage, - }; - if (message) { - logger.info(message); - } - } - }); - } - - guardApiRoute(handler: RequestHandler) { - const license = this; - - return function licenseCheck( - ctx: RequestHandlerContext, - request: KibanaRequest, - response: KibanaResponseFactory - ) { - const licenseStatus = license.getStatus(); - - if (!licenseStatus.isValid) { - return response.customError({ - body: { - message: licenseStatus.message || '', - }, - statusCode: 403, - }); - } - - return handler(ctx, request, response); - }; - } - - getStatus() { - return this.licenseStatus; - } -} diff --git a/x-pack/plugins/ingest_pipelines/server/types.ts b/x-pack/plugins/ingest_pipelines/server/types.ts index 912a0c88eef62a..efc0679708a5d9 100644 --- a/x-pack/plugins/ingest_pipelines/server/types.ts +++ b/x-pack/plugins/ingest_pipelines/server/types.ts @@ -6,21 +6,17 @@ */ import { IRouter } from 'src/core/server'; -import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { License } from './services'; import { handleEsError } from './shared_imports'; export interface Dependencies { security: SecurityPluginSetup; features: FeaturesPluginSetup; - licensing: LicensingPluginSetup; } export interface RouteDependencies { router: IRouter; - license: License; config: { isSecurityEnabled: () => boolean; }; diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 72b8bfa38491ac..30b4e2d954d2b8 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -6,15 +6,14 @@ */ import React from 'react'; -import { Observable, Subject } from 'rxjs'; +import { Subject } from 'rxjs'; import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { App } from './app'; import { LensAppProps, LensAppServices } from './types'; import { EditorFrameInstance, EditorFrameProps } from '../types'; import { Document } from '../persistence'; -import { DOC_TYPE } from '../../common'; -import { mount } from 'enzyme'; +import { makeDefaultServices, mountWithProvider } from '../mocks'; import { I18nProvider } from '@kbn/i18n/react'; import { SavedObjectSaveModal, @@ -22,31 +21,20 @@ import { } from '../../../../../src/plugins/saved_objects/public'; import { createMemoryHistory } from 'history'; import { - DataPublicPluginStart, esFilters, FilterManager, IFieldType, IIndexPattern, - UI_SETTINGS, + IndexPattern, + Query, } from '../../../../../src/plugins/data/public'; -import { navigationPluginMock } from '../../../../../src/plugins/navigation/public/mocks'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; -import { coreMock } from 'src/core/public/mocks'; -import { - LensByValueInput, - LensSavedObjectAttributes, - LensByReferenceInput, -} from '../editor_frame_service/embeddable/embeddable'; +import { LensByValueInput } from '../editor_frame_service/embeddable/embeddable'; import { SavedObjectReference } from '../../../../../src/core/types'; -import { - mockAttributeService, - createEmbeddableStateTransferMock, -} from '../../../../../src/plugins/embeddable/public/mocks'; -import { LensAttributeService } from '../lens_attribute_service'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { EmbeddableStateTransfer } from '../../../../../src/plugins/embeddable/public'; import moment from 'moment'; +import { setState, LensAppState } from '../state_management/index'; jest.mock('../editor_frame_service/editor_frame/expression_helpers'); jest.mock('src/core/public'); jest.mock('../../../../../src/plugins/saved_objects/public', () => { @@ -61,13 +49,16 @@ jest.mock('../../../../../src/plugins/saved_objects/public', () => { }; }); -const navigationStartMock = navigationPluginMock.createStartContract(); +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); -jest.spyOn(navigationStartMock.ui.TopNavMenu.prototype, 'constructor').mockImplementation(() => { - return
; + return { + ...original, + debounce: (fn: unknown) => fn, + }; }); -const { TopNavMenu } = navigationStartMock.ui; +// const navigationStartMock = navigationPluginMock.createStartContract(); function createMockFrame(): jest.Mocked { return { @@ -77,91 +68,7 @@ function createMockFrame(): jest.Mocked { const sessionIdSubject = new Subject(); -function createMockSearchService() { - let sessionIdCounter = 1; - return { - session: { - start: jest.fn(() => `sessionId-${sessionIdCounter++}`), - clear: jest.fn(), - getSessionId: jest.fn(() => `sessionId-${sessionIdCounter}`), - getSession$: jest.fn(() => sessionIdSubject.asObservable()), - }, - }; -} - -function createMockFilterManager() { - const unsubscribe = jest.fn(); - - let subscriber: () => void; - let filters: unknown = []; - - return { - getUpdates$: () => ({ - subscribe: ({ next }: { next: () => void }) => { - subscriber = next; - return unsubscribe; - }, - }), - setFilters: jest.fn((newFilters: unknown[]) => { - filters = newFilters; - if (subscriber) subscriber(); - }), - setAppFilters: jest.fn((newFilters: unknown[]) => { - filters = newFilters; - if (subscriber) subscriber(); - }), - getFilters: () => filters, - getGlobalFilters: () => { - // @ts-ignore - return filters.filter(esFilters.isFilterPinned); - }, - removeAll: () => { - filters = []; - subscriber(); - }, - }; -} - -function createMockQueryString() { - return { - getQuery: jest.fn(() => ({ query: '', language: 'kuery' })), - setQuery: jest.fn(), - getDefaultQuery: jest.fn(() => ({ query: '', language: 'kuery' })), - }; -} - -function createMockTimefilter() { - const unsubscribe = jest.fn(); - - let timeFilter = { from: 'now-7d', to: 'now' }; - let subscriber: () => void; - return { - getTime: jest.fn(() => timeFilter), - setTime: jest.fn((newTimeFilter) => { - timeFilter = newTimeFilter; - if (subscriber) { - subscriber(); - } - }), - getTimeUpdate$: () => ({ - subscribe: ({ next }: { next: () => void }) => { - subscriber = next; - return unsubscribe; - }, - }), - calculateBounds: jest.fn(() => ({ - min: moment('2021-01-10T04:00:00.000Z'), - max: moment('2021-01-10T08:00:00.000Z'), - })), - getBounds: jest.fn(() => timeFilter), - getRefreshInterval: () => {}, - getRefreshIntervalDefaults: () => {}, - getAutoRefreshFetch$: () => new Observable(), - }; -} - describe('Lens App', () => { - let core: ReturnType; let defaultDoc: Document; let defaultSavedObjectId: string; @@ -171,27 +78,6 @@ describe('Lens App', () => { expectedSaveAndReturnButton: { emphasize: true, testId: 'lnsApp_saveAndReturnButton' }, }; - function makeAttributeService(): LensAttributeService { - const attributeServiceMock = mockAttributeService< - LensSavedObjectAttributes, - LensByValueInput, - LensByReferenceInput - >( - DOC_TYPE, - { - saveMethod: jest.fn(), - unwrapMethod: jest.fn(), - checkForDuplicateTitle: jest.fn(), - }, - core - ); - attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); - attributeServiceMock.wrapAttributes = jest - .fn() - .mockResolvedValue({ savedObjectId: defaultSavedObjectId }); - return attributeServiceMock; - } - function makeDefaultProps(): jest.Mocked { return { editorFrame: createMockFrame(), @@ -203,64 +89,15 @@ describe('Lens App', () => { }; } - function makeDefaultServices(): jest.Mocked { - return { - http: core.http, - chrome: core.chrome, - overlays: core.overlays, - uiSettings: core.uiSettings, - navigation: navigationStartMock, - notifications: core.notifications, - attributeService: makeAttributeService(), - savedObjectsClient: core.savedObjects.client, - dashboardFeatureFlag: { allowByValueEmbeddables: false }, - stateTransfer: createEmbeddableStateTransferMock() as EmbeddableStateTransfer, - getOriginatingAppName: jest.fn(() => 'defaultOriginatingApp'), - application: { - ...core.application, - capabilities: { - ...core.application.capabilities, - visualize: { save: true, saveQuery: true, show: true }, - }, - getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), - }, - data: ({ - query: { - filterManager: createMockFilterManager(), - timefilter: { - timefilter: createMockTimefilter(), - }, - queryString: createMockQueryString(), - state$: new Observable(), - }, - indexPatterns: { - get: jest.fn((id) => { - return new Promise((resolve) => resolve({ id })); - }), - }, - search: createMockSearchService(), - nowProvider: { - get: jest.fn(), - }, - } as unknown) as DataPublicPluginStart, - storage: { - get: jest.fn(), - set: jest.fn(), - remove: jest.fn(), - clear: jest.fn(), - }, - }; - } - - function mountWith({ - props: incomingProps, - services: incomingServices, + async function mountWith({ + props = makeDefaultProps(), + services = makeDefaultServices(sessionIdSubject), + storePreloadedState, }: { props?: jest.Mocked; services?: jest.Mocked; + storePreloadedState?: Partial; }) { - const props = incomingProps ?? makeDefaultProps(); - const services = incomingServices ?? makeDefaultServices(); const wrappingComponent: React.FC<{ children: React.ReactNode; }> = ({ children }) => { @@ -270,61 +107,40 @@ describe('Lens App', () => { ); }; + + const { instance, lensStore } = await mountWithProvider( + , + services.data, + storePreloadedState, + wrappingComponent + ); + const frame = props.editorFrame as ReturnType; - const component = mount(, { wrappingComponent }); - return { component, frame, props, services }; + return { instance, frame, props, services, lensStore }; } beforeEach(() => { - core = coreMock.createStart({ basePath: '/testbasepath' }); defaultSavedObjectId = '1234'; defaultDoc = ({ savedObjectId: defaultSavedObjectId, title: 'An extremely cool default document!', expression: 'definitely a valid expression', state: { - query: 'kuery', + query: 'lucene', filters: [{ query: { match_phrase: { src: 'test' } } }], }, references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], } as unknown) as Document; - - core.uiSettings.get.mockImplementation( - jest.fn((type) => { - if (type === UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS) { - return { from: 'now-7d', to: 'now' }; - } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { - return 'kuery'; - } else if (type === 'state:storeInSessionStorage') { - return false; - } else { - return []; - } - }) - ); }); - it('renders the editor frame', () => { - const { frame } = mountWith({}); + it('renders the editor frame', async () => { + const { frame } = await mountWith({}); expect(frame.EditorFrameContainer.mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { - "dateRange": Object { - "fromDate": "2021-01-10T04:00:00.000Z", - "toDate": "2021-01-10T08:00:00.000Z", - }, - "doc": undefined, - "filters": Array [], "initialContext": undefined, - "onChange": [Function], "onError": [Function], - "query": Object { - "language": "kuery", - "query": "", - }, - "savedQuery": undefined, - "searchSessionId": "sessionId-1", "showNoDataPopover": [Function], }, Object {}, @@ -333,13 +149,8 @@ describe('Lens App', () => { `); }); - it('clears app filters on load', () => { - const { services } = mountWith({}); - expect(services.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]); - }); - - it('passes global filters to frame', async () => { - const services = makeDefaultServices(); + it('updates global filters with store state', async () => { + const services = makeDefaultServices(sessionIdSubject); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern); @@ -349,25 +160,28 @@ describe('Lens App', () => { services.data.query.filterManager.getGlobalFilters = jest.fn().mockImplementation(() => { return [pinnedFilter]; }); - const { component, frame } = mountWith({ services }); + const { instance, lensStore } = await mountWith({ services }); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, - query: { query: '', language: 'kuery' }, + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + query: { query: '', language: 'lucene' }, filters: [pinnedFilter], + resolvedDateRange: { + fromDate: '2021-01-10T04:00:00.000Z', + toDate: '2021-01-10T08:00:00.000Z', + }, }), - {} - ); + }); + expect(services.data.query.filterManager.getFilters).not.toHaveBeenCalled(); }); - it('displays errors from the frame in a toast', () => { - const { component, frame, services } = mountWith({}); + it('displays errors from the frame in a toast', async () => { + const { instance, frame, services } = await mountWith({}); const onError = frame.EditorFrameContainer.mock.calls[0][0].onError; onError({ message: 'error' }); - component.update(); + instance.update(); expect(services.notifications.toasts.addDanger).toHaveBeenCalled(); }); @@ -384,7 +198,7 @@ describe('Lens App', () => { } as unknown) as Document; it('sets breadcrumbs when the document title changes', async () => { - const { component, services } = mountWith({}); + const { instance, services, lensStore } = await mountWith({}); expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ { @@ -395,9 +209,13 @@ describe('Lens App', () => { { text: 'Create' }, ]); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(breadcrumbDoc); await act(async () => { - component.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); + instance.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); + lensStore.dispatch( + setState({ + persistedDoc: breadcrumbDoc, + }) + ); }); expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ @@ -412,10 +230,17 @@ describe('Lens App', () => { it('sets originatingApp breadcrumb when the document title changes', async () => { const props = makeDefaultProps(); - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); props.incomingState = { originatingApp: 'coolContainer' }; services.getOriginatingAppName = jest.fn(() => 'The Coolest Container Ever Made'); - const { component } = mountWith({ props, services }); + + const { instance, lensStore } = await mountWith({ + props, + services, + storePreloadedState: { + isLinkedToOriginatingApp: true, + }, + }); expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ { text: 'The Coolest Container Ever Made', onClick: expect.anything() }, @@ -427,9 +252,14 @@ describe('Lens App', () => { { text: 'Create' }, ]); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(breadcrumbDoc); await act(async () => { - component.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); + instance.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); + + lensStore.dispatch( + setState({ + persistedDoc: breadcrumbDoc, + }) + ); }); expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ @@ -445,99 +275,36 @@ describe('Lens App', () => { }); describe('persistence', () => { - it('does not load a document if there is no initial input', () => { - const { services } = mountWith({}); - expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); - }); - it('loads a document and uses query and filters if initial input is provided', async () => { - const { component, frame, services } = mountWith({}); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + const { instance, lensStore, services } = await mountWith({}); + const document = ({ savedObjectId: defaultSavedObjectId, state: { query: 'fake query', filters: [{ query: { match_phrase: { src: 'test' } } }], }, references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], - }); + } as unknown) as Document; - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + act(() => { + lensStore.dispatch( + setState({ + query: ('fake query' as unknown) as Query, + indexPatternsForTopNav: ([{ id: '1' }] as unknown) as IndexPattern[], + lastKnownDoc: document, + persistedDoc: document, + }) + ); }); + instance.update(); - expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ - savedObjectId: defaultSavedObjectId, - }); - expect(services.data.indexPatterns.get).toHaveBeenCalledWith('1'); - expect(services.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ - { query: { match_phrase: { src: 'test' } } }, - ]); - expect(TopNavMenu).toHaveBeenCalledWith( + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: 'fake query', indexPatterns: [{ id: '1' }], }), {} ); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - doc: expect.objectContaining({ - savedObjectId: defaultSavedObjectId, - state: expect.objectContaining({ - query: 'fake query', - filters: [{ query: { match_phrase: { src: 'test' } } }], - }), - }), - }), - {} - ); - }); - - it('does not load documents on sequential renders unless the id changes', async () => { - const { services, component } = mountWith({}); - - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); - }); - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); - }); - expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); - - await act(async () => { - component.setProps({ initialInput: { savedObjectId: '5678' } }); - }); - - expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(2); - }); - - it('handles document load errors', async () => { - const services = makeDefaultServices(); - services.attributeService.unwrapAttributes = jest.fn().mockRejectedValue('failed to load'); - const { component, props } = mountWith({ services }); - - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); - }); - - expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ - savedObjectId: defaultSavedObjectId, - }); - expect(services.notifications.toasts.addDanger).toHaveBeenCalled(); - expect(props.redirectTo).toHaveBeenCalled(); - }); - - it('adds to the recently accessed list on load', async () => { - const { component, services } = mountWith({}); - - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); - }); - expect(services.chrome.recentlyAccessed.add).toHaveBeenCalledWith( - '/app/lens#/edit/1234', - 'An extremely cool default document!', - '1234' - ); }); describe('save buttons', () => { @@ -584,7 +351,7 @@ describe('Lens App', () => { : undefined, }; - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); services.attributeService.wrapAttributes = jest .fn() .mockImplementation(async ({ savedObjectId }) => ({ @@ -599,39 +366,27 @@ describe('Lens App', () => { }, } as jest.ResolvedValue); - let frame: jest.Mocked = {} as jest.Mocked; - let component: ReactWrapper = {} as ReactWrapper; - await act(async () => { - const { frame: newFrame, component: newComponent } = mountWith({ services, props }); - frame = newFrame; - component = newComponent; - }); - - if (initialSavedObjectId) { - expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); - } else { - expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); - } + const { frame, instance, lensStore } = await mountWith({ services, props }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; + act(() => { + lensStore.dispatch( + setState({ + isSaveable: true, + lastKnownDoc: { savedObjectId: initialSavedObjectId, ...lastKnownDoc } as Document, + }) + ); + }); - act(() => - onChange({ - filterableIndexPatterns: [], - doc: { savedObjectId: initialSavedObjectId, ...lastKnownDoc } as Document, - isSaveable: true, - }) - ); - component.update(); - expect(getButton(component).disableButton).toEqual(false); + instance.update(); + expect(getButton(instance).disableButton).toEqual(false); await act(async () => { - testSave(component, { ...saveProps }); + testSave(instance, { ...saveProps }); }); - return { props, services, component, frame }; + return { props, services, instance, frame, lensStore }; } it('shows a disabled save button when the user does not have permissions', async () => { - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); services.application = { ...services.application, capabilities: { @@ -639,36 +394,36 @@ describe('Lens App', () => { visualize: { save: false, saveQuery: false, show: true }, }, }; - const { component, frame } = mountWith({ services }); - expect(getButton(component).disableButton).toEqual(true); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ savedObjectId: 'will save this' } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); - expect(getButton(component).disableButton).toEqual(true); + const { instance, lensStore } = await mountWith({ services }); + expect(getButton(instance).disableButton).toEqual(true); + act(() => { + lensStore.dispatch( + setState({ + lastKnownDoc: ({ savedObjectId: 'will save this' } as unknown) as Document, + isSaveable: true, + }) + ); + }); + instance.update(); + expect(getButton(instance).disableButton).toEqual(true); }); it('shows a save button that is enabled when the frame has provided its state and does not show save and return or save as', async () => { - const { component, frame } = mountWith({}); - expect(getButton(component).disableButton).toEqual(true); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ savedObjectId: 'will save this' } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); - expect(getButton(component).disableButton).toEqual(false); + const { instance, lensStore, services } = await mountWith({}); + expect(getButton(instance).disableButton).toEqual(true); + act(() => { + lensStore.dispatch( + setState({ + isSaveable: true, + lastKnownDoc: ({ savedObjectId: 'will save this' } as unknown) as Document, + }) + ); + }); + instance.update(); + expect(getButton(instance).disableButton).toEqual(false); await act(async () => { - const topNavMenuConfig = component.find(TopNavMenu).prop('config'); + const topNavMenuConfig = instance.find(services.navigation.ui.TopNavMenu).prop('config'); expect(topNavMenuConfig).not.toContainEqual( expect.objectContaining(navMenuItems.expectedSaveAndReturnButton) ); @@ -683,7 +438,7 @@ describe('Lens App', () => { it('Shows Save and Return and Save As buttons in create by value mode with originating app', async () => { const props = makeDefaultProps(); - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); services.dashboardFeatureFlag = { allowByValueEmbeddables: true }; props.incomingState = { originatingApp: 'ultraDashboard', @@ -697,10 +452,16 @@ describe('Lens App', () => { } as LensByValueInput, }; - const { component } = mountWith({ props, services }); + const { instance } = await mountWith({ + props, + services, + storePreloadedState: { + isLinkedToOriginatingApp: true, + }, + }); await act(async () => { - const topNavMenuConfig = component.find(TopNavMenu).prop('config'); + const topNavMenuConfig = instance.find(services.navigation.ui.TopNavMenu).prop('config'); expect(topNavMenuConfig).toContainEqual( expect.objectContaining(navMenuItems.expectedSaveAndReturnButton) ); @@ -720,10 +481,15 @@ describe('Lens App', () => { originatingApp: 'ultraDashboard', }; - const { component } = mountWith({ props }); + const { instance, services } = await mountWith({ + props, + storePreloadedState: { + isLinkedToOriginatingApp: true, + }, + }); await act(async () => { - const topNavMenuConfig = component.find(TopNavMenu).prop('config'); + const topNavMenuConfig = instance.find(services.navigation.ui.TopNavMenu).prop('config'); expect(topNavMenuConfig).toContainEqual( expect.objectContaining(navMenuItems.expectedSaveAndReturnButton) ); @@ -770,7 +536,7 @@ describe('Lens App', () => { }); it('saves the latest doc as a copy', async () => { - const { props, services, component } = await save({ + const { props, services, instance } = await save({ initialSavedObjectId: defaultSavedObjectId, newCopyOnSave: true, newTitle: 'hello there', @@ -784,7 +550,7 @@ describe('Lens App', () => { ); expect(props.redirectTo).toHaveBeenCalledWith(defaultSavedObjectId); await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + instance.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); expect(services.attributeService.wrapAttributes).toHaveBeenCalledTimes(1); expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith( @@ -793,7 +559,7 @@ describe('Lens App', () => { }); it('saves existing docs', async () => { - const { props, services, component } = await save({ + const { props, services, instance, lensStore } = await save({ initialSavedObjectId: defaultSavedObjectId, newCopyOnSave: false, newTitle: 'hello there', @@ -808,35 +574,51 @@ describe('Lens App', () => { ); expect(props.redirectTo).not.toHaveBeenCalled(); await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + instance.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); + + expect(lensStore.dispatch).toHaveBeenCalledWith({ + payload: { + lastKnownDoc: expect.objectContaining({ + savedObjectId: defaultSavedObjectId, + title: 'hello there', + }), + persistedDoc: expect.objectContaining({ + savedObjectId: defaultSavedObjectId, + title: 'hello there', + }), + isLinkedToOriginatingApp: false, + }, + type: 'app/setState', + }); + expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith( "Saved 'hello there'" ); }); it('handles save failure by showing a warning, but still allows another save', async () => { - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); services.attributeService.wrapAttributes = jest .fn() .mockRejectedValue({ message: 'failed' }); - const { component, props, frame } = mountWith({ services }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ id: undefined } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); + const { instance, props, lensStore } = await mountWith({ services }); + act(() => { + lensStore.dispatch( + setState({ + isSaveable: true, + lastKnownDoc: ({ id: undefined } as unknown) as Document, + }) + ); + }); + + instance.update(); await act(async () => { - testSave(component, { newCopyOnSave: false, newTitle: 'hello there' }); + testSave(instance, { newCopyOnSave: false, newTitle: 'hello there' }); }); expect(props.redirectTo).not.toHaveBeenCalled(); - expect(getButton(component).disableButton).toEqual(false); + expect(getButton(instance).disableButton).toEqual(false); }); it('saves new doc and redirects to originating app', async () => { @@ -895,28 +677,29 @@ describe('Lens App', () => { }); it('checks for duplicate title before saving', async () => { - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); services.attributeService.wrapAttributes = jest .fn() .mockReturnValue(Promise.resolve({ savedObjectId: '123' })); - const { component, frame } = mountWith({ services }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - await act(async () => - onChange({ - filterableIndexPatterns: [], - doc: ({ savedObjectId: '123' } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); + const { instance, lensStore } = await mountWith({ services }); await act(async () => { - component.setProps({ initialInput: { savedObjectId: '123' } }); - getButton(component).run(component.getDOMNode()); + lensStore.dispatch( + setState({ + isSaveable: true, + lastKnownDoc: ({ savedObjectId: '123' } as unknown) as Document, + }) + ); }); - component.update(); + + instance.update(); + await act(async () => { + instance.setProps({ initialInput: { savedObjectId: '123' } }); + getButton(instance).run(instance.getDOMNode()); + }); + instance.update(); const onTitleDuplicate = jest.fn(); await act(async () => { - component.find(SavedObjectSaveModal).prop('onSave')({ + instance.find(SavedObjectSaveModal).prop('onSave')({ onTitleDuplicate, isTitleDuplicateConfirmed: false, newCopyOnSave: false, @@ -933,19 +716,20 @@ describe('Lens App', () => { }); it('does not show the copy button on first save', async () => { - const { component, frame } = mountWith({}); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - await act(async () => - onChange({ - filterableIndexPatterns: [], - doc: ({} as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); - await act(async () => getButton(component).run(component.getDOMNode())); - component.update(); - expect(component.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false); + const { instance, lensStore } = await mountWith({}); + await act(async () => { + lensStore.dispatch( + setState({ + isSaveable: true, + lastKnownDoc: ({} as unknown) as Document, + }) + ); + }); + + instance.update(); + await act(async () => getButton(instance).run(instance.getDOMNode())); + instance.update(); + expect(instance.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false); }); }); }); @@ -960,38 +744,38 @@ describe('Lens App', () => { } it('should be disabled when no data is available', async () => { - const { component, frame } = mountWith({}); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - await act(async () => - onChange({ - filterableIndexPatterns: [], - doc: ({} as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); - expect(getButton(component).disableButton).toEqual(true); + const { instance, lensStore } = await mountWith({}); + await act(async () => { + lensStore.dispatch( + setState({ + isSaveable: true, + lastKnownDoc: ({} as unknown) as Document, + }) + ); + }); + instance.update(); + expect(getButton(instance).disableButton).toEqual(true); }); it('should disable download when not saveable', async () => { - const { component, frame } = mountWith({}); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - - await act(async () => - onChange({ - filterableIndexPatterns: [], - doc: ({} as unknown) as Document, - isSaveable: false, - activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, - }) - ); + const { instance, lensStore } = await mountWith({}); - component.update(); - expect(getButton(component).disableButton).toEqual(true); + await act(async () => { + lensStore.dispatch( + setState({ + lastKnownDoc: ({} as unknown) as Document, + isSaveable: false, + activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, + }) + ); + }); + + instance.update(); + expect(getButton(instance).disableButton).toEqual(true); }); it('should still be enabled even if the user is missing save permissions', async () => { - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); services.application = { ...services.application, capabilities: { @@ -1000,59 +784,63 @@ describe('Lens App', () => { }, }; - const { component, frame } = mountWith({ services }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - await act(async () => - onChange({ - filterableIndexPatterns: [], - doc: ({} as unknown) as Document, - isSaveable: true, - activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, - }) - ); - component.update(); - expect(getButton(component).disableButton).toEqual(false); + const { instance, lensStore } = await mountWith({ services }); + await act(async () => { + lensStore.dispatch( + setState({ + lastKnownDoc: ({} as unknown) as Document, + isSaveable: true, + activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, + }) + ); + }); + instance.update(); + expect(getButton(instance).disableButton).toEqual(false); }); }); describe('query bar state management', () => { - it('uses the default time and query language settings', () => { - const { frame } = mountWith({}); - expect(TopNavMenu).toHaveBeenCalledWith( + it('uses the default time and query language settings', async () => { + const { lensStore, services } = await mountWith({}); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ - query: { query: '', language: 'kuery' }, + query: { query: '', language: 'lucene' }, dateRangeFrom: 'now-7d', dateRangeTo: 'now', }), {} ); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, - query: { query: '', language: 'kuery' }, + + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + query: { query: '', language: 'lucene' }, + resolvedDateRange: { + fromDate: '2021-01-10T04:00:00.000Z', + toDate: '2021-01-10T08:00:00.000Z', + }, }), - {} - ); + }); }); it('updates the index patterns when the editor frame is changed', async () => { - const { component, frame } = mountWith({}); - expect(TopNavMenu).toHaveBeenCalledWith( + const { instance, lensStore, services } = await mountWith({}); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ indexPatterns: [], }), {} ); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => { - onChange({ - filterableIndexPatterns: ['1'], - doc: ({ id: undefined } as unknown) as Document, - isSaveable: true, - }); + lensStore.dispatch( + setState({ + indexPatternsForTopNav: [{ id: '1' }] as IndexPattern[], + lastKnownDoc: ({} as unknown) as Document, + isSaveable: true, + }) + ); }); - component.update(); - expect(TopNavMenu).toHaveBeenCalledWith( + instance.update(); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ indexPatterns: [{ id: '1' }], }), @@ -1060,14 +848,16 @@ describe('Lens App', () => { ); // Do it again to verify that the dirty checking is done right await act(async () => { - onChange({ - filterableIndexPatterns: ['2'], - doc: ({ id: undefined } as unknown) as Document, - isSaveable: true, - }); + lensStore.dispatch( + setState({ + indexPatternsForTopNav: [{ id: '2' }] as IndexPattern[], + lastKnownDoc: ({} as unknown) as Document, + isSaveable: true, + }) + ); }); - component.update(); - expect(TopNavMenu).toHaveBeenLastCalledWith( + instance.update(); + expect(services.navigation.ui.TopNavMenu).toHaveBeenLastCalledWith( expect.objectContaining({ indexPatterns: [{ id: '2' }], }), @@ -1075,20 +865,20 @@ describe('Lens App', () => { ); }); - it('updates the editor frame when the user changes query or time in the search bar', () => { - const { component, frame, services } = mountWith({}); + it('updates the editor frame when the user changes query or time in the search bar', async () => { + const { instance, services, lensStore } = await mountWith({}); (services.data.query.timefilter.timefilter.calculateBounds as jest.Mock).mockReturnValue({ min: moment('2021-01-09T04:00:00.000Z'), max: moment('2021-01-09T08:00:00.000Z'), }); act(() => - component.find(TopNavMenu).prop('onQuerySubmit')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) ); - component.update(); - expect(TopNavMenu).toHaveBeenCalledWith( + instance.update(); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: { query: 'new', language: 'lucene' }, dateRangeFrom: 'now-14d', @@ -1100,64 +890,75 @@ describe('Lens App', () => { from: 'now-14d', to: 'now-7d', }); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - dateRange: { fromDate: '2021-01-09T04:00:00.000Z', toDate: '2021-01-09T08:00:00.000Z' }, + + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ query: { query: 'new', language: 'lucene' }, + resolvedDateRange: { + fromDate: '2021-01-09T04:00:00.000Z', + toDate: '2021-01-09T08:00:00.000Z', + }, }), - {} - ); + }); }); - it('updates the filters when the user changes them', () => { - const { component, frame, services } = mountWith({}); + it('updates the filters when the user changes them', async () => { + const { instance, services, lensStore } = await mountWith({}); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + filters: [], + }), + }); act(() => services.data.query.filterManager.setFilters([ esFilters.buildExistsFilter(field, indexPattern), ]) ); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ filters: [esFilters.buildExistsFilter(field, indexPattern)], }), - {} - ); + }); }); - it('updates the searchSessionId when the user changes query or time in the search bar', () => { - const { component, frame, services } = mountWith({}); + it('updates the searchSessionId when the user changes query or time in the search bar', async () => { + const { instance, services, lensStore } = await mountWith({}); + + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `sessionId-1`, + }), + }); + act(() => - component.find(TopNavMenu).prop('onQuerySubmit')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: '', language: 'lucene' }, }) ); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-1`, - }), - {} - ); + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `sessionId-2`, + }), + }); // trigger again, this time changing just the query act(() => - component.find(TopNavMenu).prop('onQuerySubmit')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) ); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-2`, + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `sessionId-3`, }), - {} - ); - + }); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; act(() => @@ -1165,19 +966,18 @@ describe('Lens App', () => { esFilters.buildExistsFilter(field, indexPattern), ]) ); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-3`, + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `sessionId-4`, }), - {} - ); + }); }); }); describe('saved query handling', () => { - it('does not allow saving when the user is missing the saveQuery permission', () => { - const services = makeDefaultServices(); + it('does not allow saving when the user is missing the saveQuery permission', async () => { + const services = makeDefaultServices(sessionIdSubject); services.application = { ...services.application, capabilities: { @@ -1185,16 +985,16 @@ describe('Lens App', () => { visualize: { save: false, saveQuery: false, show: true }, }, }; - mountWith({ services }); - expect(TopNavMenu).toHaveBeenCalledWith( + await mountWith({ services }); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ showSaveQuery: false }), {} ); }); - it('persists the saved query ID when the query is saved', () => { - const { component } = mountWith({}); - expect(TopNavMenu).toHaveBeenCalledWith( + it('persists the saved query ID when the query is saved', async () => { + const { instance, services } = await mountWith({}); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ showSaveQuery: true, savedQuery: undefined, @@ -1205,7 +1005,7 @@ describe('Lens App', () => { {} ); act(() => { - component.find(TopNavMenu).prop('onSaved')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onSaved')!({ id: '1', attributes: { title: '', @@ -1214,7 +1014,7 @@ describe('Lens App', () => { }, }); }); - expect(TopNavMenu).toHaveBeenCalledWith( + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ savedQuery: { id: '1', @@ -1229,10 +1029,10 @@ describe('Lens App', () => { ); }); - it('changes the saved query ID when the query is updated', () => { - const { component } = mountWith({}); + it('changes the saved query ID when the query is updated', async () => { + const { instance, services } = await mountWith({}); act(() => { - component.find(TopNavMenu).prop('onSaved')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onSaved')!({ id: '1', attributes: { title: '', @@ -1242,7 +1042,7 @@ describe('Lens App', () => { }); }); act(() => { - component.find(TopNavMenu).prop('onSavedQueryUpdated')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onSavedQueryUpdated')!({ id: '2', attributes: { title: 'new title', @@ -1251,7 +1051,7 @@ describe('Lens App', () => { }, }); }); - expect(TopNavMenu).toHaveBeenCalledWith( + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ savedQuery: { id: '2', @@ -1266,10 +1066,10 @@ describe('Lens App', () => { ); }); - it('updates the query if saved query is selected', () => { - const { component } = mountWith({}); + it('updates the query if saved query is selected', async () => { + const { instance, services } = await mountWith({}); act(() => { - component.find(TopNavMenu).prop('onSavedQueryUpdated')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onSavedQueryUpdated')!({ id: '2', attributes: { title: 'new title', @@ -1278,7 +1078,7 @@ describe('Lens App', () => { }, }); }); - expect(TopNavMenu).toHaveBeenCalledWith( + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: { query: 'abc:def', language: 'lucene' }, }), @@ -1286,10 +1086,10 @@ describe('Lens App', () => { ); }); - it('clears all existing unpinned filters when the active saved query is cleared', () => { - const { component, frame, services } = mountWith({}); + it('clears all existing unpinned filters when the active saved query is cleared', async () => { + const { instance, services, lensStore } = await mountWith({}); act(() => - component.find(TopNavMenu).prop('onQuerySubmit')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) @@ -1301,23 +1101,22 @@ describe('Lens App', () => { const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); - component.update(); - act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenLastCalledWith( - expect.objectContaining({ + instance.update(); + act(() => instance.find(services.navigation.ui.TopNavMenu).prop('onClearSavedQuery')!()); + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ filters: [pinned], }), - {} - ); + }); }); }); describe('search session id management', () => { - it('updates the searchSessionId when the query is updated', () => { - const { component, frame } = mountWith({}); + it('updates the searchSessionId when the query is updated', async () => { + const { instance, lensStore, services } = await mountWith({}); act(() => { - component.find(TopNavMenu).prop('onSaved')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onSaved')!({ id: '1', attributes: { title: '', @@ -1327,7 +1126,7 @@ describe('Lens App', () => { }); }); act(() => { - component.find(TopNavMenu).prop('onSavedQueryUpdated')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onSavedQueryUpdated')!({ id: '2', attributes: { title: 'new title', @@ -1336,37 +1135,18 @@ describe('Lens App', () => { }, }); }); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ searchSessionId: `sessionId-2`, }), - {} - ); - }); - - it('re-renders the frame if session id changes from the outside', async () => { - const services = makeDefaultServices(); - const { frame } = mountWith({ props: undefined, services }); - - act(() => { - sessionIdSubject.next('new-session-id'); - }); - await act(async () => { - await new Promise((r) => setTimeout(r, 0)); }); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `new-session-id`, - }), - {} - ); }); - it('updates the searchSessionId when the active saved query is cleared', () => { - const { component, frame, services } = mountWith({}); + it('updates the searchSessionId when the active saved query is cleared', async () => { + const { instance, services, lensStore } = await mountWith({}); act(() => - component.find(TopNavMenu).prop('onQuerySubmit')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) @@ -1378,15 +1158,14 @@ describe('Lens App', () => { const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); - component.update(); - act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-2`, + instance.update(); + act(() => instance.find(services.navigation.ui.TopNavMenu).prop('onClearSavedQuery')!()); + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `sessionId-4`, }), - {} - ); + }); }); const mockUpdate = { @@ -1407,70 +1186,39 @@ describe('Lens App', () => { activeData: undefined, }; - it('does not update the searchSessionId when the state changes', () => { - const { component, frame } = mountWith({}); - act(() => { - component.find(frame.EditorFrameContainer).prop('onChange')(mockUpdate); - }); - component.update(); - expect(frame.EditorFrameContainer).not.toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-2`, - }), - {} - ); - }); - - it('does update the searchSessionId when the state changes and too much time passed', () => { - const { component, frame, services } = mountWith({}); - - // time range is 100,000ms ago to 30,000ms ago (that's a lag of 30 percent) - (services.data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000)); - (services.data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ - from: 'now-2m', - to: 'now', - }); - (services.data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({ - min: moment(Date.now() - 100000), - max: moment(Date.now() - 30000), - }); + it('updates the state if session id changes from the outside', async () => { + const services = makeDefaultServices(sessionIdSubject); + const { lensStore } = await mountWith({ props: undefined, services }); act(() => { - component.find(frame.EditorFrameContainer).prop('onChange')(mockUpdate); + sessionIdSubject.next('new-session-id'); }); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-2`, - }), - {} - ); - }); - - it('does not update the searchSessionId when the state changes and too little time has passed', () => { - const { component, frame, services } = mountWith({}); - - // time range is 100,000ms ago to 300ms ago (that's a lag of .3 percent, not enough to trigger a session update) - (services.data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 300)); - (services.data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ - from: 'now-2m', - to: 'now', + await act(async () => { + await new Promise((r) => setTimeout(r, 0)); }); - (services.data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({ - min: moment(Date.now() - 100000), - max: moment(Date.now() - 300), + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `new-session-id`, + }), }); + }); + it('does not update the searchSessionId when the state changes', async () => { + const { lensStore } = await mountWith({}); act(() => { - component.find(frame.EditorFrameContainer).prop('onChange')(mockUpdate); + lensStore.dispatch( + setState({ + indexPatternsForTopNav: [], + lastKnownDoc: mockUpdate.doc, + isSaveable: true, + }) + ); }); - component.update(); - expect(frame.EditorFrameContainer).not.toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-2`, + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `sessionId-1`, }), - {} - ); + }); }); }); @@ -1483,16 +1231,16 @@ describe('Lens App', () => { confirmLeave = jest.fn(); }); - it('should not show a confirm message if there is no expression to save', () => { - const { props } = mountWith({}); + it('should not show a confirm message if there is no expression to save', async () => { + const { props } = await mountWith({}); const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(defaultLeave).toHaveBeenCalled(); expect(confirmLeave).not.toHaveBeenCalled(); }); - it('does not confirm if the user is missing save permissions', () => { - const services = makeDefaultServices(); + it('does not confirm if the user is missing save permissions', async () => { + const services = makeDefaultServices(sessionIdSubject); services.application = { ...services.application, capabilities: { @@ -1500,36 +1248,36 @@ describe('Lens App', () => { visualize: { save: false, saveQuery: false, show: true }, }, }; - const { component, frame, props } = mountWith({ services }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ - savedObjectId: undefined, - references: [], - } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); + const { instance, props, lensStore } = await mountWith({ services }); + act(() => { + lensStore.dispatch( + setState({ + indexPatternsForTopNav: [] as IndexPattern[], + lastKnownDoc: ({ + savedObjectId: undefined, + references: [], + } as unknown) as Document, + isSaveable: true, + }) + ); + }); + instance.update(); const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(defaultLeave).toHaveBeenCalled(); expect(confirmLeave).not.toHaveBeenCalled(); }); - it('should confirm when leaving with an unsaved doc', () => { - const { component, frame, props } = mountWith({}); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ savedObjectId: undefined, state: {} } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); + it('should confirm when leaving with an unsaved doc', async () => { + const { lensStore, props } = await mountWith({}); + act(() => { + lensStore.dispatch( + setState({ + lastKnownDoc: ({ savedObjectId: undefined, state: {} } as unknown) as Document, + isSaveable: true, + }) + ); + }); const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(confirmLeave).toHaveBeenCalled(); @@ -1537,22 +1285,19 @@ describe('Lens App', () => { }); it('should confirm when leaving with unsaved changes to an existing doc', async () => { - const { component, frame, props } = mountWith({}); - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + const { lensStore, props } = await mountWith({}); + act(() => { + lensStore.dispatch( + setState({ + persistedDoc: defaultDoc, + lastKnownDoc: ({ + savedObjectId: defaultSavedObjectId, + references: [], + } as unknown) as Document, + isSaveable: true, + }) + ); }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ - savedObjectId: defaultSavedObjectId, - references: [], - } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(confirmLeave).toHaveBeenCalled(); @@ -1560,19 +1305,16 @@ describe('Lens App', () => { }); it('should not confirm when changes are saved', async () => { - const { component, frame, props } = mountWith({}); - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + const { lensStore, props } = await mountWith({}); + act(() => { + lensStore.dispatch( + setState({ + lastKnownDoc: defaultDoc, + persistedDoc: defaultDoc, + isSaveable: true, + }) + ); }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: defaultDoc, - isSaveable: true, - }) - ); - component.update(); const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(defaultLeave).toHaveBeenCalled(); @@ -1580,19 +1322,19 @@ describe('Lens App', () => { }); it('should confirm when the latest doc is invalid', async () => { - const { component, frame, props } = mountWith({}); - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + const { lensStore, props } = await mountWith({}); + act(() => { + lensStore.dispatch( + setState({ + persistedDoc: defaultDoc, + lastKnownDoc: ({ + savedObjectId: defaultSavedObjectId, + references: [], + } as unknown) as Document, + isSaveable: true, + }) + ); }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ savedObjectId: defaultSavedObjectId, references: [] } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(confirmLeave).toHaveBeenCalled(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index c172f36913c217..61ed2934a40011 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -7,49 +7,38 @@ import './app.scss'; -import _ from 'lodash'; -import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { isEqual, partition } from 'lodash'; +import React, { useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { Toast } from 'kibana/public'; import { VisualizeFieldContext } from 'src/plugins/ui_actions/public'; -import { Datatable } from 'src/plugins/expressions/public'; import { EuiBreadcrumb } from '@elastic/eui'; -import { delay, finalize, switchMap, tap } from 'rxjs/operators'; -import { downloadMultipleAs } from '../../../../../src/plugins/share/public'; import { createKbnUrlStateStorage, withNotifyOnErrors, } from '../../../../../src/plugins/kibana_utils/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import { - OnSaveProps, - checkForDuplicateTitle, -} from '../../../../../src/plugins/saved_objects/public'; +import { checkForDuplicateTitle } from '../../../../../src/plugins/saved_objects/public'; import { injectFilterReferences } from '../persistence'; import { trackUiEvent } from '../lens_ui_telemetry'; -import { - DataPublicPluginStart, - esFilters, - exporters, - Filter, - IndexPattern as IndexPatternInstance, - IndexPatternsContract, - Query, - SavedQuery, - syncQueryStateWithUrl, - waitUntilNextSessionCompletes$, -} from '../../../../../src/plugins/data/public'; -import { LENS_EMBEDDABLE_TYPE, getFullPath, APP_ID } from '../../common'; -import { LensAppProps, LensAppServices, LensAppState } from './types'; -import { getLensTopNavConfig } from './lens_top_nav'; +import { esFilters, syncQueryStateWithUrl } from '../../../../../src/plugins/data/public'; +import { getFullPath, APP_ID } from '../../common'; +import { LensAppProps, LensAppServices, RunSave } from './types'; +import { LensTopNavMenu } from './lens_top_nav'; import { Document } from '../persistence'; import { SaveModal } from './save_modal'; import { LensByReferenceInput, LensEmbeddableInput, } from '../editor_frame_service/embeddable/embeddable'; -import { useTimeRange } from './time_range'; import { EditorFrameInstance } from '../types'; +import { + setState as setAppState, + useLensSelector, + useLensDispatch, + LensAppState, + DispatchSetState, +} from '../state_management'; export function App({ history, @@ -67,7 +56,6 @@ export function App({ data, chrome, overlays, - navigation, uiSettings, application, stateTransfer, @@ -81,29 +69,18 @@ export function App({ dashboardFeatureFlag, } = useKibana().services; - const startSession = useCallback(() => data.search.session.start(), [data.search.session]); - - const [state, setState] = useState(() => { - return { - query: data.query.queryString.getQuery(), - // Do not use app-specific filters from previous app, - // only if Lens was opened with the intention to visualize a field (e.g. coming from Discover) - filters: !initialContext - ? data.query.filterManager.getGlobalFilters() - : data.query.filterManager.getFilters(), - isLoading: Boolean(initialInput), - indexPatternsForTopNav: [], - isLinkedToOriginatingApp: Boolean(incomingState?.originatingApp), - isSaveable: false, - searchSessionId: startSession(), - }; - }); + const dispatch = useLensDispatch(); + const dispatchSetState: DispatchSetState = useCallback( + (state: Partial) => dispatch(setAppState(state)), + [dispatch] + ); + + const appState = useLensSelector((state) => state.app); // Used to show a popover that guides the user towards changing the date range when no data is available. const [indicateNoData, setIndicateNoData] = useState(false); const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); - - const { lastKnownDoc } = state; + const { lastKnownDoc } = appState; const showNoDataPopover = useCallback(() => { setIndicateNoData(true); @@ -116,19 +93,10 @@ export function App({ }, [ setIndicateNoData, indicateNoData, - state.query, - state.filters, - state.indexPatternsForTopNav, - state.searchSessionId, + appState.indexPatternsForTopNav, + appState.searchSessionId, ]); - const { resolvedDateRange, from: fromDate, to: toDate } = useTimeRange( - data, - state.lastKnownDoc, - setState, - state.searchSessionId - ); - const onError = useCallback( (e: { message: string }) => notifications.toasts.addDanger({ @@ -142,56 +110,13 @@ export function App({ Boolean( // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag.allowByValueEmbeddables && - state.isLinkedToOriginatingApp && + appState.isLinkedToOriginatingApp && !(initialInput as LensByReferenceInput)?.savedObjectId ), - [dashboardFeatureFlag.allowByValueEmbeddables, state.isLinkedToOriginatingApp, initialInput] + [dashboardFeatureFlag.allowByValueEmbeddables, appState.isLinkedToOriginatingApp, initialInput] ); useEffect(() => { - // Clear app-specific filters when navigating to Lens. Necessary because Lens - // can be loaded without a full page refresh. If the user navigates to Lens from Discover - // we keep the filters - if (!initialContext) { - data.query.filterManager.setAppFilters([]); - } - - const filterSubscription = data.query.filterManager.getUpdates$().subscribe({ - next: () => { - setState((s) => ({ - ...s, - filters: data.query.filterManager.getFilters(), - searchSessionId: startSession(), - })); - trackUiEvent('app_filters_updated'); - }, - }); - - const timeSubscription = data.query.timefilter.timefilter.getTimeUpdate$().subscribe({ - next: () => { - setState((s) => ({ - ...s, - searchSessionId: startSession(), - })); - }, - }); - - const autoRefreshSubscription = data.query.timefilter.timefilter - .getAutoRefreshFetch$() - .pipe( - tap(() => { - setState((s) => ({ - ...s, - searchSessionId: startSession(), - })); - }), - switchMap((done) => - // best way in lens to estimate that all panels are updated is to rely on search session service state - waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done)) - ) - ) - .subscribe(); - const kbnUrlStateStorage = createKbnUrlStateStorage({ history, useHash: uiSettings.get('state:storeInSessionStorage'), @@ -202,41 +127,10 @@ export function App({ kbnUrlStateStorage ); - const sessionSubscription = data.search.session - .getSession$() - // wait for a tick to filter/timerange subscribers the chance to update the session id in the state - .pipe(delay(0)) - // then update if it didn't get updated yet - .subscribe((newSessionId) => { - if (newSessionId) { - setState((prevState) => { - if (prevState.searchSessionId !== newSessionId) { - return { ...prevState, searchSessionId: newSessionId }; - } else { - return prevState; - } - }); - } - }); - return () => { stopSyncingQueryServiceStateWithUrl(); - filterSubscription.unsubscribe(); - timeSubscription.unsubscribe(); - autoRefreshSubscription.unsubscribe(); - sessionSubscription.unsubscribe(); }; - }, [ - data.query.filterManager, - data.query.timefilter.timefilter, - data.search.session, - notifications.toasts, - uiSettings, - data.query, - history, - initialContext, - startSession, - ]); + }, [data.search.session, notifications.toasts, uiSettings, data.query, history]); useEffect(() => { onAppLeave((actions) => { @@ -244,11 +138,11 @@ export function App({ // or when the user has configured something without saving if ( application.capabilities.visualize.save && - !_.isEqual( - state.persistedDoc?.state, + !isEqual( + appState.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters(lastKnownDoc)?.state ) && - (state.isSaveable || state.persistedDoc) + (appState.isSaveable || appState.persistedDoc) ) { return actions.confirm( i18n.translate('xpack.lens.app.unsavedWorkMessage', { @@ -265,8 +159,8 @@ export function App({ }, [ onAppLeave, lastKnownDoc, - state.isSaveable, - state.persistedDoc, + appState.isSaveable, + appState.persistedDoc, application.capabilities.visualize.save, ]); @@ -274,7 +168,7 @@ export function App({ useEffect(() => { const isByValueMode = getIsByValueMode(); const breadcrumbs: EuiBreadcrumb[] = []; - if (state.isLinkedToOriginatingApp && getOriginatingAppName() && redirectToOrigin) { + if (appState.isLinkedToOriginatingApp && getOriginatingAppName() && redirectToOrigin) { breadcrumbs.push({ onClick: () => { redirectToOrigin(); @@ -297,113 +191,31 @@ export function App({ let currentDocTitle = i18n.translate('xpack.lens.breadcrumbsCreate', { defaultMessage: 'Create', }); - if (state.persistedDoc) { + if (appState.persistedDoc) { currentDocTitle = isByValueMode ? i18n.translate('xpack.lens.breadcrumbsByValue', { defaultMessage: 'Edit visualization' }) - : state.persistedDoc.title; + : appState.persistedDoc.title; } breadcrumbs.push({ text: currentDocTitle }); chrome.setBreadcrumbs(breadcrumbs); }, [ dashboardFeatureFlag.allowByValueEmbeddables, - state.isLinkedToOriginatingApp, getOriginatingAppName, - state.persistedDoc, redirectToOrigin, getIsByValueMode, - initialInput, application, chrome, - ]); - - useEffect(() => { - if ( - !initialInput || - (attributeService.inputIsRefType(initialInput) && - initialInput.savedObjectId === state.persistedDoc?.savedObjectId) - ) { - return; - } - - setState((s) => ({ ...s, isLoading: true })); - attributeService - .unwrapAttributes(initialInput) - .then((attributes) => { - if (!initialInput) { - return; - } - const doc = { - ...initialInput, - ...attributes, - type: LENS_EMBEDDABLE_TYPE, - }; - - if (attributeService.inputIsRefType(initialInput)) { - chrome.recentlyAccessed.add( - getFullPath(initialInput.savedObjectId), - attributes.title, - initialInput.savedObjectId - ); - } - const indexPatternIds = _.uniq( - doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) - ); - getAllIndexPatterns(indexPatternIds, data.indexPatterns) - .then(({ indexPatterns }) => { - // Don't overwrite any pinned filters - data.query.filterManager.setAppFilters( - injectFilterReferences(doc.state.filters, doc.references) - ); - setState((s) => ({ - ...s, - isLoading: false, - ...(!_.isEqual(state.persistedDoc, doc) ? { persistedDoc: doc } : null), - lastKnownDoc: doc, - query: doc.state.query, - indexPatternsForTopNav: indexPatterns, - })); - }) - .catch((e) => { - setState((s) => ({ ...s, isLoading: false })); - redirectTo(); - }); - }) - .catch((e) => { - setState((s) => ({ ...s, isLoading: false })); - notifications.toasts.addDanger( - i18n.translate('xpack.lens.app.docLoadingError', { - defaultMessage: 'Error loading saved document', - }) - ); - - redirectTo(); - }); - }, [ - notifications, - data.indexPatterns, - data.query.filterManager, initialInput, - attributeService, - redirectTo, - chrome.recentlyAccessed, - state.persistedDoc, + appState.isLinkedToOriginatingApp, + appState.persistedDoc, ]); const tagsIds = - state.persistedDoc && savedObjectsTagging - ? savedObjectsTagging.ui.getTagIdsFromReferences(state.persistedDoc.references) + appState.persistedDoc && savedObjectsTagging + ? savedObjectsTagging.ui.getTagIdsFromReferences(appState.persistedDoc.references) : []; - const runSave = async ( - saveProps: Omit & { - returnToOrigin: boolean; - dashboardId?: string | null; - onTitleDuplicate?: OnSaveProps['onTitleDuplicate']; - newDescription?: string; - newTags?: string[]; - }, - options: { saveToLibrary: boolean } - ) => { + const runSave: RunSave = async (saveProps, options) => { if (!lastKnownDoc) { return; } @@ -502,10 +314,8 @@ export function App({ docToSave.title, newInput.savedObjectId ); - setState((s) => ({ - ...s, - isLinkedToOriginatingApp: false, - })); + + dispatchSetState({ isLinkedToOriginatingApp: false }); setIsSaveModalVisible(false); // remove editor state so the connection is still broken after reload @@ -519,12 +329,12 @@ export function App({ ...docToSave, ...newInput, }; - setState((s) => ({ - ...s, + + dispatchSetState({ + isLinkedToOriginatingApp: false, persistedDoc: newDoc, lastKnownDoc: newDoc, - isLinkedToOriginatingApp: false, - })); + }); setIsSaveModalVisible(false); } catch (e) { @@ -535,187 +345,37 @@ export function App({ } }; - const lastKnownDocRef = useRef(state.lastKnownDoc); - lastKnownDocRef.current = state.lastKnownDoc; - - const activeDataRef = useRef(state.activeData); - activeDataRef.current = state.activeData; - - const { TopNavMenu } = navigation.ui; - const savingToLibraryPermitted = Boolean( - state.isSaveable && application.capabilities.visualize.save - ); - const savingToDashboardPermitted = Boolean( - state.isSaveable && application.capabilities.dashboard?.showWriteControls + appState.isSaveable && application.capabilities.visualize.save ); - const unsavedTitle = i18n.translate('xpack.lens.app.unsavedFilename', { - defaultMessage: 'unsaved', - }); - const topNavConfig = getLensTopNavConfig({ - showSaveAndReturn: Boolean( - state.isLinkedToOriginatingApp && - // Temporarily required until the 'by value' paradigm is default. - (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) - ), - enableExportToCSV: Boolean( - state.isSaveable && state.activeData && Object.keys(state.activeData).length - ), - isByValueMode: getIsByValueMode(), - allowByValue: dashboardFeatureFlag.allowByValueEmbeddables, - showCancel: Boolean(state.isLinkedToOriginatingApp), - savingToLibraryPermitted, - savingToDashboardPermitted, - actions: { - exportToCSV: () => { - if (!state.activeData) { - return; - } - const datatables = Object.values(state.activeData); - const content = datatables.reduce>( - (memo, datatable, i) => { - // skip empty datatables - if (datatable) { - const postFix = datatables.length > 1 ? `-${i + 1}` : ''; - - memo[`${lastKnownDoc?.title || unsavedTitle}${postFix}.csv`] = { - content: exporters.datatableToCSV(datatable, { - csvSeparator: uiSettings.get('csv:separator', ','), - quoteValues: uiSettings.get('csv:quoteValues', true), - formatFactory: data.fieldFormats.deserialize, - }), - type: exporters.CSV_MIME_TYPE, - }; - } - return memo; - }, - {} - ); - if (content) { - downloadMultipleAs(content); - } - }, - saveAndReturn: () => { - if (savingToDashboardPermitted && lastKnownDoc) { - // disabling the validation on app leave because the document has been saved. - onAppLeave((actions) => { - return actions.default(); - }); - runSave( - { - newTitle: lastKnownDoc.title, - newCopyOnSave: false, - isTitleDuplicateConfirmed: false, - returnToOrigin: true, - }, - { - saveToLibrary: - (initialInput && attributeService.inputIsRefType(initialInput)) ?? false, - } - ); - } - }, - showSaveModal: () => { - if (savingToDashboardPermitted || savingToLibraryPermitted) { - setIsSaveModalVisible(true); - } - }, - cancel: () => { - if (redirectToOrigin) { - redirectToOrigin(); - } - }, - }, - }); - return ( <>
- { - const { dateRange, query } = payload; - const currentRange = data.query.timefilter.timefilter.getTime(); - if (dateRange.from !== currentRange.from || dateRange.to !== currentRange.to) { - data.query.timefilter.timefilter.setTime(dateRange); - trackUiEvent('app_date_change'); - } else { - // Query has changed, renew the session id. - // Time change will be picked up by the time subscription - setState((s) => ({ - ...s, - searchSessionId: startSession(), - })); - trackUiEvent('app_query_change'); - } - setState((s) => ({ - ...s, - query: query || s.query, - })); - }} - onSaved={(savedQuery) => { - setState((s) => ({ ...s, savedQuery })); - }} - onSavedQueryUpdated={(savedQuery) => { - const savedQueryFilters = savedQuery.attributes.filters || []; - const globalFilters = data.query.filterManager.getGlobalFilters(); - data.query.filterManager.setFilters([...globalFilters, ...savedQueryFilters]); - setState((s) => ({ - ...s, - savedQuery: { ...savedQuery }, // Shallow query for reference issues - query: savedQuery.attributes.query, - })); - }} - onClearSavedQuery={() => { - data.query.filterManager.setFilters(data.query.filterManager.getGlobalFilters()); - setState((s) => ({ - ...s, - savedQuery: undefined, - filters: data.query.filterManager.getGlobalFilters(), - query: data.query.queryString.getDefaultQuery(), - })); - }} - query={state.query} - dateRangeFrom={fromDate} - dateRangeTo={toDate} + - {(!state.isLoading || state.persistedDoc) && ( + {(!appState.isAppLoading || appState.persistedDoc) && ( )}
Toast; showNoDataPopover: () => void; initialContext: VisualizeFieldContext | undefined; - setState: React.Dispatch>; - data: DataPublicPluginStart; - lastKnownDoc: React.MutableRefObject; - activeData: React.MutableRefObject | undefined>; }) { const { EditorFrameContainer } = editorFrame; return ( { - if (isSaveable !== oldIsSaveable) { - setState((s) => ({ ...s, isSaveable })); - } - if (!_.isEqual(persistedDoc, doc) && !_.isEqual(lastKnownDoc.current, doc)) { - setState((s) => ({ ...s, lastKnownDoc: doc })); - } - if (!_.isEqual(activeDataRef.current, activeData)) { - setState((s) => ({ ...s, activeData })); - } - - // Update the cached index patterns if the user made a change to any of them - if ( - indexPatternsForTopNav.length !== filterableIndexPatterns.length || - filterableIndexPatterns.some( - (id) => !indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id) - ) - ) { - getAllIndexPatterns(filterableIndexPatterns, data.indexPatterns).then( - ({ indexPatterns }) => { - if (indexPatterns) { - setState((s) => ({ ...s, indexPatternsForTopNav: indexPatterns })); - } - } - ); - } - }} /> ); }); -export async function getAllIndexPatterns( - ids: string[], - indexPatternsService: IndexPatternsContract -): Promise<{ indexPatterns: IndexPatternInstance[]; rejectedIds: string[] }> { - const responses = await Promise.allSettled(ids.map((id) => indexPatternsService.get(id))); - const fullfilled = responses.filter( - (response): response is PromiseFulfilledResult => - response.status === 'fulfilled' - ); - const rejectedIds = responses - .map((_response, i) => ids[i]) - .filter((id, i) => responses[i].status === 'rejected'); - // return also the rejected ids in case we want to show something later on - return { indexPatterns: fullfilled.map((response) => response.value), rejectedIds }; -} - function getLastKnownDocWithoutPinnedFilters(doc?: Document) { if (!doc) return undefined; - const [pinnedFilters, appFilters] = _.partition( + const [pinnedFilters, appFilters] = partition( injectFilterReferences(doc.state?.filters || [], doc.references), esFilters.isFilterPinned ); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index f90a21b2818d47..245e964bbd2e67 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -5,11 +5,25 @@ * 2.0. */ +import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; +import React from 'react'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; -import { LensTopNavActions } from './types'; +import { LensAppServices, LensTopNavActions, LensTopNavMenuProps } from './types'; +import { downloadMultipleAs } from '../../../../../src/plugins/share/public'; +import { trackUiEvent } from '../lens_ui_telemetry'; +import { exporters } from '../../../../../src/plugins/data/public'; -export function getLensTopNavConfig(options: { +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { + setState as setAppState, + useLensSelector, + useLensDispatch, + LensAppState, + DispatchSetState, +} from '../state_management'; + +function getLensTopNavConfig(options: { showSaveAndReturn: boolean; enableExportToCSV: boolean; showCancel: boolean; @@ -101,6 +115,185 @@ export function getLensTopNavConfig(options: { }), }); } - return topNavMenu; } + +export const LensTopNavMenu = ({ + setHeaderActionMenu, + initialInput, + indicateNoData, + setIsSaveModalVisible, + getIsByValueMode, + runSave, + onAppLeave, + redirectToOrigin, +}: LensTopNavMenuProps) => { + const { + data, + navigation, + uiSettings, + application, + attributeService, + dashboardFeatureFlag, + } = useKibana().services; + + const dispatch = useLensDispatch(); + const dispatchSetState: DispatchSetState = React.useCallback( + (state: Partial) => dispatch(setAppState(state)), + [dispatch] + ); + + const { + isSaveable, + isLinkedToOriginatingApp, + indexPatternsForTopNav, + query, + lastKnownDoc, + activeData, + savedQuery, + } = useLensSelector((state) => state.app); + + const { TopNavMenu } = navigation.ui; + const { from, to } = data.query.timefilter.timefilter.getTime(); + + const savingToLibraryPermitted = Boolean(isSaveable && application.capabilities.visualize.save); + const savingToDashboardPermitted = Boolean( + isSaveable && application.capabilities.dashboard?.showWriteControls + ); + + const unsavedTitle = i18n.translate('xpack.lens.app.unsavedFilename', { + defaultMessage: 'unsaved', + }); + const topNavConfig = getLensTopNavConfig({ + showSaveAndReturn: Boolean( + isLinkedToOriginatingApp && + // Temporarily required until the 'by value' paradigm is default. + (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) + ), + enableExportToCSV: Boolean(isSaveable && activeData && Object.keys(activeData).length), + isByValueMode: getIsByValueMode(), + allowByValue: dashboardFeatureFlag.allowByValueEmbeddables, + showCancel: Boolean(isLinkedToOriginatingApp), + savingToLibraryPermitted, + savingToDashboardPermitted, + actions: { + exportToCSV: () => { + if (!activeData) { + return; + } + const datatables = Object.values(activeData); + const content = datatables.reduce>( + (memo, datatable, i) => { + // skip empty datatables + if (datatable) { + const postFix = datatables.length > 1 ? `-${i + 1}` : ''; + + memo[`${lastKnownDoc?.title || unsavedTitle}${postFix}.csv`] = { + content: exporters.datatableToCSV(datatable, { + csvSeparator: uiSettings.get('csv:separator', ','), + quoteValues: uiSettings.get('csv:quoteValues', true), + formatFactory: data.fieldFormats.deserialize, + }), + type: exporters.CSV_MIME_TYPE, + }; + } + return memo; + }, + {} + ); + if (content) { + downloadMultipleAs(content); + } + }, + saveAndReturn: () => { + if (savingToDashboardPermitted && lastKnownDoc) { + // disabling the validation on app leave because the document has been saved. + onAppLeave((actions) => { + return actions.default(); + }); + runSave( + { + newTitle: lastKnownDoc.title, + newCopyOnSave: false, + isTitleDuplicateConfirmed: false, + returnToOrigin: true, + }, + { + saveToLibrary: + (initialInput && attributeService.inputIsRefType(initialInput)) ?? false, + } + ); + } + }, + showSaveModal: () => { + if (savingToDashboardPermitted || savingToLibraryPermitted) { + setIsSaveModalVisible(true); + } + }, + cancel: () => { + if (redirectToOrigin) { + redirectToOrigin(); + } + }, + }, + }); + + return ( + { + const { dateRange, query: newQuery } = payload; + const currentRange = data.query.timefilter.timefilter.getTime(); + if (dateRange.from !== currentRange.from || dateRange.to !== currentRange.to) { + data.query.timefilter.timefilter.setTime(dateRange); + trackUiEvent('app_date_change'); + } else { + // Query has changed, renew the session id. + // Time change will be picked up by the time subscription + dispatchSetState({ searchSessionId: data.search.session.start() }); + trackUiEvent('app_query_change'); + } + if (newQuery) { + if (!isEqual(newQuery, query)) { + dispatchSetState({ query: newQuery }); + } + } + }} + onSaved={(newSavedQuery) => { + dispatchSetState({ savedQuery: newSavedQuery }); + }} + onSavedQueryUpdated={(newSavedQuery) => { + const savedQueryFilters = newSavedQuery.attributes.filters || []; + const globalFilters = data.query.filterManager.getGlobalFilters(); + data.query.filterManager.setFilters([...globalFilters, ...savedQueryFilters]); + dispatchSetState({ + query: newSavedQuery.attributes.query, + savedQuery: { ...newSavedQuery }, + }); // Shallow query for reference issues + }} + onClearSavedQuery={() => { + data.query.filterManager.setFilters(data.query.filterManager.getGlobalFilters()); + dispatchSetState({ + filters: data.query.filterManager.getGlobalFilters(), + query: data.query.queryString.getDefaultQuery(), + savedQuery: undefined, + }); + }} + indexPatterns={indexPatternsForTopNav} + query={query} + dateRangeFrom={from} + dateRangeTo={to} + indicateNoData={indicateNoData} + showSearchBar={true} + showDatePicker={true} + showQueryBar={true} + showFilterBar={true} + data-test-subj="lnsApp_topNav" + screenTitle={'lens'} + appName={'lens'} + /> + ); +}; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx new file mode 100644 index 00000000000000..e84f6fd43418be --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { makeDefaultServices, mockLensStore } from '../mocks'; +import { act } from 'react-dom/test-utils'; +import { loadDocument } from './mounter'; +import { LensEmbeddableInput } from '../editor_frame_service/embeddable/embeddable'; + +const defaultSavedObjectId = '1234'; + +describe('Mounter', () => { + const byValueFlag = { allowByValueEmbeddables: true }; + describe('loadDocument', () => { + it('does not load a document if there is no initial input', async () => { + const services = makeDefaultServices(); + const redirectCallback = jest.fn(); + const lensStore = mockLensStore({ data: services.data }); + await loadDocument(redirectCallback, undefined, services, lensStore, undefined, byValueFlag); + expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); + }); + + it('loads a document and uses query and filters if initial input is provided', async () => { + const services = makeDefaultServices(); + const redirectCallback = jest.fn(); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + savedObjectId: defaultSavedObjectId, + state: { + query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], + }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], + }); + + const lensStore = await mockLensStore({ data: services.data }); + await act(async () => { + await loadDocument( + redirectCallback, + { savedObjectId: defaultSavedObjectId } as LensEmbeddableInput, + services, + lensStore, + undefined, + byValueFlag + ); + }); + + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ + savedObjectId: defaultSavedObjectId, + }); + + expect(services.data.indexPatterns.get).toHaveBeenCalledWith('1'); + + expect(services.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ + { query: { match_phrase: { src: 'test' } } }, + ]); + + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + persistedDoc: expect.objectContaining({ + savedObjectId: defaultSavedObjectId, + state: expect.objectContaining({ + query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], + }), + }), + }), + }); + }); + + it('does not load documents on sequential renders unless the id changes', async () => { + const redirectCallback = jest.fn(); + const services = makeDefaultServices(); + const lensStore = mockLensStore({ data: services.data }); + + await act(async () => { + await loadDocument( + redirectCallback, + { savedObjectId: defaultSavedObjectId } as LensEmbeddableInput, + services, + lensStore, + undefined, + byValueFlag + ); + }); + + await act(async () => { + await loadDocument( + redirectCallback, + { savedObjectId: defaultSavedObjectId } as LensEmbeddableInput, + services, + lensStore, + undefined, + byValueFlag + ); + }); + + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); + + await act(async () => { + await loadDocument( + redirectCallback, + { savedObjectId: '5678' } as LensEmbeddableInput, + services, + lensStore, + undefined, + byValueFlag + ); + }); + + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(2); + }); + + it('handles document load errors', async () => { + const services = makeDefaultServices(); + const redirectCallback = jest.fn(); + + const lensStore = mockLensStore({ data: services.data }); + + services.attributeService.unwrapAttributes = jest.fn().mockRejectedValue('failed to load'); + + await act(async () => { + await loadDocument( + redirectCallback, + { savedObjectId: defaultSavedObjectId } as LensEmbeddableInput, + services, + lensStore, + undefined, + byValueFlag + ); + }); + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ + savedObjectId: defaultSavedObjectId, + }); + expect(services.notifications.toasts.addDanger).toHaveBeenCalled(); + expect(redirectCallback).toHaveBeenCalled(); + }); + + it('adds to the recently accessed list on load', async () => { + const redirectCallback = jest.fn(); + + const services = makeDefaultServices(); + const lensStore = mockLensStore({ data: services.data }); + await act(async () => { + await loadDocument( + redirectCallback, + ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput, + services, + lensStore, + undefined, + byValueFlag + ); + }); + + expect(services.chrome.recentlyAccessed.add).toHaveBeenCalledWith( + '/app/lens#/edit/1234', + 'An extremely cool default document!', + '1234' + ); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index e6eb115562d378..3e56fbb2003cb4 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -15,6 +15,9 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; import { DashboardFeatureFlagConfig } from 'src/plugins/dashboard/public'; +import { Provider } from 'react-redux'; +import { uniq, isEqual } from 'lodash'; +import { EmbeddableEditorState } from 'src/plugins/embeddable/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry'; @@ -23,7 +26,7 @@ import { App } from './app'; import { EditorFrameStart } from '../types'; import { addHelpMenuToAppChrome } from '../help_menu_util'; import { LensPluginStartDependencies } from '../plugin'; -import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID, getFullPath } from '../../common'; import { LensEmbeddableInput, LensByReferenceInput, @@ -34,6 +37,16 @@ import { LensAttributeService } from '../lens_attribute_service'; import { LensAppServices, RedirectToOriginProps, HistoryLocationState } from './types'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { + makeConfigureStore, + navigateAway, + getPreloadedState, + LensRootStore, + setState, +} from '../state_management'; +import { getAllIndexPatterns, getResolvedDateRange } from '../utils'; +import { injectFilterReferences } from '../persistence'; + export async function mountApp( core: CoreSetup, params: AppMountParameters, @@ -59,6 +72,8 @@ export async function mountApp( const historyLocationState = params.history.location.state as HistoryLocationState; const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID); + const dashboardFeatureFlag = await getByValueFeatureFlag(); + const lensServices: LensAppServices = { data, storage, @@ -80,7 +95,7 @@ export async function mountApp( }, // Temporarily required until the 'by value' paradigm is default. - dashboardFeatureFlag: await getByValueFeatureFlag(), + dashboardFeatureFlag, }; addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks); @@ -149,8 +164,31 @@ export async function mountApp( coreStart.application.navigateToApp(embeddableEditorIncomingState?.originatingApp); } }; + const initialContext = + historyLocationState && historyLocationState.type === ACTION_VISUALIZE_LENS_FIELD + ? historyLocationState.payload + : undefined; + + // Clear app-specific filters when navigating to Lens. Necessary because Lens + // can be loaded without a full page refresh. If the user navigates to Lens from Discover + // we keep the filters + if (!initialContext) { + data.query.filterManager.setAppFilters([]); + } + const preloadedState = getPreloadedState({ + query: data.query.queryString.getQuery(), + // Do not use app-specific filters from previous app, + // only if Lens was opened with the intention to visualize a field (e.g. coming from Discover) + filters: !initialContext + ? data.query.filterManager.getGlobalFilters() + : data.query.filterManager.getFilters(), + searchSessionId: data.search.session.getSessionId(), + resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), + isLinkedToOriginatingApp: Boolean(embeddableEditorIncomingState?.originatingApp), + }); + + const lensStore: LensRootStore = makeConfigureStore(preloadedState, { data }); - // const featureFlagConfig = await getByValueFeatureFlag(); const EditorRenderer = React.memo( (props: { id?: string; history: History; editByValue?: boolean }) => { const redirectCallback = useCallback( @@ -160,23 +198,30 @@ export async function mountApp( [props.history] ); trackUiEvent('loaded'); + const initialInput = getInitialInput(props.id, props.editByValue); + loadDocument( + redirectCallback, + initialInput, + lensServices, + lensStore, + embeddableEditorIncomingState, + dashboardFeatureFlag + ); return ( - + + + ); } ); @@ -229,8 +274,98 @@ export async function mountApp( params.element ); return () => { - data.search.session.clear(); unmountComponentAtNode(params.element); unlistenParentHistory(); + lensStore.dispatch(navigateAway()); }; } + +export function loadDocument( + redirectCallback: (savedObjectId?: string) => void, + initialInput: LensEmbeddableInput | undefined, + lensServices: LensAppServices, + lensStore: LensRootStore, + embeddableEditorIncomingState: EmbeddableEditorState | undefined, + dashboardFeatureFlag: DashboardFeatureFlagConfig +) { + const { attributeService, chrome, notifications, data } = lensServices; + const { persistedDoc } = lensStore.getState().app; + if ( + !initialInput || + (attributeService.inputIsRefType(initialInput) && + initialInput.savedObjectId === persistedDoc?.savedObjectId) + ) { + return; + } + lensStore.dispatch(setState({ isAppLoading: true })); + + attributeService + .unwrapAttributes(initialInput) + .then((attributes) => { + if (!initialInput) { + return; + } + const doc = { + ...initialInput, + ...attributes, + type: LENS_EMBEDDABLE_TYPE, + }; + + if (attributeService.inputIsRefType(initialInput)) { + chrome.recentlyAccessed.add( + getFullPath(initialInput.savedObjectId), + attributes.title, + initialInput.savedObjectId + ); + } + const indexPatternIds = uniq( + doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) + ); + getAllIndexPatterns(indexPatternIds, data.indexPatterns) + .then(({ indexPatterns }) => { + // Don't overwrite any pinned filters + data.query.filterManager.setAppFilters( + injectFilterReferences(doc.state.filters, doc.references) + ); + const currentSessionId = data.search.session.getSessionId(); + lensStore.dispatch( + setState({ + query: doc.state.query, + isAppLoading: false, + indexPatternsForTopNav: indexPatterns, + lastKnownDoc: doc, + searchSessionId: + dashboardFeatureFlag.allowByValueEmbeddables && + Boolean(embeddableEditorIncomingState?.originatingApp) && + !(initialInput as LensByReferenceInput)?.savedObjectId && + currentSessionId + ? currentSessionId + : data.search.session.start(), + ...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null), + }) + ); + }) + .catch((e) => { + lensStore.dispatch( + setState({ + isAppLoading: false, + }) + ); + redirectCallback(); + }); + }) + .catch((e) => { + lensStore.dispatch( + setState({ + isAppLoading: false, + }) + ); + notifications.toasts.addDanger( + i18n.translate('xpack.lens.app.docLoadingError', { + defaultMessage: 'Error loading saved document', + }) + ); + + redirectCallback(); + }); +} diff --git a/x-pack/plugins/lens/public/app_plugin/time_range.ts b/x-pack/plugins/lens/public/app_plugin/time_range.ts deleted file mode 100644 index c9e507f3e6f132..00000000000000 --- a/x-pack/plugins/lens/public/app_plugin/time_range.ts +++ /dev/null @@ -1,84 +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 './app.scss'; - -import _ from 'lodash'; -import moment from 'moment'; -import { useEffect, useMemo } from 'react'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { LensAppState } from './types'; -import { Document } from '../persistence'; - -function containsDynamicMath(dateMathString: string) { - return dateMathString.includes('now'); -} - -const TIME_LAG_PERCENTAGE_LIMIT = 0.02; - -/** - * Fetches the current global time range from data plugin and restarts session - * if the fixed "now" parameter is diverging too much from the actual current time. - * @param data data plugin contract to manage current now value, time range and session - * @param lastKnownDoc Current state of the editor - * @param setState state setter for Lens app state - * @param searchSessionId current session id - */ -export function useTimeRange( - data: DataPublicPluginStart, - lastKnownDoc: Document | undefined, - setState: React.Dispatch>, - searchSessionId: string -) { - const timefilter = data.query.timefilter.timefilter; - const { from, to } = data.query.timefilter.timefilter.getTime(); - - // Need a stable reference for the frame component of the dateRange - const resolvedDateRange = useMemo(() => { - const { min, max } = timefilter.calculateBounds({ - from, - to, - }); - return { fromDate: min?.toISOString() || from, toDate: max?.toISOString() || to }; - // recalculate current date range if the session gets updated because it - // might change "now" and calculateBounds depends on it internally - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [timefilter, searchSessionId, from, to]); - - useEffect(() => { - const unresolvedTimeRange = timefilter.getTime(); - if ( - !containsDynamicMath(unresolvedTimeRange.from) && - !containsDynamicMath(unresolvedTimeRange.to) - ) { - return; - } - - const { min, max } = timefilter.getBounds(); - - if (!min || !max) { - // bounds not fully specified, bailing out - return; - } - - // calculate length of currently configured range in ms - const timeRangeLength = moment.duration(max.diff(min)).asMilliseconds(); - - // calculate lag of managed "now" for date math - const nowDiff = Date.now() - data.nowProvider.get().valueOf(); - - // if the lag is signifcant, start a new session to clear the cache - if (nowDiff > timeRangeLength * TIME_LAG_PERCENTAGE_LIMIT) { - setState((s) => ({ - ...s, - searchSessionId: data.search.session.start(), - })); - } - }, [data.nowProvider, data.search.session, timefilter, lastKnownDoc, setState]); - - return { resolvedDateRange, from, to }; -} diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index c9143542e67bfb..72850552723f33 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -6,6 +6,7 @@ */ import { History } from 'history'; +import { OnSaveProps } from 'src/plugins/saved_objects/public'; import { ApplicationStart, AppMountParameters, @@ -16,14 +17,7 @@ import { OverlayStart, SavedObjectsStart, } from '../../../../../src/core/public'; -import { - DataPublicPluginStart, - Filter, - IndexPattern, - Query, - SavedQuery, -} from '../../../../../src/plugins/data/public'; -import { Document } from '../persistence'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { LensEmbeddableInput } from '../editor_frame_service/embeddable/embeddable'; import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; import { LensAttributeService } from '../lens_attribute_service'; @@ -38,28 +32,7 @@ import { EmbeddableEditorState, EmbeddableStateTransfer, } from '../../../../../src/plugins/embeddable/public'; -import { TableInspectorAdapter } from '../editor_frame_service/types'; import { EditorFrameInstance } from '../types'; - -export interface LensAppState { - isLoading: boolean; - persistedDoc?: Document; - lastKnownDoc?: Document; - - // index patterns used to determine which filters are available in the top nav. - indexPatternsForTopNav: IndexPattern[]; - - // Determines whether the lens editor shows the 'save and return' button, and the originating app breadcrumb. - isLinkedToOriginatingApp?: boolean; - - query: Query; - filters: Filter[]; - savedQuery?: SavedQuery; - isSaveable: boolean; - activeData?: TableInspectorAdapter; - searchSessionId: string; -} - export interface RedirectToOriginProps { input?: LensEmbeddableInput; isCopied?: boolean; @@ -82,6 +55,32 @@ export interface LensAppProps { initialContext?: VisualizeFieldContext; } +export type RunSave = ( + saveProps: Omit & { + returnToOrigin: boolean; + dashboardId?: string | null; + onTitleDuplicate?: OnSaveProps['onTitleDuplicate']; + newDescription?: string; + newTags?: string[]; + }, + options: { + saveToLibrary: boolean; + } +) => Promise; + +export interface LensTopNavMenuProps { + onAppLeave: AppMountParameters['onAppLeave']; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + + redirectToOrigin?: (props?: RedirectToOriginProps) => void; + // The initial input passed in by the container when editing. Can be either by reference or by value. + initialInput?: LensEmbeddableInput; + getIsByValueMode: () => boolean; + indicateNoData: boolean; + setIsSaveModalVisible: React.Dispatch>; + runSave: RunSave; +} + export interface HistoryLocationState { type: typeof ACTION_VISUALIZE_LENS_FIELD; payload: VisualizeFieldContext; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap index afc69c2e8861f9..a4be46f61990b6 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap +++ b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap @@ -13,6 +13,13 @@ exports[`DatatableComponent it renders actions column when there are row actions "b": "left", "c": "right", }, + "getColorForValue": [MockFunction], + "minMaxByColumnId": Object { + "c": Object { + "max": 3, + "min": 3, + }, + }, "rowHasRowClickTriggerActions": Array [ true, true, @@ -244,6 +251,13 @@ exports[`DatatableComponent it renders the title and value 1`] = ` "b": "left", "c": "right", }, + "getColorForValue": [MockFunction], + "minMaxByColumnId": Object { + "c": Object { + "max": 3, + "min": 3, + }, + }, "rowHasRowClickTriggerActions": undefined, "table": Object { "columns": Array [ @@ -462,6 +476,13 @@ exports[`DatatableComponent it should not render actions on header when it is in "b": "left", "c": "right", }, + "getColorForValue": [MockFunction], + "minMaxByColumnId": Object { + "c": Object { + "max": 3, + "min": 3, + }, + }, "rowHasRowClickTriggerActions": Array [ false, false, diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.test.tsx index 9bc982ebd9944b..67255dc8a953ea 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.test.tsx @@ -11,6 +11,12 @@ import { DataContext } from './table_basic'; import { createGridCell } from './cell_value'; import { FieldFormat } from 'src/plugins/data/public'; import { Datatable } from 'src/plugins/expressions/public'; +import { IUiSettingsClient } from 'kibana/public'; +import { act } from 'react-dom/test-utils'; +import { ReactWrapper } from 'enzyme'; +import { Args, ColumnConfigArg } from '../expression'; +import { DataContextType } from './types'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; describe('datatable cell renderer', () => { const table: Datatable = { @@ -30,7 +36,9 @@ describe('datatable cell renderer', () => { { a: { convert: (x) => `formatted ${x}` } as FieldFormat, }, - DataContext + { columns: [], sortingColumnId: '', sortingDirection: 'none' }, + DataContext, + ({ get: jest.fn() } as unknown) as IUiSettingsClient ); it('renders formatted value', () => { @@ -78,4 +86,111 @@ describe('datatable cell renderer', () => { ); expect(cell.find('.lnsTableCell').prop('className')).toContain('--right'); }); + + describe('dynamic coloring', () => { + const paletteRegistry = chartPluginMock.createPaletteRegistry(); + const customPalette = paletteRegistry.get('custom'); + + function getCellRenderer(columnConfig: Args) { + return createGridCell( + { + a: { convert: (x) => `formatted ${x}` } as FieldFormat, + }, + columnConfig, + DataContext, + ({ get: jest.fn() } as unknown) as IUiSettingsClient + ); + } + function getColumnConfiguration(): Args { + return { + title: 'myData', + columns: [ + { + columnId: 'a', + colorMode: 'none', + palette: { + type: 'palette', + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc', '#ddd', '#eee'], + gradient: false, + stops: [20, 40, 60, 80, 100], + range: 'percent', + rangeMin: 0, + rangeMax: 100, + }, + }, + type: 'lens_datatable_column', + } as ColumnConfigArg, + ], + sortingColumnId: '', + sortingDirection: 'none', + }; + } + + function flushEffect(component: ReactWrapper) { + return act(async () => { + await component; + await new Promise((r) => setImmediate(r)); + component.update(); + }); + } + + async function renderCellComponent(columnConfig: Args, context: Partial = {}) { + const CellRendererWithPalette = getCellRenderer(columnConfig); + const setCellProps = jest.fn(); + + const cell = mountWithIntl( + 123 */ } }, + getColorForValue: customPalette.getColorForValue, + ...context, + }} + > + + + ); + + await flushEffect(cell); + + return { setCellProps, cell }; + } + + it('ignores coloring when colorMode is set to "none"', async () => { + const { setCellProps } = await renderCellComponent(getColumnConfiguration()); + + expect(setCellProps).not.toHaveBeenCalled(); + }); + + it('should set the coloring of the cell when enabled', async () => { + const columnConfig = getColumnConfiguration(); + columnConfig.columns[0].colorMode = 'cell'; + + const { setCellProps } = await renderCellComponent(columnConfig, {}); + + expect(setCellProps).toHaveBeenCalledWith({ + style: expect.objectContaining({ backgroundColor: 'blue' }), + }); + }); + + it('should set the coloring of the text when enabled', async () => { + const columnConfig = getColumnConfiguration(); + columnConfig.columns[0].colorMode = 'text'; + + const { setCellProps } = await renderCellComponent(columnConfig, {}); + + expect(setCellProps).toHaveBeenCalledWith({ + style: expect.objectContaining({ color: 'blue' }), + }); + }); + }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx index 2261dd06b532ba..a6c50f00cb77fd 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx @@ -5,30 +5,74 @@ * 2.0. */ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { IUiSettingsClient } from 'kibana/public'; import type { FormatFactory } from '../../types'; import type { DataContextType } from './types'; +import { ColumnConfig } from './table_basic'; +import { getContrastColor } from '../../shared_components/coloring/utils'; +import { getOriginalId } from '../transpose_helpers'; export const createGridCell = ( formatters: Record>, - DataContext: React.Context -) => ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => { - const { table, alignments } = useContext(DataContext); - const rowValue = table?.rows[rowIndex][columnId]; - const content = formatters[columnId]?.convert(rowValue, 'html'); - const currentAlignment = alignments && alignments[columnId]; - const alignmentClassName = `lnsTableCell--${currentAlignment}`; + columnConfig: ColumnConfig, + DataContext: React.Context, + uiSettings: IUiSettingsClient +) => { + // Changing theme requires a full reload of the page, so we can cache here + const IS_DARK_THEME = uiSettings.get('theme:darkMode'); + return ({ rowIndex, columnId, setCellProps }: EuiDataGridCellValueElementProps) => { + const { table, alignments, minMaxByColumnId, getColorForValue } = useContext(DataContext); + const rowValue = table?.rows[rowIndex][columnId]; + const content = formatters[columnId]?.convert(rowValue, 'html'); + const currentAlignment = alignments && alignments[columnId]; + const alignmentClassName = `lnsTableCell--${currentAlignment}`; - return ( -
- ); + const { colorMode, palette } = + columnConfig.columns.find(({ columnId: id }) => id === columnId) || {}; + + useEffect(() => { + const originalId = getOriginalId(columnId); + if (minMaxByColumnId?.[originalId]) { + if (colorMode !== 'none' && palette?.params && getColorForValue) { + // workout the bucket the value belongs to + const color = getColorForValue(rowValue, palette.params, minMaxByColumnId[originalId]); + if (color) { + const style = { [colorMode === 'cell' ? 'backgroundColor' : 'color']: color }; + if (colorMode === 'cell' && color) { + style.color = getContrastColor(color, IS_DARK_THEME); + } + setCellProps({ + style, + }); + } + } + } + // make sure to clean it up when something change + // this avoids cell's styling to stick forever + return () => { + if (minMaxByColumnId?.[originalId]) { + setCellProps({ + style: { + backgroundColor: undefined, + color: undefined, + }, + }); + } + }; + }, [rowValue, columnId, setCellProps, colorMode, palette, minMaxByColumnId, getColorForValue]); + + return ( +
+ ); + }; }; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.scss b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.scss new file mode 100644 index 00000000000000..504adb05e57d7b --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.scss @@ -0,0 +1,7 @@ +.lnsDynamicColoringRow { + align-items: center; +} + +.lnsDynamicColoringClickable { + cursor: pointer; +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx index e0d31a3ed02012..88948e9a7615b8 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx @@ -12,12 +12,18 @@ import { DatatableVisualizationState } from '../visualization'; import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks'; import { mountWithIntl } from '@kbn/test/jest'; import { TableDimensionEditor } from './dimension_editor'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { PaletteRegistry } from 'src/plugins/charts/public'; +import { PalettePanelContainer } from './palette_panel_container'; +import { act } from 'react-dom/test-utils'; describe('data table dimension editor', () => { let frame: FramePublicAPI; let state: DatatableVisualizationState; let setState: (newState: DatatableVisualizationState) => void; - let props: VisualizationDimensionEditorProps; + let props: VisualizationDimensionEditorProps & { + paletteService: PaletteRegistry; + }; function testState(): DatatableVisualizationState { return { @@ -59,6 +65,8 @@ describe('data table dimension editor', () => { layerId: 'first', state, setState, + paletteService: chartPluginMock.createPaletteRegistry(), + panelRef: React.createRef(), }; }); @@ -72,17 +80,23 @@ describe('data table dimension editor', () => { it('should render default alignment for number', () => { frame.activeData!.first.columns[0].meta.type = 'number'; const instance = mountWithIntl(); - expect(instance.find(EuiButtonGroup).prop('idSelected')).toEqual( - expect.stringContaining('right') - ); + expect( + instance + .find('[data-test-subj="lnsDatatable_alignment_groups"]') + .find(EuiButtonGroup) + .prop('idSelected') + ).toEqual(expect.stringContaining('right')); }); it('should render specific alignment', () => { state.columns[0].alignment = 'center'; const instance = mountWithIntl(); - expect(instance.find(EuiButtonGroup).prop('idSelected')).toEqual( - expect.stringContaining('center') - ); + expect( + instance + .find('[data-test-subj="lnsDatatable_alignment_groups"]') + .find(EuiButtonGroup) + .prop('idSelected') + ).toEqual(expect.stringContaining('center')); }); it('should set state for the right column', () => { @@ -95,7 +109,10 @@ describe('data table dimension editor', () => { }, ]; const instance = mountWithIntl(); - instance.find(EuiButtonGroup).prop('onChange')('center'); + instance + .find('[data-test-subj="lnsDatatable_alignment_groups"]') + .find(EuiButtonGroup) + .prop('onChange')('center'); expect(setState).toHaveBeenCalledWith({ ...state, columns: [ @@ -109,4 +126,90 @@ describe('data table dimension editor', () => { ], }); }); + + it('should not show the dynamic coloring option for non numeric columns', () => { + const instance = mountWithIntl(); + expect(instance.find('[data-test-subj="lnsDatatable_dynamicColoring_groups"]').exists()).toBe( + false + ); + expect(instance.find('[data-test-subj="lnsDatatable_dynamicColoring_palette"]').exists()).toBe( + false + ); + }); + + it('should set the dynamic coloring default to "none"', () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + const instance = mountWithIntl(); + expect( + instance + .find('[data-test-subj="lnsDatatable_dynamicColoring_groups"]') + .find(EuiButtonGroup) + .prop('idSelected') + ).toEqual(expect.stringContaining('none')); + + expect(instance.find('[data-test-subj="lnsDatatable_dynamicColoring_palette"]').exists()).toBe( + false + ); + }); + + it('should show the dynamic palette display ony when colorMode is different from "none"', () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + state.columns[0].colorMode = 'text'; + const instance = mountWithIntl(); + expect( + instance + .find('[data-test-subj="lnsDatatable_dynamicColoring_groups"]') + .find(EuiButtonGroup) + .prop('idSelected') + ).toEqual(expect.stringContaining('text')); + + expect(instance.find('[data-test-subj="lnsDatatable_dynamicColoring_palette"]').exists()).toBe( + true + ); + }); + + it('should set the coloring mode to the right column', () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + state.columns = [ + { + columnId: 'foo', + }, + { + columnId: 'bar', + }, + ]; + const instance = mountWithIntl(); + instance + .find('[data-test-subj="lnsDatatable_dynamicColoring_groups"]') + .find(EuiButtonGroup) + .prop('onChange')('cell'); + expect(setState).toHaveBeenCalledWith({ + ...state, + columns: [ + { + columnId: 'foo', + colorMode: 'cell', + palette: expect.objectContaining({ type: 'palette' }), + }, + { + columnId: 'bar', + }, + ], + }); + }); + + it('should open the palette panel when "Settings" link is clicked in the palette input', () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + state.columns[0].colorMode = 'cell'; + const instance = mountWithIntl(); + + act(() => + (instance + .find('[data-test-subj="lnsDatatable_dynamicColoring_trigger"]') + .first() + .prop('onClick') as () => void)?.() + ); + + expect(instance.find(PalettePanelContainer).exists()).toBe(true); + }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx index a750744811790f..76c47a9c743c51 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx @@ -5,36 +5,91 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiSwitch, EuiButtonGroup, htmlIdGenerator } from '@elastic/eui'; +import { + EuiFormRow, + EuiSwitch, + EuiButtonGroup, + htmlIdGenerator, + EuiColorPaletteDisplay, + EuiFlexItem, + EuiFlexGroup, + EuiButtonEmpty, +} from '@elastic/eui'; +import { PaletteRegistry } from 'src/plugins/charts/public'; import { VisualizationDimensionEditorProps } from '../../types'; import { DatatableVisualizationState } from '../visualization'; import { getOriginalId } from '../transpose_helpers'; +import { + CustomizablePalette, + applyPaletteParams, + defaultPaletteParams, + FIXED_PROGRESSION, + getStopsForFixedMode, +} from '../../shared_components/'; +import { PalettePanelContainer } from './palette_panel_container'; +import { findMinMaxByColumnId } from './shared_utils'; +import './dimension_editor.scss'; const idPrefix = htmlIdGenerator()(); +type ColumnType = DatatableVisualizationState['columns'][number]; + +function updateColumnWith( + state: DatatableVisualizationState, + columnId: string, + newColumnProps: Partial +) { + return state.columns.map((currentColumn) => { + if (currentColumn.columnId === columnId) { + return { ...currentColumn, ...newColumnProps }; + } else { + return currentColumn; + } + }); +} + export function TableDimensionEditor( - props: VisualizationDimensionEditorProps + props: VisualizationDimensionEditorProps & { + paletteService: PaletteRegistry; + } ) { const { state, setState, frame, accessor } = props; const column = state.columns.find(({ columnId }) => accessor === columnId); + const [isPaletteOpen, setIsPaletteOpen] = useState(false); if (!column) return null; if (column.isTransposed) return null; + const currentData = frame.activeData?.[state.layerId]; + // either read config state or use same logic as chart itself - const currentAlignment = - column?.alignment || - (frame.activeData && - frame.activeData[state.layerId]?.columns.find( - (col) => col.id === accessor || getOriginalId(col.id) === accessor - )?.meta.type === 'number' - ? 'right' - : 'left'); + const isNumericField = + currentData?.columns.find((col) => col.id === accessor || getOriginalId(col.id) === accessor) + ?.meta.type === 'number'; + + const currentAlignment = column?.alignment || (isNumericField ? 'right' : 'left'); + const currentColorMode = column?.colorMode || 'none'; + const hasDynamicColoring = currentColorMode !== 'none'; const visibleColumnsCount = state.columns.filter((c) => !c.hidden).length; + const hasTransposedColumn = state.columns.some(({ isTransposed }) => isTransposed); + const columnsToCheck = hasTransposedColumn + ? currentData?.columns.filter(({ id }) => getOriginalId(id) === accessor).map(({ id }) => id) || + [] + : [accessor]; + const minMaxByColumnId = findMinMaxByColumnId(columnsToCheck, currentData); + const currentMinMax = minMaxByColumnId[accessor]; + + const activePalette = column?.palette || { + type: 'palette', + name: defaultPaletteParams.name, + }; + // need to tell the helper that the colorStops are required to display + const displayStops = applyPaletteParams(props.paletteService, activePalette, currentMinMax); + return ( <> { - const newMode = id.replace(idPrefix, '') as 'left' | 'right' | 'center'; - const newColumns = state.columns.map((currentColumn) => { - if (currentColumn.columnId === accessor) { - return { - ...currentColumn, - alignment: newMode, - }; - } else { - return currentColumn; - } + const newMode = id.replace(idPrefix, '') as ColumnType['alignment']; + setState({ + ...state, + columns: updateColumnWith(state, accessor, { alignment: newMode }), }); - setState({ ...state, columns: newColumns }); }} /> @@ -127,6 +175,135 @@ export function TableDimensionEditor( /> )} + {isNumericField && ( + <> + + { + const newMode = id.replace(idPrefix, '') as ColumnType['colorMode']; + const params: Partial = { + colorMode: newMode, + }; + if (!column?.palette && newMode !== 'none') { + params.palette = { + ...activePalette, + params: { + ...activePalette.params, + // that's ok, at first open we're going to throw them away and recompute + stops: displayStops, + }, + }; + } + // clear up when switching to no coloring + if (column?.palette && newMode === 'none') { + params.palette = undefined; + } + setState({ + ...state, + columns: updateColumnWith(state, accessor, params), + }); + }} + /> + + {hasDynamicColoring && ( + + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + /> + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + size="xs" + flush="both" + > + {i18n.translate('xpack.lens.paletteTableGradient.customize', { + defaultMessage: 'Edit', + })} + + setIsPaletteOpen(!isPaletteOpen)} + > + { + setState({ + ...state, + columns: updateColumnWith(state, accessor, { palette: newPalette }), + }); + }} + /> + + + + + )} + + )} ); } diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/palette_panel_container.scss b/x-pack/plugins/lens/public/datatable_visualization/components/palette_panel_container.scss new file mode 100644 index 00000000000000..db14d064d1881e --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/palette_panel_container.scss @@ -0,0 +1,53 @@ +@import '@elastic/eui/src/components/flyout/variables'; +@import '@elastic/eui/src/components/flyout/mixins'; + +.lnsPalettePanelContainer { + // Use the EuiFlyout style + @include euiFlyout; + // But with custom positioning to keep it within the sidebar contents + position: absolute; + right: 0; + left: 0; + top: 0; + bottom: 0; + animation: euiFlyout $euiAnimSpeedNormal $euiAnimSlightResistance; + // making just a bit higher than the dimension flyout to stack on top of it + z-index: $euiZLevel3 + 1 +} + +.lnsPalettePanelContainer__footer { + padding: $euiSizeS; +} + +.lnsPalettePanelContainer__header { + padding: $euiSizeS $euiSizeXS; +} + +.lnsPalettePanelContainer__headerTitle { + padding: $euiSizeS $euiSizeXS; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + +.lnsPalettePanelContainer__headerLink { + &:focus-within { + background-color: transparentize($euiColorVis1, .9); + + .lnsPalettePanelContainer__headerTitle { + text-decoration: underline; + } + } +} + +.lnsPalettePanelContainer__backIcon { + &:hover { + transform: none !important; // sass-lint:disable-line no-important + } + + &:focus { + background-color: transparent; + } +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/palette_panel_container.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/palette_panel_container.tsx new file mode 100644 index 00000000000000..1371fbe73ef845 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/palette_panel_container.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import './palette_panel_container.scss'; + +import React, { useState, useEffect, MutableRefObject } from 'react'; +import { + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiTitle, + EuiButtonIcon, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFocusTrap, + EuiOutsideClickDetector, + EuiPortal, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +export function PalettePanelContainer({ + isOpen, + handleClose, + children, + siblingRef, +}: { + isOpen: boolean; + handleClose: () => void; + children: React.ReactElement | React.ReactElement[]; + siblingRef: MutableRefObject; +}) { + const [focusTrapIsEnabled, setFocusTrapIsEnabled] = useState(false); + + const closeFlyout = () => { + handleClose(); + setFocusTrapIsEnabled(false); + }; + + useEffect(() => { + if (isOpen) { + // without setTimeout here the flyout pushes content when animating + setTimeout(() => { + setFocusTrapIsEnabled(true); + }, 255); + } + }, [isOpen]); + + return isOpen && siblingRef.current ? ( + + + +
+ + + + + + + +

+ + {i18n.translate('xpack.lens.table.palettePanelTitle', { + defaultMessage: 'Edit color', + })} + +

+
+
+
+
+ + {children} + + + + {i18n.translate('xpack.lens.table.palettePanelContainer.back', { + defaultMessage: 'Back', + })} + + +
+
+
+
+ ) : null; +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/shared_utils.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/shared_utils.tsx new file mode 100644 index 00000000000000..92a949e65c67ea --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/shared_utils.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Datatable } from 'src/plugins/expressions'; +import { getOriginalId } from '../transpose_helpers'; + +export const findMinMaxByColumnId = (columnIds: string[], table: Datatable | undefined) => { + const minMax: Record = {}; + + if (table != null) { + for (const columnId of columnIds) { + const originalId = getOriginalId(columnId); + minMax[originalId] = minMax[originalId] || { max: -Infinity, min: Infinity }; + table.rows.forEach((row) => { + const rowValue = row[columnId]; + if (rowValue != null) { + if (minMax[originalId].min > rowValue) { + minMax[originalId].min = rowValue; + } + if (minMax[originalId].max < rowValue) { + minMax[originalId].max = rowValue; + } + } + }); + // what happens when there's no data in the table? Fallback to a percent range + if (minMax[originalId].max === -Infinity) { + minMax[originalId] = { max: 100, min: 0, fallback: true }; + } + } + } + return minMax; +}; 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 22577e8ef5fd31..509969c2b71ec0 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 @@ -15,6 +15,8 @@ import { LensIconChartDatatable } from '../../assets/chart_datatable'; import { DataContext, DatatableComponent } from './table_basic'; import { LensMultiTable } from '../../types'; import { DatatableProps } from '../expression'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { IUiSettingsClient } from 'kibana/public'; function sampleArgs() { const indexPatternId = 'indexPatternId'; @@ -99,6 +101,8 @@ describe('DatatableComponent', () => { formatFactory={(x) => x as IFieldFormat} dispatchEvent={onDispatchEvent} getType={jest.fn()} + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} renderMode="edit" /> ) @@ -118,6 +122,8 @@ describe('DatatableComponent', () => { getType={jest.fn()} rowHasRowClickTriggerActions={[true, true, true]} renderMode="edit" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> ) ).toMatchSnapshot(); @@ -136,6 +142,8 @@ describe('DatatableComponent', () => { getType={jest.fn()} rowHasRowClickTriggerActions={[false, false, false]} renderMode="display" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> ) ).toMatchSnapshot(); @@ -158,6 +166,8 @@ describe('DatatableComponent', () => { dispatchEvent={onDispatchEvent} getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} renderMode="edit" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> ); @@ -199,6 +209,8 @@ describe('DatatableComponent', () => { dispatchEvent={onDispatchEvent} getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} renderMode="edit" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> ); @@ -279,6 +291,8 @@ describe('DatatableComponent', () => { dispatchEvent={onDispatchEvent} getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} renderMode="edit" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> ); @@ -325,6 +339,8 @@ describe('DatatableComponent', () => { type === 'count' ? ({ type: 'metrics' } as IAggType) : ({ type: 'buckets' } as IAggType) )} renderMode="edit" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> ); expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDatatable); @@ -345,6 +361,8 @@ describe('DatatableComponent', () => { dispatchEvent={onDispatchEvent} getType={jest.fn()} renderMode="edit" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> ); @@ -393,6 +411,8 @@ describe('DatatableComponent', () => { dispatchEvent={onDispatchEvent} getType={jest.fn()} renderMode="display" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> ); @@ -421,6 +441,8 @@ describe('DatatableComponent', () => { dispatchEvent={onDispatchEvent} getType={jest.fn()} renderMode="display" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> ); @@ -447,6 +469,8 @@ describe('DatatableComponent', () => { dispatchEvent={onDispatchEvent} getType={jest.fn()} renderMode="display" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> ); @@ -471,6 +495,8 @@ describe('DatatableComponent', () => { dispatchEvent={onDispatchEvent} getType={jest.fn()} renderMode="edit" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> ); // mnake a copy of the data, changing only the name of the first column @@ -483,4 +509,34 @@ describe('DatatableComponent', () => { 'new a' ); }); + + test('it does compute minMax for each numeric column', () => { + const { data, args } = sampleArgs(); + + const wrapper = shallow( + ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + renderMode="display" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} + /> + ); + + expect(wrapper.find(DataContext.Provider).prop('value').minMaxByColumnId).toEqual({ + c: { min: 3, max: 3 }, + }); + }); }); 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 24cde07cebaa0e..e6fcf3f321f7f3 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 @@ -18,6 +18,7 @@ import { EuiDataGridSorting, EuiDataGridStyle, } from '@elastic/eui'; +import { CustomPaletteState, PaletteOutput } from 'src/plugins/charts/common'; import { FormatFactory, LensFilterEvent, LensTableRowContextMenuEvent } from '../../types'; import { VisualizationContainer } from '../../visualization_container'; import { EmptyPlaceholder } from '../../shared_components'; @@ -40,6 +41,8 @@ import { createGridSortingConfig, createTransposeColumnFilterHandler, } from './table_actions'; +import { findMinMaxByColumnId } from './shared_utils'; +import { CUSTOM_PALETTE } from '../../shared_components/coloring/constants'; export const DataContext = React.createContext({}); @@ -50,8 +53,9 @@ const gridStyle: EuiDataGridStyle = { export interface ColumnConfig { columns: Array< - ColumnState & { + Omit & { type: 'lens_datatable_column'; + palette?: PaletteOutput; } >; sortingColumnId: string | undefined; @@ -203,20 +207,34 @@ export const DatatableComponent = (props: DatatableRenderProps) => { ] ); + const isNumericMap: Record = useMemo(() => { + const numericMap: Record = {}; + for (const column of firstLocalTable.columns) { + numericMap[column.id] = column.meta.type === 'number'; + } + return numericMap; + }, [firstLocalTable]); + const alignments: Record = useMemo(() => { const alignmentMap: Record = {}; columnConfig.columns.forEach((column) => { if (column.alignment) { alignmentMap[column.columnId] = column.alignment; } else { - const isNumeric = - firstLocalTable.columns.find((dataColumn) => dataColumn.id === column.columnId)?.meta - .type === 'number'; - alignmentMap[column.columnId] = isNumeric ? 'right' : 'left'; + alignmentMap[column.columnId] = isNumericMap[column.columnId] ? 'right' : 'left'; } }); return alignmentMap; - }, [firstLocalTable, columnConfig]); + }, [columnConfig, isNumericMap]); + + const minMaxByColumnId: Record = useMemo(() => { + return findMinMaxByColumnId( + columnConfig.columns + .filter(({ columnId }) => isNumericMap[columnId]) + .map(({ columnId }) => columnId), + firstTable + ); + }, [firstTable, isNumericMap, columnConfig]); const trailingControlColumns: EuiDataGridControlColumn[] = useMemo(() => { if (!hasAtLeastOneRowClickAction || !onRowContextMenuClick) { @@ -254,7 +272,10 @@ export const DatatableComponent = (props: DatatableRenderProps) => { ]; }, [firstTableRef, onRowContextMenuClick, columnConfig, hasAtLeastOneRowClickAction]); - const renderCellValue = useMemo(() => createGridCell(formatters, DataContext), [formatters]); + const renderCellValue = useMemo( + () => createGridCell(formatters, columnConfig, DataContext, props.uiSettings), + [formatters, columnConfig, props.uiSettings] + ); const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns: () => {} }), [ visibleColumns, @@ -286,6 +307,8 @@ export const DatatableComponent = (props: DatatableRenderProps) => { table: firstLocalTable, rowHasRowClickTriggerActions: props.rowHasRowClickTriggerActions, alignments, + minMaxByColumnId, + getColorForValue: props.paletteService.get(CUSTOM_PALETTE).getColorForValue!, }} > IAggType; renderMode: RenderMode; + paletteService: PaletteRegistry; + uiSettings: IUiSettingsClient; /** * A boolean for each table row, which is true if the row active @@ -55,4 +59,10 @@ export interface DataContextType { table?: Datatable; rowHasRowClickTriggerActions?: boolean[]; alignments?: Record; + minMaxByColumnId?: Record; + getColorForValue?: ( + value: number | undefined, + state: CustomPaletteState, + minMax: { min: number; max: number } + ) => string | undefined; } diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 7d879217abf8b2..2d5f4aea988562 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -17,6 +17,9 @@ import { ExpressionFunctionDefinition, ExpressionRenderDefinition, } from 'src/plugins/expressions'; +import { CustomPaletteState, PaletteOutput } from 'src/plugins/charts/common'; +import { PaletteRegistry } from 'src/plugins/charts/public'; +import { IUiSettingsClient } from 'kibana/public'; import { getSortingCriteria } from './sorting'; import { DatatableComponent } from './components/table_basic'; @@ -26,10 +29,15 @@ import type { FormatFactory, ILensInterpreterRenderHandlers, LensMultiTable } fr import type { DatatableRender } from './components/types'; import { transposeTable } from './transpose_helpers'; +export type ColumnConfigArg = Omit & { + type: 'lens_datatable_column'; + palette?: PaletteOutput; +}; + export interface Args { title: string; description?: string; - columns: Array; + columns: ColumnConfigArg[]; sortingColumnId: string | undefined; sortingDirection: 'asc' | 'desc' | 'none'; } @@ -160,6 +168,11 @@ export const datatableColumn: ExpressionFunctionDefinition< width: { types: ['number'], help: '' }, isTransposed: { types: ['boolean'], help: '' }, transposable: { types: ['boolean'], help: '' }, + colorMode: { types: ['string'], help: '' }, + palette: { + types: ['palette'], + help: '', + }, }, fn: function fn(input: unknown, args: ColumnState) { return { @@ -172,6 +185,8 @@ export const datatableColumn: ExpressionFunctionDefinition< export const getDatatableRenderer = (dependencies: { formatFactory: FormatFactory; getType: Promise<(name: string) => IAggType>; + paletteService: PaletteRegistry; + uiSettings: IUiSettingsClient; }): ExpressionRenderDefinition => ({ name: 'lens_datatable_renderer', displayName: i18n.translate('xpack.lens.datatable.visualizationName', { @@ -222,8 +237,10 @@ export const getDatatableRenderer = (dependencies: { formatFactory={dependencies.formatFactory} dispatchEvent={handlers.event} renderMode={handlers.getRenderMode()} + paletteService={dependencies.paletteService} getType={resolvedGetType} rowHasRowClickTriggerActions={rowHasRowClickTriggerActions} + uiSettings={dependencies.uiSettings} /> , domNode, diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts index f0939f61952294..7f48d00d00f7f7 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/index.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts @@ -6,6 +6,7 @@ */ import { CoreSetup } from 'kibana/public'; +import { ChartsPluginSetup } from 'src/plugins/charts/public'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { EditorFrameSetup, FormatFactory } from '../types'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; @@ -17,6 +18,7 @@ export interface DatatableVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; formatFactory: Promise; editorFrame: EditorFrameSetup; + charts: ChartsPluginSetup; } export class DatatableVisualization { @@ -24,15 +26,16 @@ export class DatatableVisualization { setup( core: CoreSetup, - { expressions, formatFactory, editorFrame }: DatatableVisualizationPluginSetupPlugins + { expressions, formatFactory, editorFrame, charts }: DatatableVisualizationPluginSetupPlugins ) { editorFrame.registerVisualization(async () => { const { getDatatable, datatableColumn, getDatatableRenderer, - datatableVisualization, + getDatatableVisualization, } = await import('../async_services'); + const palettes = await charts.palettes.getPalettes(); const resolvedFormatFactory = await formatFactory; expressions.registerFunction(() => datatableColumn); @@ -43,9 +46,11 @@ export class DatatableVisualization { getType: core .getStartServices() .then(([_, { data: dataStart }]) => dataStart.search.aggs.types.get), + paletteService: palettes, + uiSettings: core.uiSettings, }) ); - return datatableVisualization; + return getDatatableVisualization({ paletteService: palettes }); }); } } diff --git a/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.ts b/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.ts index 6e29e018b481e2..a35edf7499073a 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.ts @@ -7,9 +7,9 @@ import type { FieldFormat } from 'src/plugins/data/public'; import type { Datatable, DatatableColumn, DatatableRow } from 'src/plugins/expressions'; +import { ColumnConfig } from './components/table_basic'; -import { Args } from './expression'; -import { ColumnState } from './visualization'; +import { Args, ColumnConfigArg } from './expression'; const TRANSPOSE_SEPARATOR = '---'; @@ -87,11 +87,11 @@ export function transposeTable( function transposeRows( firstTable: Datatable, - bucketsColumnArgs: Array, + bucketsColumnArgs: ColumnConfigArg[], formatters: Record, transposedColumnFormatter: FieldFormat, transposedColumnId: string, - metricsColumnArgs: Array + metricsColumnArgs: ColumnConfigArg[] ) { const rowsByBucketColumns: Record = groupRowsByBucketColumns( firstTable, @@ -113,8 +113,8 @@ function transposeRows( */ function updateColumnArgs( args: Args, - bucketsColumnArgs: Array, - transposedColumnGroups: Array> + bucketsColumnArgs: ColumnConfig['columns'], + transposedColumnGroups: Array ) { args.columns = [...bucketsColumnArgs]; // add first column from each group, then add second column for each group, ... @@ -151,8 +151,8 @@ function getUniqueValues(table: Datatable, formatter: FieldFormat, columnId: str */ function transposeColumns( args: Args, - bucketsColumnArgs: Array, - metricColumns: Array, + bucketsColumnArgs: ColumnConfig['columns'], + metricColumns: ColumnConfig['columns'], firstTable: Datatable, uniqueValues: string[], uniqueRawValues: unknown[], @@ -196,10 +196,10 @@ function transposeColumns( */ function mergeRowGroups( rowsByBucketColumns: Record, - bucketColumns: ColumnState[], + bucketColumns: ColumnConfigArg[], formatter: FieldFormat, transposedColumnId: string, - metricColumns: ColumnState[] + metricColumns: ColumnConfigArg[] ) { return Object.values(rowsByBucketColumns).map((rows) => { const mergedRow: DatatableRow = {}; @@ -222,7 +222,7 @@ function mergeRowGroups( */ function groupRowsByBucketColumns( firstTable: Datatable, - bucketColumns: ColumnState[], + bucketColumns: ColumnConfigArg[], formatters: Record ) { const rowsByBucketColumns: Record = {}; diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 1848565114dea5..ea8237defc2911 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -8,7 +8,7 @@ import { Ast } from '@kbn/interpreter/common'; import { buildExpression } from '../../../../../src/plugins/expressions/public'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; -import { DatatableVisualizationState, datatableVisualization } from './visualization'; +import { DatatableVisualizationState, getDatatableVisualization } from './visualization'; import { Operation, DataType, @@ -16,6 +16,7 @@ import { TableSuggestionColumn, VisualizationDimensionGroupConfig, } from '../types'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; function mockFrame(): FramePublicAPI { return { @@ -32,6 +33,10 @@ function mockFrame(): FramePublicAPI { }; } +const datatableVisualization = getDatatableVisualization({ + paletteService: chartPluginMock.createPaletteRegistry(), +}); + describe('Datatable Visualization', () => { describe('#initialize', () => { it('should initialize from the empty state', () => { @@ -427,22 +432,28 @@ describe('Datatable Visualization', () => { ); const columnArgs = buildExpression(expression).findFunction('lens_datatable_column'); expect(columnArgs).toHaveLength(2); - expect(columnArgs[0].arguments).toEqual({ - columnId: ['c'], - hidden: [], - width: [], - isTransposed: [], - transposable: [true], - alignment: [], - }); - expect(columnArgs[1].arguments).toEqual({ - columnId: ['b'], - hidden: [], - width: [], - isTransposed: [], - transposable: [true], - alignment: [], - }); + expect(columnArgs[0].arguments).toEqual( + expect.objectContaining({ + columnId: ['c'], + hidden: [], + width: [], + isTransposed: [], + transposable: [true], + alignment: [], + colorMode: ['none'], + }) + ); + expect(columnArgs[1].arguments).toEqual( + expect.objectContaining({ + columnId: ['b'], + hidden: [], + width: [], + isTransposed: [], + transposable: [true], + alignment: [], + colorMode: ['none'], + }) + ); }); it('returns no expression if the metric dimension is not defined', () => { diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 9bd482c73bff58..efde4160019e73 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -11,6 +11,7 @@ import { Ast } from '@kbn/interpreter/common'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { DatatableColumn } from 'src/plugins/expressions/public'; +import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; import { SuggestionRequest, Visualization, @@ -19,6 +20,9 @@ import { } from '../types'; import { LensIconChartDatatable } from '../assets/chart_datatable'; import { TableDimensionEditor } from './components/dimension_editor'; +import { CUSTOM_PALETTE } from '../shared_components/coloring/constants'; +import { CustomPaletteParams } from '../shared_components/coloring/types'; +import { getStopsForFixedMode } from '../shared_components'; export interface ColumnState { columnId: string; @@ -32,6 +36,8 @@ export interface ColumnState { originalName?: string; bucketValues?: Array<{ originalBucketColumn: DatatableColumn; value: unknown }>; alignment?: 'left' | 'right' | 'center'; + palette?: PaletteOutput; + colorMode?: 'none' | 'cell' | 'text'; } export interface SortingState { @@ -49,7 +55,11 @@ const visualizationLabel = i18n.translate('xpack.lens.datatable.label', { defaultMessage: 'Table', }); -export const datatableVisualization: Visualization = { +export const getDatatableVisualization = ({ + paletteService, +}: { + paletteService: PaletteRegistry; +}): Visualization => ({ id: 'lnsDatatable', visualizationTypes: [ @@ -239,10 +249,26 @@ export const datatableVisualization: Visualization layerId: state.layerId, accessors: sortedColumns .filter((c) => !datasource!.getOperationForColumnId(c)?.isBucketed) - .map((accessor) => ({ - columnId: accessor, - triggerIcon: columnMap[accessor].hidden ? 'invisible' : undefined, - })), + .map((accessor) => { + const columnConfig = columnMap[accessor]; + const hasColoring = Boolean( + columnConfig.colorMode !== 'none' && columnConfig.palette?.params?.stops + ); + return { + columnId: accessor, + triggerIcon: columnConfig.hidden + ? 'invisible' + : hasColoring + ? 'colorBy' + : undefined, + palette: hasColoring + ? getStopsForFixedMode( + columnConfig.palette?.params?.stops || [], + columnConfig.palette?.params?.colorStops + ) + : undefined, + }; + }), supportsMoreColumns: true, filterOperations: (op) => !op.isBucketed, required: true, @@ -285,7 +311,7 @@ export const datatableVisualization: Visualization renderDimensionEditor(domElement, props) { render( - + , domElement ); @@ -320,26 +346,41 @@ export const datatableVisualization: Visualization arguments: { title: [title || ''], description: [description || ''], - columns: columns.map((column) => ({ - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_datatable_column', - arguments: { - columnId: [column.columnId], - hidden: typeof column.hidden === 'undefined' ? [] : [column.hidden], - width: typeof column.width === 'undefined' ? [] : [column.width], - isTransposed: - typeof column.isTransposed === 'undefined' ? [] : [column.isTransposed], - transposable: [ - !datasource!.getOperationForColumnId(column.columnId)?.isBucketed, - ], - alignment: typeof column.alignment === 'undefined' ? [] : [column.alignment], + columns: columns.map((column) => { + const paletteParams = { + ...column.palette?.params, + // rewrite colors and stops as two distinct arguments + colors: (column.palette?.params?.stops || []).map(({ color }) => color), + stops: + column.palette?.params?.name === 'custom' + ? (column.palette?.params?.stops || []).map(({ stop }) => stop) + : [], + reverse: false, // managed at UI level + }; + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_datatable_column', + arguments: { + columnId: [column.columnId], + hidden: typeof column.hidden === 'undefined' ? [] : [column.hidden], + width: typeof column.width === 'undefined' ? [] : [column.width], + isTransposed: + typeof column.isTransposed === 'undefined' ? [] : [column.isTransposed], + transposable: [ + !datasource!.getOperationForColumnId(column.columnId)?.isBucketed, + ], + alignment: typeof column.alignment === 'undefined' ? [] : [column.alignment], + colorMode: [column.colorMode ?? 'none'], + palette: [paletteService.get(CUSTOM_PALETTE).toExpression(paletteParams)], + }, }, - }, - ], - })), + ], + }; + }), sortingColumnId: [state.sorting?.columnId || ''], sortingDirection: [state.sorting?.direction || 'none'], }, @@ -395,7 +436,7 @@ export const datatableVisualization: Visualization return state; } }, -}; +}); function getDataSourceAndSortedColumns( state: DatatableVisualizationState, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx index b8d3170b3e1650..a8d610f2740de5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx @@ -29,11 +29,13 @@ export function DimensionContainer({ groupLabel, handleClose, panel, + panelRef, }: { isOpen: boolean; handleClose: () => void; panel: React.ReactElement; groupLabel: string; + panelRef: (el: HTMLDivElement) => void; }) { const [focusTrapIsEnabled, setFocusTrapIsEnabled] = useState(false); @@ -73,65 +75,67 @@ export function DimensionContainer({ }); return isOpen ? ( - - - -
- - - - - - - -

- - {i18n.translate('xpack.lens.configure.configurePanelTitle', { - defaultMessage: '{groupLabel} configuration', - values: { - groupLabel, - }, - })} - -

-
-
-
-
- - {panel} - - - - {i18n.translate('xpack.lens.dimensionContainer.close', { - defaultMessage: 'Close', - })} - - -
-
-
+
+ + + +
+ + + + +

+ + {i18n.translate('xpack.lens.configure.configurePanelTitle', { + defaultMessage: '{groupLabel} configuration', + values: { + groupLabel, + }, + })} + +

+
+
+ + + +
+
+ + {panel} + + + + {i18n.translate('xpack.lens.dimensionContainer.close', { + defaultMessage: 'Close', + })} + + +
+
+
+
) : null; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts index dd95770655d1a6..7d8a373192ee57 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import _ from 'lodash'; +import { mapValues } from 'lodash'; import { EditorFrameState } from '../state_management'; import { Datasource, Visualization } from '../../../types'; @@ -35,7 +35,7 @@ export function removeLayer(opts: RemoveLayerOptions): EditorFrameState { return { ...state, - datasourceStates: _.mapValues(state.datasourceStates, (datasourceState, datasourceId) => { + datasourceStates: mapValues(state.datasourceStates, (datasourceState, datasourceId) => { const datasource = datasourceMap[datasourceId!]; return { ...datasourceState, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index cf3c9099d4b0dd..a605a94a346468 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -7,7 +7,7 @@ import './layer_panel.scss'; -import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { EuiPanel, EuiSpacer, @@ -72,6 +72,7 @@ export function LayerPanel( setActiveDimension(initialActiveDimensionState); }, [activeVisualization.id]); + const panelRef = useRef(null); const registerLayerRef = useCallback((el) => registerNewLayerRef(layerId, el), [ layerId, registerNewLayerRef, @@ -405,6 +406,7 @@ export function LayerPanel( (panelRef.current = el)} isOpen={!!activeId} groupLabel={activeGroup?.groupLabel || ''} handleClose={() => { @@ -484,6 +486,7 @@ export function LayerPanel( groupId: activeGroup.groupId, accessor: activeId, setState: props.updateVisualization, + panelRef, }} />
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index f23e4c74e1a8be..351b4009240ebb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -7,6 +7,7 @@ import React, { ReactElement } from 'react'; import { ReactWrapper } from 'enzyme'; +import { setState, LensRootStore } from '../../state_management/index'; // Tests are executed in a jsdom environment who does not have sizing methods, // thus the AutoSizer will always compute a 0x0 size space @@ -28,8 +29,7 @@ jest.mock('react-virtualized-auto-sizer', () => { }); import { EuiPanel, EuiToolTip } from '@elastic/eui'; -import { mountWithIntl as mount } from '@kbn/test/jest'; -import { EditorFrame } from './editor_frame'; +import { EditorFrame, EditorFrameProps } from './editor_frame'; import { DatasourcePublicAPI, DatasourceSuggestion, Visualization } from '../../types'; import { act } from 'react-dom/test-utils'; import { coreMock } from 'src/core/public/mocks'; @@ -44,9 +44,9 @@ import { ReactExpressionRendererType } from 'src/plugins/expressions/public'; import { DragDrop } from '../../drag_drop'; import { FrameLayout } from './frame_layout'; import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks'; -import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks'; +import { mockDataPlugin, mountWithProvider } from '../../mocks'; function generateSuggestion(state = {}): DatasourceSuggestion { return { @@ -62,7 +62,7 @@ function generateSuggestion(state = {}): DatasourceSuggestion { } function getDefaultProps() { - return { + const defaultProps = { store: { save: jest.fn(), load: jest.fn(), @@ -72,18 +72,17 @@ function getDefaultProps() { onChange: jest.fn(), dateRange: { fromDate: '', toDate: '' }, query: { query: '', language: 'lucene' }, - filters: [], core: coreMock.createStart(), plugins: { uiActions: uiActionsPluginMock.createStartContract(), - data: dataPluginMock.createStartContract(), + data: mockDataPlugin(), expressions: expressionsPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), }, palettes: chartPluginMock.createPaletteRegistry(), showNoDataPopover: jest.fn(), - searchSessionId: 'sessionId', }; + return defaultProps; } describe('editor_frame', () => { @@ -133,85 +132,57 @@ describe('editor_frame', () => { describe('initialization', () => { it('should initialize initial datasource', async () => { mockVisualization.getLayerIds.mockReturnValue([]); - await act(async () => { - mount( - - ); - }); - - expect(mockDatasource.initialize).toHaveBeenCalled(); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, - it('should not initialize datasource and visualization if no initial one is specificed', () => { - act(() => { - mount( - - ); - }); + ExpressionRenderer: expressionRendererMock, + }; - expect(mockVisualization.initialize).not.toHaveBeenCalled(); - expect(mockDatasource.initialize).not.toHaveBeenCalled(); + await mountWithProvider(, props.plugins.data); + expect(mockDatasource.initialize).toHaveBeenCalled(); }); it('should initialize all datasources with state from doc', async () => { const mockDatasource3 = createMockDatasource('testDatasource3'); const datasource1State = { datasource1: '' }; const datasource2State = { datasource2: '' }; + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + testDatasource3: mockDatasource3, + }, - await act(async () => { - mount( - - ); + ExpressionRenderer: expressionRendererMock, + }; + + await mountWithProvider(, props.plugins.data, { + persistedDoc: { + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { + testDatasource: datasource1State, + testDatasource2: datasource2State, + }, + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }, }); + expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State, [], undefined, { isFullEditor: true, }); @@ -222,42 +193,40 @@ describe('editor_frame', () => { }); it('should not render something before all datasources are initialized', async () => { + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await act(async () => { - mount( - - ); + mountWithProvider(, props.plugins.data); expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled(); }); expect(mockDatasource.renderDataPanel).toHaveBeenCalled(); }); it('should not initialize visualization before datasource is initialized', async () => { + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await act(async () => { - mount( - - ); + mountWithProvider(, props.plugins.data); expect(mockVisualization.initialize).not.toHaveBeenCalled(); }); @@ -265,23 +234,19 @@ describe('editor_frame', () => { }); it('should pass the public frame api into visualization initialize', async () => { - const defaultProps = getDefaultProps(); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + }; await act(async () => { - mount( - - ); + mountWithProvider(, props.plugins.data); expect(mockVisualization.initialize).not.toHaveBeenCalled(); }); @@ -291,33 +256,43 @@ describe('editor_frame', () => { removeLayers: expect.any(Function), query: { query: '', language: 'lucene' }, filters: [], - dateRange: { fromDate: 'now-7d', toDate: 'now' }, - availablePalettes: defaultProps.palettes, - searchSessionId: 'sessionId', + dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, + availablePalettes: props.palettes, + searchSessionId: 'sessionId-1', }); }); it('should add new layer on active datasource on frame api call', async () => { const initialState = { datasource2: '' }; mockDatasource2.initialize.mockReturnValue(Promise.resolve(initialState)); - await act(async () => { - mount( - , props.plugins.data, { + persistedDoc: { + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { testDatasource2: mockDatasource2, - }} - initialDatasourceId="testDatasource2" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); + }, + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }, }); - act(() => { mockVisualization.initialize.mock.calls[0][0].addNewLayer(); }); @@ -332,22 +307,33 @@ describe('editor_frame', () => { mockDatasource2.getLayers.mockReturnValue(['abc', 'def']); mockDatasource2.removeLayer.mockReturnValue({ removed: true }); mockVisualization.getLayerIds.mockReturnValue(['first', 'abc', 'def']); - await act(async () => { - mount( - , props.plugins.data, { + persistedDoc: { + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { testDatasource2: mockDatasource2, - }} - initialDatasourceId="testDatasource2" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); + }, + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }, }); act(() => { @@ -362,28 +348,26 @@ describe('editor_frame', () => { const initialState = {}; let databaseInitialized: ({}) => void; - await act(async () => { - mount( - - new Promise((resolve) => { - databaseInitialized = resolve; - }), - }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + initialize: () => + new Promise((resolve) => { + databaseInitialized = resolve; + }), + }, + }, + + ExpressionRenderer: expressionRendererMock, + }; + + await mountWithProvider(, props.plugins.data); + await act(async () => { databaseInitialized!(initialState); }); @@ -397,25 +381,22 @@ describe('editor_frame', () => { const initialState = {}; mockDatasource.getLayers.mockReturnValue(['first']); - await act(async () => { - mount( - initialState }, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - initialize: () => Promise.resolve(), - }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { ...mockVisualization, initialize: () => initialState }, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + initialize: () => Promise.resolve(), + }, + }, + + ExpressionRenderer: expressionRendererMock, + }; + + await mountWithProvider(, props.plugins.data); expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: initialState }) @@ -427,25 +408,21 @@ describe('editor_frame', () => { it('should render the resulting expression using the expression renderer', async () => { mockDatasource.getLayers.mockReturnValue(['first']); - await act(async () => { - instance = mount( - 'vis' }, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - toExpression: () => 'datasource', - }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { ...mockVisualization, toExpression: () => 'vis' }, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + toExpression: () => 'datasource', + }, + }, + + ExpressionRenderer: expressionRendererMock, + }; + instance = (await mountWithProvider(, props.plugins.data)).instance; instance.update(); @@ -466,37 +443,34 @@ describe('editor_frame', () => { ); mockDatasource2.getLayers.mockReturnValue(['second', 'third']); - await act(async () => { - instance = mount( - 'vis' }, - }} - datasourceMap={{ - testDatasource: mockDatasource, - testDatasource2: mockDatasource2, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - doc={{ - visualizationType: 'testVis', - title: '', - state: { - datasourceStates: { - testDatasource: {}, - testDatasource2: {}, - }, - visualization: {}, - query: { query: '', language: 'lucene' }, - filters: [], + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { ...mockVisualization, toExpression: () => 'vis' }, + }, + datasourceMap: { testDatasource: mockDatasource, testDatasource2: mockDatasource2 }, + + ExpressionRenderer: expressionRendererMock, + }; + + instance = ( + await mountWithProvider(, props.plugins.data, { + persistedDoc: { + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { + testDatasource: {}, + testDatasource2: {}, }, - references: [], - }} - /> - ); - }); + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }, + }) + ).instance; instance.update(); @@ -577,23 +551,18 @@ describe('editor_frame', () => { describe('state update', () => { it('should re-render config panel after state update', async () => { mockDatasource.getLayers.mockReturnValue(['first']); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, - await act(async () => { - mount( - - ); - }); + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); const updatedState = {}; const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1] .setState; @@ -601,8 +570,9 @@ describe('editor_frame', () => { setDatasourceState(updatedState); }); + // TODO: temporary regression // validation requires to calls this getConfiguration API - expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(7); + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(9); expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ state: updatedState, @@ -613,22 +583,18 @@ describe('editor_frame', () => { it('should re-render data panel after state update', async () => { mockDatasource.getLayers.mockReturnValue(['first']); - await act(async () => { - mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1] .setState; @@ -653,23 +619,18 @@ describe('editor_frame', () => { it('should re-render config panel with updated datasource api after datasource state update', async () => { mockDatasource.getLayers.mockReturnValue(['first']); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, - await act(async () => { - mount( - - ); - }); + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); const updatedPublicAPI: DatasourcePublicAPI = { datasourceId: 'testDatasource', @@ -684,8 +645,9 @@ describe('editor_frame', () => { setDatasourceState({}); }); + // TODO: temporary regression, selectors will help // validation requires to calls this getConfiguration API - expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(7); + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(9); expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ frame: expect.objectContaining({ @@ -703,37 +665,33 @@ describe('editor_frame', () => { mockDatasource.getLayers.mockReturnValue(['first']); mockDatasource2.getLayers.mockReturnValue(['second', 'third']); mockVisualization.getLayerIds.mockReturnValue(['first', 'second', 'third']); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, - await act(async () => { - mount( - - ); + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data, { + persistedDoc: { + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { + testDatasource: {}, + testDatasource2: {}, + }, + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }, }); expect(mockVisualization.getConfiguration).toHaveBeenCalled(); @@ -756,36 +714,33 @@ describe('editor_frame', () => { const datasource1State = { datasource1: '' }; const datasource2State = { datasource2: '' }; - await act(async () => { - mount( - - ); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data, { + persistedDoc: { + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { + testDatasource: datasource1State, + testDatasource2: datasource2State, + }, + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }, }); expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith( @@ -813,22 +768,18 @@ describe('editor_frame', () => { mockDatasource.initialize.mockResolvedValue(datasourceState); mockDatasource.getLayers.mockReturnValue(['first']); - await act(async () => { - mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({ state: datasourceState, @@ -870,24 +821,20 @@ describe('editor_frame', () => { }, ]); - await act(async () => { - instance = mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + testVis2: mockVisualization2, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + instance = (await mountWithProvider(, props.plugins.data)).instance; // necessary to flush elements to dom synchronously instance.update(); @@ -984,49 +931,41 @@ describe('editor_frame', () => { describe('suggestions', () => { it('should fetch suggestions of currently active datasource when initializes from visualization trigger', async () => { - await act(async () => { - mount( - - ); - }); + const props = { + ...getDefaultProps(), + initialContext: { + indexPatternId: '1', + fieldName: 'test', + }, + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); expect(mockDatasource.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalled(); }); it('should fetch suggestions of currently active datasource', async () => { - await act(async () => { - mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); expect(mockDatasource.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled(); expect(mockDatasource2.getDatasourceSuggestionsFromCurrentState).not.toHaveBeenCalled(); @@ -1046,24 +985,20 @@ describe('editor_frame', () => { }, ]); - await act(async () => { - mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + testVis2: mockVisualization2, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); expect(mockVisualization.getSuggestions).toHaveBeenCalled(); expect(mockVisualization2.getSuggestions).toHaveBeenCalled(); @@ -1072,71 +1007,66 @@ describe('editor_frame', () => { let instance: ReactWrapper; it('should display top 5 suggestions in descending order', async () => { mockDatasource.getLayers.mockReturnValue(['first']); - - await act(async () => { - instance = mount( - [ - { - score: 0.1, - state: {}, - title: 'Suggestion6', - previewIcon: 'empty', - }, - { - score: 0.5, - state: {}, - title: 'Suggestion3', - previewIcon: 'empty', - }, - { - score: 0.7, - state: {}, - title: 'Suggestion2', - previewIcon: 'empty', - }, - { - score: 0.8, - state: {}, - title: 'Suggestion1', - previewIcon: 'empty', - }, - ], + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.1, + state: {}, + title: 'Suggestion6', + previewIcon: 'empty', }, - testVis2: { - ...mockVisualization, - getSuggestions: () => [ - { - score: 0.4, - state: {}, - title: 'Suggestion5', - previewIcon: 'empty', - }, - { - score: 0.45, - state: {}, - title: 'Suggestion4', - previewIcon: 'empty', - }, - ], + { + score: 0.5, + state: {}, + title: 'Suggestion3', + previewIcon: 'empty', }, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + { + score: 0.7, + state: {}, + title: 'Suggestion2', + previewIcon: 'empty', }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + { + score: 0.8, + state: {}, + title: 'Suggestion1', + previewIcon: 'empty', + }, + ], + }, + testVis2: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.4, + state: {}, + title: 'Suggestion5', + previewIcon: 'empty', + }, + { + score: 0.45, + state: {}, + title: 'Suggestion4', + previewIcon: 'empty', + }, + ], + }, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + }, + }, + + ExpressionRenderer: expressionRendererMock, + }; + instance = (await mountWithProvider(, props.plugins.data)).instance; // TODO why is this necessary? instance.update(); @@ -1159,37 +1089,32 @@ describe('editor_frame', () => { mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']); const newDatasourceState = {}; const suggestionVisState = {}; - - await act(async () => { - instance = mount( - [ - { - score: 0.8, - state: suggestionVisState, - title: 'Suggestion1', - previewIcon: 'empty', - }, - ], - }, - testVis2: mockVisualization2, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.8, + state: suggestionVisState, + title: 'Suggestion1', + previewIcon: 'empty', }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis2" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + ], + }, + testVis2: mockVisualization2, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + }, + }, + + ExpressionRenderer: expressionRendererMock, + }; + instance = (await mountWithProvider(, props.plugins.data)).instance; // TODO why is this necessary? instance.update(); @@ -1199,7 +1124,8 @@ describe('editor_frame', () => { }); // validation requires to calls this getConfiguration API - expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(5); + // TODO: why so many times? + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(10); expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, @@ -1216,45 +1142,40 @@ describe('editor_frame', () => { it('should switch to best suggested visualization on field drop', async () => { mockDatasource.getLayers.mockReturnValue(['first']); const suggestionVisState = {}; - - await act(async () => { - instance = mount( - [ - { - score: 0.2, - state: {}, - title: 'Suggestion1', - previewIcon: 'empty', - }, - { - score: 0.8, - state: suggestionVisState, - title: 'Suggestion2', - previewIcon: 'empty', - }, - ], + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.2, + state: {}, + title: 'Suggestion1', + previewIcon: 'empty', }, - testVis2: mockVisualization2, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsForField: () => [generateSuggestion()], - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], - getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], + { + score: 0.8, + state: suggestionVisState, + title: 'Suggestion2', + previewIcon: 'empty', }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + ], + }, + testVis2: mockVisualization2, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + getDatasourceSuggestionsForField: () => [generateSuggestion()], + getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], + }, + }, + + ExpressionRenderer: expressionRendererMock, + }; + instance = (await mountWithProvider(, props.plugins.data)).instance; // TODO why is this necessary? instance.update(); @@ -1274,63 +1195,58 @@ describe('editor_frame', () => { mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']); const suggestionVisState = {}; - await act(async () => { - instance = mount( - [ - { - score: 0.2, - state: {}, - title: 'Suggestion1', - previewIcon: 'empty', - }, - { - score: 0.6, - state: {}, - title: 'Suggestion2', - previewIcon: 'empty', - }, - ], + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.2, + state: {}, + title: 'Suggestion1', + previewIcon: 'empty', }, - testVis2: { - ...mockVisualization2, - getSuggestions: () => [ - { - score: 0.8, - state: suggestionVisState, - title: 'Suggestion3', - previewIcon: 'empty', - }, - ], + { + score: 0.6, + state: {}, + title: 'Suggestion2', + previewIcon: 'empty', }, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsForField: () => [generateSuggestion()], - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], - getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], - renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { - if (!dragging || dragging.id !== 'draggedField') { - setDragging({ - id: 'draggedField', - humanData: { label: 'draggedField' }, - }); - } - }, + ], + }, + testVis2: { + ...mockVisualization2, + getSuggestions: () => [ + { + score: 0.8, + state: suggestionVisState, + title: 'Suggestion3', + previewIcon: 'empty', }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis2" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + ], + }, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + getDatasourceSuggestionsForField: () => [generateSuggestion()], + getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], + renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { + if (!dragging || dragging.id !== 'draggedField') { + setDragging({ + id: 'draggedField', + humanData: { label: 'draggedField' }, + }); + } + }, + }, + }, + ExpressionRenderer: expressionRendererMock, + } as EditorFrameProps; + instance = (await mountWithProvider(, props.plugins.data)).instance; // TODO why is this necessary? instance.update(); @@ -1384,58 +1300,55 @@ describe('editor_frame', () => { ], }; - await act(async () => { - instance = mount( - [ - { - score: 0.2, - state: {}, - title: 'Suggestion1', - previewIcon: 'empty', - }, - { - score: 0.6, - state: {}, - title: 'Suggestion2', - previewIcon: 'empty', - }, - ], - }, - testVis2: { - ...mockVisualization2, - getSuggestions: () => [], + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.2, + state: {}, + title: 'Suggestion1', + previewIcon: 'empty', }, - testVis3: { - ...mockVisualization3, + { + score: 0.6, + state: {}, + title: 'Suggestion2', + previewIcon: 'empty', }, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsForField: () => [generateSuggestion()], - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], - getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], - renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { - if (!dragging || dragging.id !== 'draggedField') { - setDragging({ - id: 'draggedField', - humanData: { label: '1' }, - }); - } - }, - }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis2" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + ], + }, + testVis2: { + ...mockVisualization2, + getSuggestions: () => [], + }, + testVis3: { + ...mockVisualization3, + }, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + getDatasourceSuggestionsForField: () => [generateSuggestion()], + getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], + renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { + if (!dragging || dragging.id !== 'draggedField') { + setDragging({ + id: 'draggedField', + humanData: { label: '1' }, + }); + } + }, + }, + }, + + ExpressionRenderer: expressionRendererMock, + } as EditorFrameProps; + + instance = (await mountWithProvider(, props.plugins.data)).instance; // TODO why is this necessary? instance.update(); @@ -1481,74 +1394,79 @@ describe('editor_frame', () => { })); mockVisualization.initialize.mockReturnValue({ initialState: true }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + onChange, + }; + + let lensStore: LensRootStore = {} as LensRootStore; await act(async () => { - mount( - - ); - expect(onChange).toHaveBeenCalledTimes(0); + const mounted = await mountWithProvider(, props.plugins.data); + lensStore = mounted.lensStore; + expect(lensStore.dispatch).toHaveBeenCalledTimes(0); resolver({}); }); - expect(onChange).toHaveBeenCalledTimes(2); - expect(onChange).toHaveBeenNthCalledWith(1, { - filterableIndexPatterns: ['1'], - doc: { - id: undefined, - description: undefined, - references: [ - { - id: '1', - name: 'index-pattern-0', - type: 'index-pattern', + expect(lensStore.dispatch).toHaveBeenCalledTimes(2); + expect(lensStore.dispatch).toHaveBeenNthCalledWith(1, { + payload: { + indexPatternsForTopNav: [{ id: '1' }], + lastKnownDoc: { + savedObjectId: undefined, + description: undefined, + references: [ + { + id: '1', + name: 'index-pattern-0', + type: 'index-pattern', + }, + ], + state: { + visualization: null, // Not yet loaded + datasourceStates: { testDatasource: {} }, + query: { query: '', language: 'lucene' }, + filters: [], }, - ], - state: { - visualization: null, // Not yet loaded - datasourceStates: { testDatasource: {} }, - query: { query: '', language: 'lucene' }, - filters: [], + title: '', + type: 'lens', + visualizationType: 'testVis', }, - title: '', - type: 'lens', - visualizationType: 'testVis', }, - isSaveable: false, + type: 'app/onChangeFromEditorFrame', }); - expect(onChange).toHaveBeenLastCalledWith({ - filterableIndexPatterns: ['1'], - doc: { - references: [ - { - id: '1', - name: 'index-pattern-0', - type: 'index-pattern', + expect(lensStore.dispatch).toHaveBeenLastCalledWith({ + payload: { + indexPatternsForTopNav: [{ id: '1' }], + lastKnownDoc: { + references: [ + { + id: '1', + name: 'index-pattern-0', + type: 'index-pattern', + }, + ], + description: undefined, + savedObjectId: undefined, + state: { + visualization: { initialState: true }, // Now loaded + datasourceStates: { testDatasource: {} }, + query: { query: '', language: 'lucene' }, + filters: [], }, - ], - description: undefined, - id: undefined, - state: { - visualization: { initialState: true }, // Now loaded - datasourceStates: { testDatasource: {} }, - query: { query: '', language: 'lucene' }, - filters: [], + title: '', + type: 'lens', + visualizationType: 'testVis', }, - title: '', - type: 'lens', - visualizationType: 'testVis', }, - isSaveable: false, + type: 'app/onChangeFromEditorFrame', }); }); @@ -1561,48 +1479,63 @@ describe('editor_frame', () => { mockDatasource.getLayers.mockReturnValue(['first']); mockVisualization.initialize.mockReturnValue({ initialState: true }); - await act(async () => { - instance = mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + onChange, + }; - expect(onChange).toHaveBeenCalledTimes(2); + const { instance: el, lensStore } = await mountWithProvider( + , + props.plugins.data + ); + instance = el; + + expect(lensStore.dispatch).toHaveBeenCalledTimes(2); mockDatasource.toExpression.mockReturnValue('data expression'); mockVisualization.toExpression.mockReturnValue('vis expression'); - instance.setProps({ query: { query: 'new query', language: 'lucene' } }); + await act(async () => { + lensStore.dispatch(setState({ query: { query: 'new query', language: 'lucene' } })); + }); + instance.update(); - expect(onChange).toHaveBeenCalledTimes(3); - expect(onChange).toHaveBeenNthCalledWith(3, { - filterableIndexPatterns: [], - doc: { - id: undefined, - references: [], - state: { - datasourceStates: { testDatasource: { datasource: '' } }, - visualization: { initialState: true }, - query: { query: 'new query', language: 'lucene' }, - filters: [], + expect(lensStore.dispatch).toHaveBeenCalledTimes(4); + expect(lensStore.dispatch).toHaveBeenNthCalledWith(3, { + payload: { + query: { + language: 'lucene', + query: 'new query', }, - title: '', - type: 'lens', - visualizationType: 'testVis', }, - isSaveable: true, + type: 'app/setState', + }); + expect(lensStore.dispatch).toHaveBeenNthCalledWith(4, { + payload: { + lastKnownDoc: { + savedObjectId: undefined, + references: [], + state: { + datasourceStates: { testDatasource: { datasource: '' } }, + visualization: { initialState: true }, + query: { query: 'new query', language: 'lucene' }, + filters: [], + }, + title: '', + type: 'lens', + visualizationType: 'testVis', + }, + isSaveable: true, + }, + type: 'app/onChangeFromEditorFrame', }); }); @@ -1617,21 +1550,23 @@ describe('editor_frame', () => { })); mockVisualization.initialize.mockReturnValue({ initialState: true }); - await act(async () => { - instance = mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + onChange, + }; + const mounted = await mountWithProvider(, props.plugins.data); + instance = mounted.instance; + const { lensStore } = mounted; - expect(onChange).toHaveBeenCalledTimes(2); + expect(lensStore.dispatch).toHaveBeenCalledTimes(2); await act(async () => { (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({ @@ -1643,7 +1578,7 @@ describe('editor_frame', () => { }); }); - expect(onChange).toHaveBeenCalledTimes(3); + expect(lensStore.dispatch).toHaveBeenCalledTimes(3); }); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 91b59664ada838..4710e03d336bcf 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -7,7 +7,10 @@ import React, { useEffect, useReducer, useState, useCallback } from 'react'; import { CoreStart } from 'kibana/public'; +import { isEqual } from 'lodash'; import { PaletteRegistry } from 'src/plugins/charts/public'; +import { IndexPattern } from '../../../../../../src/plugins/data/public'; +import { getAllIndexPatterns } from '../../utils'; import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { Datasource, FramePublicAPI, Visualization } from '../../types'; import { reducer, getInitialState } from './state_management'; @@ -20,7 +23,6 @@ import { Document } from '../../persistence/saved_object_store'; import { DragDropIdentifier, RootDragDropProvider } from '../../drag_drop'; import { getSavedObjectFormat } from './save'; import { generateId } from '../../id_generator'; -import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public'; import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public'; import { EditorFrameStartPlugins } from '../service'; import { initializeDatasources, createDatasourceLayers } from './state_helpers'; @@ -30,37 +32,45 @@ import { switchToSuggestion, } from './suggestion_helpers'; import { trackUiEvent } from '../../lens_ui_telemetry'; +import { + useLensSelector, + useLensDispatch, + LensAppState, + DispatchSetState, + onChangeFromEditorFrame, +} from '../../state_management'; export interface EditorFrameProps { - doc?: Document; datasourceMap: Record; visualizationMap: Record; - initialDatasourceId: string | null; - initialVisualizationId: string | null; ExpressionRenderer: ReactExpressionRendererType; palettes: PaletteRegistry; onError: (e: { message: string }) => void; core: CoreStart; plugins: EditorFrameStartPlugins; - dateRange: { - fromDate: string; - toDate: string; - }; - query: Query; - filters: Filter[]; - savedQuery?: SavedQuery; - searchSessionId: string; - onChange: (arg: { - filterableIndexPatterns: string[]; - doc: Document; - isSaveable: boolean; - }) => void; showNoDataPopover: () => void; initialContext?: VisualizeFieldContext; } export function EditorFrame(props: EditorFrameProps) { - const [state, dispatch] = useReducer(reducer, props, getInitialState); + const { + filters, + searchSessionId, + savedQuery, + query, + persistedDoc, + indexPatternsForTopNav, + lastKnownDoc, + activeData, + isSaveable, + resolvedDateRange: dateRange, + } = useLensSelector((state) => state.app); + const [state, dispatch] = useReducer(reducer, { ...props, doc: persistedDoc }, getInitialState); + const dispatchLens = useLensDispatch(); + const dispatchChange: DispatchSetState = useCallback( + (s: Partial) => dispatchLens(onChangeFromEditorFrame(s)), + [dispatchLens] + ); const [visualizeTriggerFieldContext, setVisualizeTriggerFieldContext] = useState( props.initialContext ); @@ -81,7 +91,7 @@ export function EditorFrame(props: EditorFrameProps) { initializeDatasources( props.datasourceMap, state.datasourceStates, - props.doc?.references, + persistedDoc?.references, visualizeTriggerFieldContext, { isFullEditor: true } ) @@ -109,11 +119,11 @@ export function EditorFrame(props: EditorFrameProps) { const framePublicAPI: FramePublicAPI = { datasourceLayers, - activeData: state.activeData, - dateRange: props.dateRange, - query: props.query, - filters: props.filters, - searchSessionId: props.searchSessionId, + activeData, + dateRange, + query, + filters, + searchSessionId, availablePalettes: props.palettes, addNewLayer() { @@ -160,19 +170,19 @@ export function EditorFrame(props: EditorFrameProps) { useEffect( () => { - if (props.doc) { + if (persistedDoc) { dispatch({ type: 'VISUALIZATION_LOADED', doc: { - ...props.doc, + ...persistedDoc, state: { - ...props.doc.state, - visualization: props.doc.visualizationType - ? props.visualizationMap[props.doc.visualizationType].initialize( + ...persistedDoc.state, + visualization: persistedDoc.visualizationType + ? props.visualizationMap[persistedDoc.visualizationType].initialize( framePublicAPI, - props.doc.state.visualization + persistedDoc.state.visualization ) - : props.doc.state.visualization, + : persistedDoc.state.visualization, }, }, }); @@ -184,7 +194,7 @@ export function EditorFrame(props: EditorFrameProps) { } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [props.doc] + [persistedDoc] ); // Initialize visualization as soon as all datasources are ready @@ -205,7 +215,7 @@ export function EditorFrame(props: EditorFrameProps) { // Get suggestions for visualize field when all datasources are ready useEffect(() => { - if (allLoaded && visualizeTriggerFieldContext && !props.doc) { + if (allLoaded && visualizeTriggerFieldContext && !persistedDoc) { applyVisualizeFieldSuggestions({ datasourceMap: props.datasourceMap, datasourceStates: state.datasourceStates, @@ -220,6 +230,51 @@ export function EditorFrame(props: EditorFrameProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [allLoaded]); + const getStateToUpdate: ( + arg: { + filterableIndexPatterns: string[]; + doc: Document; + isSaveable: boolean; + }, + oldState: { + isSaveable: boolean; + indexPatternsForTopNav: IndexPattern[]; + persistedDoc?: Document; + lastKnownDoc?: Document; + } + ) => Promise | undefined> = async ( + { filterableIndexPatterns, doc, isSaveable: incomingIsSaveable }, + prevState + ) => { + const batchedStateToUpdate: Partial = {}; + + if (incomingIsSaveable !== prevState.isSaveable) { + batchedStateToUpdate.isSaveable = incomingIsSaveable; + } + + if (!isEqual(prevState.persistedDoc, doc) && !isEqual(prevState.lastKnownDoc, doc)) { + batchedStateToUpdate.lastKnownDoc = doc; + } + const hasIndexPatternsChanged = + prevState.indexPatternsForTopNav.length !== filterableIndexPatterns.length || + filterableIndexPatterns.some( + (id) => !prevState.indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id) + ); + // Update the cached index patterns if the user made a change to any of them + if (hasIndexPatternsChanged) { + const { indexPatterns } = await getAllIndexPatterns( + filterableIndexPatterns, + props.plugins.data.indexPatterns + ); + if (indexPatterns) { + batchedStateToUpdate.indexPatternsForTopNav = indexPatterns; + } + } + if (Object.keys(batchedStateToUpdate).length) { + return batchedStateToUpdate; + } + }; + // The frame needs to call onChange every time its internal state changes useEffect( () => { @@ -232,31 +287,43 @@ export function EditorFrame(props: EditorFrameProps) { return; } - props.onChange( - getSavedObjectFormat({ - activeDatasources: Object.keys(state.datasourceStates).reduce( - (datasourceMap, datasourceId) => ({ - ...datasourceMap, - [datasourceId]: props.datasourceMap[datasourceId], - }), - {} - ), - visualization: activeVisualization, - state, - framePublicAPI, - }) - ); + const savedObjectFormat = getSavedObjectFormat({ + activeDatasources: Object.keys(state.datasourceStates).reduce( + (datasourceMap, datasourceId) => ({ + ...datasourceMap, + [datasourceId]: props.datasourceMap[datasourceId], + }), + {} + ), + visualization: activeVisualization, + state, + framePublicAPI, + }); + + // Frame loader (app or embeddable) is expected to call this when it loads and updates + // This should be replaced with a top-down state + getStateToUpdate(savedObjectFormat, { + isSaveable, + persistedDoc, + indexPatternsForTopNav, + lastKnownDoc, + }).then((batchedStateToUpdate) => { + if (batchedStateToUpdate) { + dispatchChange(batchedStateToUpdate); + } + }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [ activeVisualization, state.datasourceStates, state.visualization, - state.activeData, - props.query, - props.filters, - props.savedQuery, + activeData, + query, + filters, + savedQuery, state.title, + dispatchChange, ] ); @@ -326,9 +393,9 @@ export function EditorFrame(props: EditorFrameProps) { } dispatch={dispatch} core={props.core} - query={props.query} - dateRange={props.dateRange} - filters={props.filters} + query={query} + dateRange={dateRange} + filters={filters} showNoDataPopover={props.showNoDataPopover} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts index 6eec13dd9d7ce0..86a28be65d2b9f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts @@ -5,9 +5,8 @@ * 2.0. */ -import _ from 'lodash'; +import { uniq } from 'lodash'; import { SavedObjectReference } from 'kibana/public'; -import { Datatable } from 'src/plugins/expressions'; import { EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; import { Datasource, Visualization, FramePublicAPI } from '../../types'; @@ -30,7 +29,6 @@ export function getSavedObjectFormat({ doc: Document; filterableIndexPatterns: string[]; isSaveable: boolean; - activeData: Record | undefined; } { const datasourceStates: Record = {}; const references: SavedObjectReference[] = []; @@ -42,7 +40,7 @@ export function getSavedObjectFormat({ references.push(...savedObjectReferences); }); - const uniqueFilterableIndexPatternIds = _.uniq( + const uniqueFilterableIndexPatternIds = uniq( references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) ); @@ -77,6 +75,5 @@ export function getSavedObjectFormat({ }, filterableIndexPatterns: uniqueFilterableIndexPatternIds, isSaveable: expression !== null, - activeData: state.activeData, }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts index 5d6dae557dbb8e..af8a9c0a855588 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts @@ -24,10 +24,7 @@ describe('editor_frame state management', () => { onError: jest.fn(), datasourceMap: { testDatasource: ({} as unknown) as Datasource }, visualizationMap: { testVis: ({ initialize: jest.fn() } as unknown) as Visualization }, - initialDatasourceId: 'testDatasource', - initialVisualizationId: 'testVis', ExpressionRenderer: createExpressionRendererMock(), - onChange: jest.fn(), core: coreMock.createStart(), plugins: { uiActions: uiActionsPluginMock.createStartContract(), @@ -36,11 +33,7 @@ describe('editor_frame state management', () => { charts: chartPluginMock.createStartContract(), }, palettes: chartPluginMock.createPaletteRegistry(), - dateRange: { fromDate: 'now-7d', toDate: 'now' }, - query: { query: '', language: 'lucene' }, - filters: [], showNoDataPopover: jest.fn(), - searchSessionId: 'sessionId', }; }); @@ -101,8 +94,8 @@ describe('editor_frame state management', () => { `); }); - it('should not set active id if no initial visualization is passed in', () => { - const initialState = getInitialState({ ...props, initialVisualizationId: null }); + it('should not set active id if initiated with empty document and visualizationMap is empty', () => { + const initialState = getInitialState({ ...props, visualizationMap: {} }); expect(initialState.visualization.state).toEqual(null); expect(initialState.visualization.activeId).toEqual(null); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts index 53aba0d6f3f6c1..aa365d1e66d6c5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts @@ -7,7 +7,6 @@ import { EditorFrameProps } from './index'; import { Document } from '../../persistence/saved_object_store'; -import { TableInspectorAdapter } from '../types'; export interface PreviewState { visualization: { @@ -23,7 +22,6 @@ export interface EditorFrameState extends PreviewState { description?: string; stagedPreview?: PreviewState; activeDatasourceId: string | null; - activeData?: TableInspectorAdapter; } export type Action = @@ -35,10 +33,6 @@ export type Action = type: 'UPDATE_TITLE'; title: string; } - | { - type: 'UPDATE_ACTIVE_DATA'; - tables: TableInspectorAdapter; - } | { type: 'UPDATE_STATE'; // Just for diagnostics, so we can determine what action @@ -103,25 +97,27 @@ export function getActiveDatasourceIdFromDoc(doc?: Document) { return null; } - const [initialDatasourceId] = Object.keys(doc.state.datasourceStates); - return initialDatasourceId || null; + const [firstDatasourceFromDoc] = Object.keys(doc.state.datasourceStates); + return firstDatasourceFromDoc || null; } -function getInitialDatasourceId(props: EditorFrameProps) { - return props.initialDatasourceId - ? props.initialDatasourceId - : getActiveDatasourceIdFromDoc(props.doc); -} - -export const getInitialState = (props: EditorFrameProps): EditorFrameState => { +export const getInitialState = ( + params: EditorFrameProps & { doc?: Document } +): EditorFrameState => { const datasourceStates: EditorFrameState['datasourceStates'] = {}; - if (props.doc) { - Object.entries(props.doc.state.datasourceStates).forEach(([datasourceId, state]) => { + const initialDatasourceId = + getActiveDatasourceIdFromDoc(params.doc) || Object.keys(params.datasourceMap)[0] || null; + + const initialVisualizationId = + (params.doc && params.doc.visualizationType) || Object.keys(params.visualizationMap)[0] || null; + + if (params.doc) { + Object.entries(params.doc.state.datasourceStates).forEach(([datasourceId, state]) => { datasourceStates[datasourceId] = { isLoading: true, state }; }); - } else if (props.initialDatasourceId) { - datasourceStates[props.initialDatasourceId] = { + } else if (initialDatasourceId) { + datasourceStates[initialDatasourceId] = { state: null, isLoading: true, }; @@ -130,10 +126,10 @@ export const getInitialState = (props: EditorFrameProps): EditorFrameState => { return { title: '', datasourceStates, - activeDatasourceId: getInitialDatasourceId(props), + activeDatasourceId: initialDatasourceId, visualization: { state: null, - activeId: props.initialVisualizationId, + activeId: initialVisualizationId, }, }; }; @@ -146,11 +142,6 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta return { ...state, title: action.title }; case 'UPDATE_STATE': return action.updater(state); - case 'UPDATE_ACTIVE_DATA': - return { - ...state, - activeData: { ...action.tables }, - }; case 'UPDATE_LAYER': return { ...state, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 83b09226265423..bd8f134f59fbb7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import _ from 'lodash'; +import { flatten } from 'lodash'; import { Ast } from '@kbn/interpreter/common'; import { IconType } from '@elastic/eui/src/components/icon/icon'; import { Datatable } from 'src/plugins/expressions'; @@ -79,7 +79,7 @@ export function getSuggestions({ ); // Collect all table suggestions from available datasources - const datasourceTableSuggestions = _.flatten( + const datasourceTableSuggestions = flatten( datasources.map(([datasourceId, datasource]) => { const datasourceState = datasourceStates[datasourceId].state; let dataSourceSuggestions; @@ -103,9 +103,9 @@ export function getSuggestions({ // Pass all table suggestions to all visualization extensions to get visualization suggestions // and rank them by score - return _.flatten( + return flatten( Object.entries(visualizationMap).map(([visualizationId, visualization]) => - _.flatten( + flatten( datasourceTableSuggestions.map((datasourceSuggestion) => { const table = datasourceSuggestion.table; const currentVisualizationState = diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index e5acd2a2f47fda..0c2eb4f39d8959 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -7,7 +7,7 @@ import './suggestion_panel.scss'; -import _, { camelCase } from 'lodash'; +import { camelCase, pick } from 'lodash'; import React, { useState, useEffect, useMemo, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -442,7 +442,7 @@ function getPreviewExpression( ) { const datasource = datasources[visualizableState.datasourceId]; const datasourceState = visualizableState.datasourceState; - const updatedLayerApis: Record = _.pick( + const updatedLayerApis: Record = pick( frame.datasourceLayers, visualizableState.keptLayerIds ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index baa9d45a431eaf..1d248c4411023c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -16,14 +16,14 @@ import { DatasourceMock, createMockFramePublicAPI, } from '../../mocks'; - +import { mockDataPlugin, mountWithProvider } from '../../../mocks'; jest.mock('../../../debounced_component', () => { return { debouncedComponent: (fn: unknown) => fn, }; }); -import { WorkspacePanel, WorkspacePanelProps } from './workspace_panel'; +import { WorkspacePanel } from './workspace_panel'; import { mountWithIntl as mount } from '@kbn/test/jest'; import { ReactWrapper } from 'enzyme'; import { DragDrop, ChildDragDropProvider } from '../../../drag_drop'; @@ -34,7 +34,6 @@ import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/publ import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/public/triggers'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public/embeddable'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; const defaultPermissions: Record>> = { navLinks: { management: true }, @@ -50,24 +49,22 @@ function createCoreStartWithPermissions(newCapabilities = defaultPermissions) { return core; } -function getDefaultProps() { - return { - activeDatasourceId: 'mock', - datasourceStates: {}, - datasourceMap: {}, - framePublicAPI: createMockFramePublicAPI(), - activeVisualizationId: 'vis', - visualizationState: {}, - dispatch: () => {}, - ExpressionRenderer: createExpressionRendererMock(), - core: createCoreStartWithPermissions(), - plugins: { - uiActions: uiActionsPluginMock.createStartContract(), - data: dataPluginMock.createStartContract(), - }, - getSuggestionForField: () => undefined, - }; -} +const defaultProps = { + activeDatasourceId: 'mock', + datasourceStates: {}, + datasourceMap: {}, + framePublicAPI: createMockFramePublicAPI(), + activeVisualizationId: 'vis', + visualizationState: {}, + dispatch: () => {}, + ExpressionRenderer: createExpressionRendererMock(), + core: createCoreStartWithPermissions(), + plugins: { + uiActions: uiActionsPluginMock.createStartContract(), + data: mockDataPlugin(), + }, + getSuggestionForField: () => undefined, +}; describe('workspace_panel', () => { let mockVisualization: jest.Mocked; @@ -78,7 +75,7 @@ describe('workspace_panel', () => { let uiActionsMock: jest.Mocked; let trigger: jest.Mocked; - let instance: ReactWrapper; + let instance: ReactWrapper; beforeEach(() => { // These are used in specific tests to assert function calls @@ -95,50 +92,56 @@ describe('workspace_panel', () => { instance.unmount(); }); - it('should render an explanatory text if no visualization is active', () => { - instance = mount( + it('should render an explanatory text if no visualization is active', async () => { + const mounted = await mountWithProvider( + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should render an explanatory text if the visualization does not produce an expression', () => { - instance = mount( + it('should render an explanatory text if the visualization does not produce an expression', async () => { + const mounted = await mountWithProvider( null }, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should render an explanatory text if the datasource does not produce an expression', () => { - instance = mount( + it('should render an explanatory text if the datasource does not produce an expression', async () => { + const mounted = await mountWithProvider( 'vis' }, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should render the resulting expression using the expression renderer', () => { + it('should render the resulting expression using the expression renderer', async () => { const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, @@ -146,9 +149,9 @@ describe('workspace_panel', () => { mockDatasource.toExpression.mockReturnValue('datasource'); mockDatasource.getLayers.mockReturnValue(['first']); - instance = mount( + const mounted = await mountWithProvider( { vis: { ...mockVisualization, toExpression: () => 'vis' }, }} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; + expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` "kibana | lens_merge_tables layerIds=\\"first\\" tables={datasource} @@ -173,16 +179,16 @@ describe('workspace_panel', () => { `); }); - it('should execute a trigger on expression event', () => { + it('should execute a trigger on expression event', async () => { const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; mockDatasource.toExpression.mockReturnValue('datasource'); mockDatasource.getLayers.mockReturnValue(['first']); - const props = getDefaultProps(); + const props = defaultProps; - instance = mount( + const mounted = await mountWithProvider( { }} ExpressionRenderer={expressionRendererMock} plugins={{ ...props.plugins, uiActions: uiActionsMock }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; const onEvent = expressionRendererMock.mock.calls[0][0].onEvent!; @@ -212,7 +220,7 @@ describe('workspace_panel', () => { expect(trigger.exec).toHaveBeenCalledWith({ data: eventData }); }); - it('should push add current data table to state on data$ emitting value', () => { + it('should push add current data table to state on data$ emitting value', async () => { const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, @@ -221,9 +229,9 @@ describe('workspace_panel', () => { mockDatasource.getLayers.mockReturnValue(['first']); const dispatch = jest.fn(); - instance = mount( + const mounted = await mountWithProvider( { }} dispatch={dispatch} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; + const onData = expressionRendererMock.mock.calls[0][0].onData$!; const tableData = { table1: { columns: [], rows: [] } }; onData(undefined, { tables: { tables: tableData } }); - expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_ACTIVE_DATA', tables: tableData }); + expect(mounted.lensStore.dispatch).toHaveBeenCalledWith({ + type: 'app/onActiveDataChange', + payload: { activeData: tableData }, + }); }); - it('should include data fetching for each layer in the expression', () => { + it('should include data fetching for each layer in the expression', async () => { const mockDatasource2 = createMockDatasource('a'); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { @@ -263,9 +277,9 @@ describe('workspace_panel', () => { mockDatasource2.toExpression.mockReturnValue('datasource2'); mockDatasource2.getLayers.mockReturnValue(['second', 'third']); - instance = mount( + const mounted = await mountWithProvider( { vis: { ...mockVisualization, toExpression: () => 'vis' }, }} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; const ast = fromExpression(instance.find(expressionRendererMock).prop('expression') as string); @@ -341,9 +357,9 @@ describe('workspace_panel', () => { expressionRendererMock = jest.fn((_arg) => ); await act(async () => { - instance = mount( + const mounted = await mountWithProvider( { vis: { ...mockVisualization, toExpression: () => 'vis' }, }} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; }); instance.update(); @@ -392,9 +410,9 @@ describe('workspace_panel', () => { expressionRendererMock = jest.fn((_arg) => ); await act(async () => { - instance = mount( + const mounted = await mountWithProvider( { vis: { ...mockVisualization, toExpression: () => 'vis' }, }} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; }); instance.update(); @@ -434,16 +454,16 @@ describe('workspace_panel', () => { expect(expressionRendererMock).toHaveBeenCalledTimes(2); }); - it('should show an error message if there are missing indexpatterns in the visualization', () => { + it('should show an error message if there are missing indexpatterns in the visualization', async () => { mockDatasource.getLayers.mockReturnValue(['first']); mockDatasource.checkIntegrity.mockReturnValue(['a']); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="missing-refs-failure"]').exists()).toBeTruthy(); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should not show the management action in case of missing indexpattern and no navigation permissions', () => { + it('should not show the management action in case of missing indexpattern and no navigation permissions', async () => { mockDatasource.getLayers.mockReturnValue(['first']); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { navLinks: { management: false }, management: { kibana: { indexPatterns: true } }, })} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect( instance.find('[data-test-subj="configuration-failure-reconfigure-indexpatterns"]').exists() ).toBeFalsy(); }); - it('should not show the management action in case of missing indexpattern and no indexPattern specific permissions', () => { + it('should not show the management action in case of missing indexpattern and no indexPattern specific permissions', async () => { mockDatasource.getLayers.mockReturnValue(['first']); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { navLinks: { management: true }, management: { kibana: { indexPatterns: false } }, })} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect( instance.find('[data-test-subj="configuration-failure-reconfigure-indexpatterns"]').exists() ).toBeFalsy(); }); - it('should show an error message if validation on datasource does not pass', () => { + it('should show an error message if validation on datasource does not pass', async () => { mockDatasource.getErrorMessages.mockReturnValue([ { shortMessage: 'An error occurred', longMessage: 'An long description here' }, ]); @@ -550,9 +576,9 @@ describe('workspace_panel', () => { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="configuration-failure"]').exists()).toBeTruthy(); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should show an error message if validation on visualization does not pass', () => { + it('should show an error message if validation on visualization does not pass', async () => { mockDatasource.getErrorMessages.mockReturnValue(undefined); mockDatasource.getLayers.mockReturnValue(['first']); mockVisualization.getErrorMessages.mockReturnValue([ @@ -585,9 +613,9 @@ describe('workspace_panel', () => { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { visualizationMap={{ vis: mockVisualization, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="configuration-failure"]').exists()).toBeTruthy(); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should show an error message if validation on both datasource and visualization do not pass', () => { + it('should show an error message if validation on both datasource and visualization do not pass', async () => { mockDatasource.getErrorMessages.mockReturnValue([ { shortMessage: 'An error occurred', longMessage: 'An long description here' }, ]); @@ -622,9 +652,9 @@ describe('workspace_panel', () => { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { visualizationMap={{ vis: mockVisualization, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; // EuiFlexItem duplicates internally the attribute, so we need to filter only the most inner one here expect( @@ -648,7 +680,7 @@ describe('workspace_panel', () => { expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should show an error message if the expression fails to parse', () => { + it('should show an error message if the expression fails to parse', async () => { mockDatasource.toExpression.mockReturnValue('|||'); mockDatasource.getLayers.mockReturnValue(['first']); const framePublicAPI = createMockFramePublicAPI(); @@ -656,9 +688,9 @@ describe('workspace_panel', () => { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="expression-failure"]').exists()).toBeTruthy(); expect(instance.find(expressionRendererMock)).toHaveLength(0); @@ -688,9 +722,9 @@ describe('workspace_panel', () => { }; await act(async () => { - instance = mount( + const mounted = await mountWithProvider( { vis: { ...mockVisualization, toExpression: () => 'vis' }, }} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; }); instance.update(); @@ -727,9 +763,9 @@ describe('workspace_panel', () => { }; await act(async () => { - instance = mount( + const mounted = await mountWithProvider( { vis: { ...mockVisualization, toExpression: () => 'vis' }, }} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; }); instance.update(); @@ -791,7 +829,7 @@ describe('workspace_panel', () => { dropTargetsByOrder={undefined} > { ); } - it('should immediately transition if exactly one suggestion is returned', () => { + it('should immediately transition if exactly one suggestion is returned', async () => { mockGetSuggestionForField.mockReturnValue({ visualizationId: 'vis', datasourceState: {}, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 3d5d9a6d84d811..94065f316340cb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -54,6 +54,7 @@ import { DropIllustration } from '../../../assets/drop_illustration'; import { getOriginalRequestErrorMessages } from '../../error_helper'; import { getMissingIndexPattern, validateDatasourceAndVisualization } from '../state_helpers'; import { DefaultInspectorAdapters } from '../../../../../../../src/plugins/expressions/common'; +import { onActiveDataChange, useLensDispatch } from '../../../state_management'; export interface WorkspacePanelProps { activeVisualizationId: string | null; @@ -428,16 +429,15 @@ export const VisualizationWrapper = ({ ] ); + const dispatchLens = useLensDispatch(); + const onData$ = useCallback( (data: unknown, inspectorAdapters?: Partial) => { if (inspectorAdapters && inspectorAdapters.tables) { - dispatch({ - type: 'UPDATE_ACTIVE_DATA', - tables: inspectorAdapters.tables.tables, - }); + dispatchLens(onActiveDataChange({ activeData: { ...inspectorAdapters.tables.tables } })); } }, - [dispatch] + [dispatchLens] ); if (localState.configurationValidationError?.length) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 3c4412813bb83e..89a04f38201697 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import _ from 'lodash'; +import { isEqual, uniqBy } from 'lodash'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { @@ -23,7 +23,7 @@ import { Subscription } from 'rxjs'; import { toExpression, Ast } from '@kbn/interpreter/common'; import { DefaultInspectorAdapters, RenderMode } from 'src/plugins/expressions'; import { map, distinctUntilChanged, skip } from 'rxjs/operators'; -import isEqual from 'fast-deep-equal'; +import fastIsEqual from 'fast-deep-equal'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { @@ -161,7 +161,7 @@ export class Embeddable input$ .pipe( map((input) => input.enhancements?.dynamicActions), - distinctUntilChanged((a, b) => isEqual(a, b)), + distinctUntilChanged((a, b) => fastIsEqual(a, b)), skip(1) ) .subscribe((input) => { @@ -195,7 +195,7 @@ export class Embeddable input$ .pipe( distinctUntilChanged((a, b) => - isEqual( + fastIsEqual( ['attributes' in a && a.attributes, 'savedObjectId' in a && a.savedObjectId], ['attributes' in b && b.attributes, 'savedObjectId' in b && b.savedObjectId] ) @@ -214,7 +214,7 @@ export class Embeddable .pipe(map(() => this.getInput())) .pipe( distinctUntilChanged((a, b) => - isEqual( + fastIsEqual( [a.filters, a.query, a.timeRange, a.searchSessionId], [b.filters, b.query, b.timeRange, b.searchSessionId] ) @@ -283,9 +283,9 @@ export class Embeddable ? containerState.filters.filter((filter) => !filter.meta.disabled) : undefined; if ( - !_.isEqual(containerState.timeRange, this.externalSearchContext.timeRange) || - !_.isEqual(containerState.query, this.externalSearchContext.query) || - !_.isEqual(cleanedFilters, this.externalSearchContext.filters) || + !isEqual(containerState.timeRange, this.externalSearchContext.timeRange) || + !isEqual(containerState.query, this.externalSearchContext.query) || + !isEqual(cleanedFilters, this.externalSearchContext.filters) || this.externalSearchContext.searchSessionId !== containerState.searchSessionId ) { this.externalSearchContext = { @@ -446,7 +446,7 @@ export class Embeddable return; } const responses = await Promise.allSettled( - _.uniqBy( + uniqBy( this.savedVis.references.filter(({ type }) => type === 'index-pattern'), 'id' ).map(({ id }) => this.deps.indexPatternService.get(id)) diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index a56b3ccaa5bde7..38669d72474df1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -105,10 +105,9 @@ export type FrameMock = jest.Mocked; export function createMockPaletteDefinition(): jest.Mocked { return { - getColors: jest.fn((_) => ['#ff0000', '#00ff00']), + getCategoricalColors: jest.fn((_) => ['#ff0000', '#00ff00']), title: 'Mock Palette', id: 'default', - renderEditor: jest.fn(), toExpression: jest.fn(() => ({ type: 'expression', chain: [ @@ -119,7 +118,7 @@ export function createMockPaletteDefinition(): jest.Mocked { }, ], })), - getColor: jest.fn().mockReturnValue('#ff0000'), + getCategoricalColor: jest.fn().mockReturnValue('#ff0000'), }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index f6500596ce5a0e..62274df23e837b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -126,26 +126,11 @@ export class EditorFrameService { collectAsyncDefinitions(this.visualizations), ]); - const firstDatasourceId = Object.keys(resolvedDatasources)[0]; - const firstVisualizationId = Object.keys(resolvedVisualizations)[0]; - - const { EditorFrame, getActiveDatasourceIdFromDoc } = await import('../async_services'); - + const { EditorFrame } = await import('../async_services'); const palettes = await plugins.charts.palettes.getPalettes(); return { - EditorFrameContainer: ({ - doc, - onError, - dateRange, - query, - filters, - savedQuery, - onChange, - showNoDataPopover, - initialContext, - searchSessionId, - }) => { + EditorFrameContainer: ({ onError, showNoDataPopover, initialContext }) => { return (
); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index eeec7871a262c7..03eb234d907660 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -237,7 +237,7 @@ const initialState: IndexPatternPrivateState = { isFirstExistenceFetch: false, }; -const dslQuery = { bool: { must: [{ match_all: {} }], filter: [], should: [], must_not: [] } }; +const dslQuery = { bool: { must: [], filter: [], should: [], must_not: [] } }; describe('IndexPattern Data Panel', () => { let defaultProps: Parameters[0] & { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 8e26713630281d..fcca4a41581c26 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -6,7 +6,6 @@ */ import './dimension_editor.scss'; -import _ from 'lodash'; import React, { useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index b80d90ba78b1dc..db0a42047a1b86 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -6,7 +6,7 @@ */ import './field_select.scss'; -import _ from 'lodash'; +import { partition } from 'lodash'; import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { @@ -68,7 +68,7 @@ export function FieldSelect({ return !currentOperationType || operationByField[fieldName]!.has(currentOperationType); } - const [specialFields, normalFields] = _.partition( + const [specialFields, normalFields] = partition( fields, (field) => currentIndexPattern.getFieldByName(field)?.type === 'document' ); @@ -121,11 +121,11 @@ export function FieldSelect({ })); } - const [metaFields, nonMetaFields] = _.partition( + const [metaFields, nonMetaFields] = partition( normalFields, (field) => currentIndexPattern.getFieldByName(field)?.meta ); - const [availableFields, emptyFields] = _.partition(nonMetaFields, containsData); + const [availableFields, emptyFields] = partition(nonMetaFields, containsData); const constructFieldsOptions = (fieldsArr: string[], label: string) => fieldsArr.length > 0 && { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts index 801b1b17a1831d..504aa0912f9cc4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts @@ -5,7 +5,6 @@ * 2.0. */ -import _ from 'lodash'; import { DatasourceDimensionDropProps } from '../../types'; import { OperationType } from '../indexpattern'; import { memoizedGetAvailableOperationsByMetadata } from '../operations'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx index e02a014935458c..71de1e10300f03 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx @@ -6,7 +6,6 @@ */ import './dimension_editor.scss'; -import _ from 'lodash'; import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index cf9f7c0c559e42..2aa031959f5d7e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -164,7 +164,7 @@ describe('IndexPattern Field Item', () => { body: JSON.stringify({ dslQuery: { bool: { - must: [{ match_all: {} }], + must: [], filter: [], should: [], must_not: [], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 8fb0994c42fb9c..8b60cf134fe6fa 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import _ from 'lodash'; import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index bde07c182555ee..803ba9f5bae5dc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import _ from 'lodash'; +import { flatten, minBy, pick, mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; import { generateId } from '../id_generator'; import { DatasourceSuggestion, TableChangeType } from '../types'; @@ -58,9 +58,9 @@ function buildSuggestion({ // It's fairly easy to accidentally introduce a mismatch between // columnOrder and columns, so this is a safeguard to ensure the // two match up. - const layers = _.mapValues(updatedState.layers, (layer) => ({ + const layers = mapValues(updatedState.layers, (layer) => ({ ...layer, - columns: _.pick(layer.columns, layer.columnOrder) as Record, + columns: pick(layer.columns, layer.columnOrder) as Record, })); const columnOrder = layers[layerId].columnOrder; @@ -111,7 +111,7 @@ export function getDatasourceSuggestionsForField( // The field we're suggesting on matches an existing layer. In this case we find the layer with // the fewest configured columns and try to add the field to this table. If this layer does not // contain any layers yet, behave as if there is no layer. - const mostEmptyLayerId = _.minBy( + const mostEmptyLayerId = minBy( layerIds, (layerId) => state.layers[layerId].columnOrder.length ) as string; @@ -386,7 +386,7 @@ export function getDatasourceSuggestionsFromCurrentState( ]); } - return _.flatten( + return flatten( Object.entries(state.layers || {}) .filter(([_id, layer]) => layer.columnOrder.length && layer.indexPatternId) .map(([layerId, layer]) => { @@ -586,7 +586,7 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer availableReferenceColumns, ] = getExistingColumnGroups(layer); - return _.flatten( + return flatten( availableBucketedColumns.map((_col, index) => { // build suggestions with fewer buckets const bucketedColumns = availableBucketedColumns.slice(0, index + 1); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index d3913728cb64ec..192f3d3c0c5359 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -6,7 +6,7 @@ */ import { HttpHandler } from 'kibana/public'; -import _ from 'lodash'; +import { last } from 'lodash'; import { loadInitialState, loadIndexPatterns, @@ -841,7 +841,7 @@ describe('loader', () => { it('should call once for each index pattern', async () => { const setState = jest.fn(); const fetchJson = (jest.fn((path: string) => { - const indexPatternTitle = _.last(path.split('/')); + const indexPatternTitle = last(path.split('/')); return { indexPatternTitle, existingFieldNames: ['field_1', 'field_2'].map( @@ -891,7 +891,7 @@ describe('loader', () => { const setState = jest.fn(); const showNoDataPopover = jest.fn(); const fetchJson = (jest.fn((path: string) => { - const indexPatternTitle = _.last(path.split('/')); + const indexPatternTitle = last(path.split('/')); return { indexPatternTitle, existingFieldNames: diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 0eb661e92bb1d9..2921251babe7ff 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -5,7 +5,7 @@ * 2.0. */ -import _ from 'lodash'; +import { uniq, mapValues } from 'lodash'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { HttpSetup, SavedObjectReference } from 'kibana/public'; import { InitializationOptions, StateSetter } from '../types'; @@ -227,7 +227,7 @@ export async function loadInitialState({ const state = persistedState && references ? injectReferences(persistedState, references) : undefined; - const requiredPatterns: string[] = _.uniq( + const requiredPatterns: string[] = uniq( state ? Object.values(state.layers) .map((l) => l.indexPatternId) @@ -312,7 +312,7 @@ export async function changeIndexPattern({ setState((s) => ({ ...s, layers: isSingleEmptyLayer(state.layers) - ? _.mapValues(state.layers, (layer) => updateLayerIndexPattern(layer, indexPatterns[id])) + ? mapValues(state.layers, (layer) => updateLayerIndexPattern(layer, indexPatterns[id])) : state.layers, indexPatterns: { ...s.indexPatterns, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index 8d18a2752fd7e3..0e74ef6b85c804 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState } from 'react'; import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; +import { useDebounceWithOptions } from '../../../../shared_components'; import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '../column_types'; import { IndexPatternLayer } from '../../../types'; import { @@ -19,12 +20,7 @@ import { hasDateField, } from './utils'; import { updateColumnParam } from '../../layer_helpers'; -import { - getFormatFromPreviousColumn, - isValidNumber, - useDebounceWithOptions, - getFilter, -} from '../helpers'; +import { getFormatFromPreviousColumn, isValidNumber, getFilter } from '../helpers'; import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; import { HelpPopover, HelpPopoverButton } from '../../../help_popover'; import type { OperationDefinition, ParamEditorProps } from '..'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx index f719ac4250912e..45abbcd3d9cf96 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -5,35 +5,11 @@ * 2.0. */ -import { useRef } from 'react'; -import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import { IndexPatternColumn, operationDefinitionMap } from '.'; import { FieldBasedIndexPatternColumn } from './column_types'; import { IndexPattern } from '../../types'; -export const useDebounceWithOptions = ( - fn: Function, - { skipFirstRender }: { skipFirstRender: boolean } = { skipFirstRender: false }, - ms?: number | undefined, - deps?: React.DependencyList | undefined -) => { - const isFirstRender = useRef(true); - const newDeps = [...(deps || []), isFirstRender]; - - return useDebounce( - () => { - if (skipFirstRender && isFirstRender.current) { - isFirstRender.current = false; - return; - } - return fn(); - }, - ms, - newDeps - ); -}; - export function getInvalidFieldMessage( column: FieldBasedIndexPatternColumn, indexPattern?: IndexPattern diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index 705a1f7172fff8..4c09ae4ed8c47b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -16,10 +16,10 @@ import { getInvalidFieldMessage, getSafeName, isValidNumber, - useDebounceWithOptions, getFilter, } from './helpers'; import { FieldBasedIndexPatternColumn } from './column_types'; +import { useDebounceWithOptions } from '../../../shared_components'; export interface PercentileIndexPatternColumn extends FieldBasedIndexPatternColumn { operationType: 'percentile'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx index b3ffb58df00d39..43f5527e42d4bf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx @@ -22,6 +22,7 @@ import { htmlIdGenerator, keys, } from '@elastic/eui'; +import { useDebounceWithOptions } from '../../../../shared_components'; import { IFieldFormat } from '../../../../../../../../src/plugins/data/common'; import { RangeTypeLens, isValidRange } from './ranges'; import { FROM_PLACEHOLDER, TO_PLACEHOLDER, TYPING_DEBOUNCE_TIME } from './constants'; @@ -31,7 +32,7 @@ import { DraggableBucketContainer, LabelInput, } from '../shared_components'; -import { isValidNumber, useDebounceWithOptions } from '../helpers'; +import { isValidNumber } from '../helpers'; const generateId = htmlIdGenerator(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx index 4851b6ff3ec975..3389c723b4daf6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx @@ -23,7 +23,7 @@ import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/public'; import { RangeColumnParams, UpdateParamsFnType, MODES_TYPES } from './ranges'; import { AdvancedRangeEditor } from './advanced_editor'; import { TYPING_DEBOUNCE_TIME, MODES, MIN_HISTOGRAM_BARS } from './constants'; -import { useDebounceWithOptions } from '../helpers'; +import { useDebounceWithOptions } from '../../../../shared_components'; import { HelpPopover, HelpPopoverButton } from '../../../help_popover'; const GranularityHelpPopover = () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.tsx index 915e67c4eba0bd..a4c0f8f1c50e05 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFieldNumber } from '@elastic/eui'; -import { useDebounceWithOptions } from '../helpers'; +import { useDebounceWithOptions } from '../../../../shared_components'; export const ValuesInput = ({ value, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index bc4a61eda39699..92452a11e94c16 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import _, { partition } from 'lodash'; +import { partition, mapValues, pickBy } from 'lodash'; import { getSortScoreByPriority } from './operations'; import type { OperationMetadata, VisualizationDimensionGroupConfig } from '../../types'; import { @@ -1071,7 +1071,7 @@ export function getColumnOrder(layer: IndexPatternLayer): string[] { } }); - const [aggregations, metrics] = _.partition(entries, ([, col]) => col.isBucketed); + const [aggregations, metrics] = partition(entries, ([, col]) => col.isBucketed); return aggregations.map(([id]) => id).concat(metrics.map(([id]) => id)); } @@ -1110,10 +1110,10 @@ export function updateLayerIndexPattern( layer: IndexPatternLayer, newIndexPattern: IndexPattern ): IndexPatternLayer { - const keptColumns: IndexPatternLayer['columns'] = _.pickBy(layer.columns, (column) => { + const keptColumns: IndexPatternLayer['columns'] = pickBy(layer.columns, (column) => { return isColumnTransferable(column, newIndexPattern, layer); }); - const newColumns: IndexPatternLayer['columns'] = _.mapValues(keptColumns, (column) => { + const newColumns: IndexPatternLayer['columns'] = mapValues(keptColumns, (column) => { const operationDefinition = operationDefinitionMap[column.operationType]; return operationDefinition.transfer ? operationDefinition.transfer(column, newIndexPattern) diff --git a/x-pack/plugins/lens/public/metric_visualization/auto_scale.tsx b/x-pack/plugins/lens/public/metric_visualization/auto_scale.tsx index cc9c10b678374e..90479370931343 100644 --- a/x-pack/plugins/lens/public/metric_visualization/auto_scale.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/auto_scale.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import _ from 'lodash'; +import { throttle } from 'lodash'; import { EuiResizeObserver } from '@elastic/eui'; interface Props extends React.HTMLAttributes { @@ -26,7 +26,7 @@ export class AutoScale extends React.Component { constructor(props: Props) { super(props); - this.scale = _.throttle(() => { + this.scale = throttle(() => { const scale = computeScale(this.parent, this.child, this.props.minScale); // Prevent an infinite render loop diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index c1f885d167659d..473c170aef2948 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -6,8 +6,35 @@ */ import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { ReactWrapper } from 'enzyme'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { mountWithIntl as mount } from '@kbn/test/jest'; +import { Observable, Subject } from 'rxjs'; +import { coreMock } from 'src/core/public/mocks'; +import moment from 'moment'; +import { Provider } from 'react-redux'; +import { act } from 'react-dom/test-utils'; import { LensPublicStart } from '.'; import { visualizationTypes } from './xy_visualization/types'; +import { navigationPluginMock } from '../../../../src/plugins/navigation/public/mocks'; +import { LensAppServices } from './app_plugin/types'; +import { DOC_TYPE } from '../common'; +import { DataPublicPluginStart, esFilters, UI_SETTINGS } from '../../../../src/plugins/data/public'; +import { + LensByValueInput, + LensSavedObjectAttributes, + LensByReferenceInput, +} from './editor_frame_service/embeddable/embeddable'; +import { + mockAttributeService, + createEmbeddableStateTransferMock, +} from '../../../../src/plugins/embeddable/public/mocks'; +import { LensAttributeService } from './lens_attribute_service'; +import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public'; + +import { makeConfigureStore, getPreloadedState, LensAppState } from './state_management/index'; +import { getResolvedDateRange } from './utils'; export type Start = jest.Mocked; @@ -26,3 +53,252 @@ const createStartContract = (): Start => { export const lensPluginMock = { createStartContract, }; + +export const defaultDoc = ({ + savedObjectId: '1234', + title: 'An extremely cool default document!', + expression: 'definitely a valid expression', + state: { + query: 'kuery', + filters: [{ query: { match_phrase: { src: 'test' } } }], + }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], +} as unknown) as Document; + +export function createMockTimefilter() { + const unsubscribe = jest.fn(); + + let timeFilter = { from: 'now-7d', to: 'now' }; + let subscriber: () => void; + return { + getTime: jest.fn(() => timeFilter), + setTime: jest.fn((newTimeFilter) => { + timeFilter = newTimeFilter; + if (subscriber) { + subscriber(); + } + }), + getTimeUpdate$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + calculateBounds: jest.fn(() => ({ + min: moment('2021-01-10T04:00:00.000Z'), + max: moment('2021-01-10T08:00:00.000Z'), + })), + getBounds: jest.fn(() => timeFilter), + getRefreshInterval: () => {}, + getRefreshIntervalDefaults: () => {}, + getAutoRefreshFetch$: () => new Observable(), + }; +} + +export function mockDataPlugin(sessionIdSubject = new Subject()) { + function createMockSearchService() { + let sessionIdCounter = 1; + return { + session: { + start: jest.fn(() => `sessionId-${sessionIdCounter++}`), + clear: jest.fn(), + getSessionId: jest.fn(() => `sessionId-${sessionIdCounter}`), + getSession$: jest.fn(() => sessionIdSubject.asObservable()), + }, + }; + } + + function createMockFilterManager() { + const unsubscribe = jest.fn(); + + let subscriber: () => void; + let filters: unknown = []; + + return { + getUpdates$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + setFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), + setAppFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), + getFilters: () => filters, + getGlobalFilters: () => { + // @ts-ignore + return filters.filter(esFilters.isFilterPinned); + }, + removeAll: () => { + filters = []; + subscriber(); + }, + }; + } + + function createMockQueryString() { + return { + getQuery: jest.fn(() => ({ query: '', language: 'lucene' })), + setQuery: jest.fn(), + getDefaultQuery: jest.fn(() => ({ query: '', language: 'lucene' })), + }; + } + return ({ + query: { + filterManager: createMockFilterManager(), + timefilter: { + timefilter: createMockTimefilter(), + }, + queryString: createMockQueryString(), + state$: new Observable(), + }, + indexPatterns: { + get: jest.fn((id) => { + return new Promise((resolve) => resolve({ id })); + }), + }, + search: createMockSearchService(), + nowProvider: { + get: jest.fn(), + }, + } as unknown) as DataPublicPluginStart; +} + +export function makeDefaultServices( + sessionIdSubject = new Subject(), + doc = defaultDoc +): jest.Mocked { + const core = coreMock.createStart({ basePath: '/testbasepath' }); + core.uiSettings.get.mockImplementation( + jest.fn((type) => { + if (type === UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS) { + return { from: 'now-7d', to: 'now' }; + } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { + return 'kuery'; + } else if (type === 'state:storeInSessionStorage') { + return false; + } else { + return []; + } + }) + ); + + const navigationStartMock = navigationPluginMock.createStartContract(); + + jest.spyOn(navigationStartMock.ui.TopNavMenu.prototype, 'constructor').mockImplementation(() => { + return
; + }); + + function makeAttributeService(): LensAttributeService { + const attributeServiceMock = mockAttributeService< + LensSavedObjectAttributes, + LensByValueInput, + LensByReferenceInput + >( + DOC_TYPE, + { + saveMethod: jest.fn(), + unwrapMethod: jest.fn(), + checkForDuplicateTitle: jest.fn(), + }, + core + ); + + attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(doc); + attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({ + savedObjectId: ((doc as unknown) as LensByReferenceInput).savedObjectId, + }); + + return attributeServiceMock; + } + + return { + http: core.http, + chrome: core.chrome, + overlays: core.overlays, + uiSettings: core.uiSettings, + navigation: navigationStartMock, + notifications: core.notifications, + attributeService: makeAttributeService(), + savedObjectsClient: core.savedObjects.client, + dashboardFeatureFlag: { allowByValueEmbeddables: false }, + stateTransfer: createEmbeddableStateTransferMock() as EmbeddableStateTransfer, + getOriginatingAppName: jest.fn(() => 'defaultOriginatingApp'), + application: { + ...core.application, + capabilities: { + ...core.application.capabilities, + visualize: { save: true, saveQuery: true, show: true }, + }, + getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), + }, + data: mockDataPlugin(sessionIdSubject), + storage: { + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }, + }; +} + +export function mockLensStore({ + data, + storePreloadedState, +}: { + data: DataPublicPluginStart; + storePreloadedState?: Partial; +}) { + const lensStore = makeConfigureStore( + getPreloadedState({ + query: data.query.queryString.getQuery(), + filters: data.query.filterManager.getGlobalFilters(), + searchSessionId: data.search.session.start(), + resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), + ...storePreloadedState, + }), + { + data, + } + ); + + const origDispatch = lensStore.dispatch; + lensStore.dispatch = jest.fn(origDispatch); + return lensStore; +} + +export const mountWithProvider = async ( + component: React.ReactElement, + data: DataPublicPluginStart, + storePreloadedState?: Partial, + extraWrappingComponent?: React.FC<{ + children: React.ReactNode; + }> +) => { + const lensStore = mockLensStore({ data, storePreloadedState }); + + const wrappingComponent: React.FC<{ + children: React.ReactNode; + }> = ({ children }) => { + if (extraWrappingComponent) { + return extraWrappingComponent({ + children: {children}, + }); + } + return {children}; + }; + + let instance: ReactWrapper = {} as ReactWrapper; + + await act(async () => { + instance = mount(component, ({ + wrappingComponent, + } as unknown) as ReactWrapper); + }); + return { instance, lensStore }; +}; 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 7191da0af6bfe7..a9e7e4adb9ca78 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 @@ -161,7 +161,7 @@ describe('PieVisualization component', () => { [] as HierarchyOfArrays ); - expect(defaultArgs.paletteService.get('mock').getColor).toHaveBeenCalledWith( + expect(defaultArgs.paletteService.get('mock').getCategoricalColor).toHaveBeenCalledWith( [ { name: 'css', 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 cc31222f6b9ab0..6c1cbe63a5a3e3 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -150,7 +150,7 @@ export function PieComponent( } } - const outputColor = paletteService.get(palette.name).getColor( + const outputColor = paletteService.get(palette.name).getCategoricalColor( seriesLayers, { behindText: categoryDisplay !== 'hide', diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index ad8d87292b1d80..f413b122d913cc 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -126,7 +126,7 @@ export const getPieVisualization = ({ triggerIcon: 'colorBy', palette: paletteService .get(state.palette?.name || 'default') - .getColors(10, state.palette?.params), + .getCategoricalColors(10, state.palette?.params), }; } diff --git a/x-pack/plugins/lens/public/shared_components/coloring/color_stops.test.tsx b/x-pack/plugins/lens/public/shared_components/coloring/color_stops.test.tsx new file mode 100644 index 00000000000000..54c7f3cef90fe3 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/coloring/color_stops.test.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiColorPicker } from '@elastic/eui'; +import { mount } from 'enzyme'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { CustomStops, CustomStopsProps } from './color_stops'; + +describe('Color Stops component', () => { + let props: CustomStopsProps; + beforeEach(() => { + props = { + colorStops: [ + { color: '#aaa', stop: 20 }, + { color: '#bbb', stop: 40 }, + { color: '#ccc', stop: 60 }, + ], + paletteConfiguration: {}, + dataBounds: { min: 0, max: 200 }, + onChange: jest.fn(), + 'data-test-prefix': 'my-test', + }; + }); + it('should display all the color stops passed', () => { + const component = mount(); + expect( + component.find('input[data-test-subj^="my-test_dynamicColoring_stop_value_"]') + ).toHaveLength(3); + }); + + it('should disable the delete buttons when there are 2 stops or less', () => { + // reduce to 2 stops + props.colorStops = props.colorStops.slice(0, 2); + const component = mount(); + expect( + component + .find('[data-test-subj="my-test_dynamicColoring_removeStop_0"]') + .first() + .prop('isDisabled') + ).toBe(true); + }); + + it('should add a new stop with default color and reasonable distance from last one', () => { + let component = mount(); + const addStopButton = component + .find('[data-test-subj="my-test_dynamicColoring_addStop"]') + .first(); + act(() => { + addStopButton.prop('onClick')!({} as React.MouseEvent); + }); + component = component.update(); + + expect( + component.find('input[data-test-subj^="my-test_dynamicColoring_stop_value_"]') + ).toHaveLength(4); + expect( + component.find('input[data-test-subj="my-test_dynamicColoring_stop_value_3"]').prop('value') + ).toBe('80'); // 60-40 + 60 + expect( + component + // workaround for https://github.com/elastic/eui/issues/4792 + .find('[data-test-subj="my-test_dynamicColoring_stop_color_3"]') + .last() // pick the inner element + .childAt(0) + .prop('color') + ).toBe('#ccc'); // pick previous color + }); + + it('should restore previous color when abandoning the field with an empty color', () => { + let component = mount(); + expect( + component + .find('[data-test-subj="my-test_dynamicColoring_stop_row_0"]') + .first() + .find(EuiColorPicker) + .first() + .prop('color') + ).toBe('#aaa'); + act(() => { + component + .find('[data-test-subj="my-test_dynamicColoring_stop_row_0"]') + .first() + .find(EuiColorPicker) + .first() + .prop('onChange')!('', { + rgba: [NaN, NaN, NaN, NaN], + hex: '', + isValid: false, + }); + }); + component = component.update(); + expect( + component + .find('[data-test-subj="my-test_dynamicColoring_stop_row_0"]') + .first() + .find(EuiColorPicker) + .first() + .prop('color') + ).toBe(''); + act(() => { + component + .find('[data-test-subj="my-test_dynamicColoring_stop_color_0"]') + .first() + .prop('onBlur')!({} as React.FocusEvent); + }); + component = component.update(); + expect( + component + .find('[data-test-subj="my-test_dynamicColoring_stop_row_0"]') + .first() + .find(EuiColorPicker) + .first() + .prop('color') + ).toBe('#aaa'); + }); + + it('should sort stops value on whole component blur', () => { + let component = mount(); + let firstStopValueInput = component + .find('[data-test-subj="my-test_dynamicColoring_stop_value_0"]') + .first(); + act(() => { + firstStopValueInput.prop('onChange')!(({ + target: { value: ' 90' }, + } as unknown) as React.ChangeEvent); + }); + + component = component.update(); + + act(() => { + component + .find('[data-test-subj="my-test_dynamicColoring_stop_row_0"]') + .first() + .prop('onBlur')!({} as React.FocusEvent); + }); + component = component.update(); + + // retrieve again the input + firstStopValueInput = component + .find('[data-test-subj="my-test_dynamicColoring_stop_value_0"]') + .first(); + expect(firstStopValueInput.prop('value')).toBe('40'); + // the previous one move at the bottom + expect( + component + .find('[data-test-subj="my-test_dynamicColoring_stop_value_2"]') + .first() + .prop('value') + ).toBe('90'); + }); +}); diff --git a/x-pack/plugins/lens/public/shared_components/coloring/color_stops.tsx b/x-pack/plugins/lens/public/shared_components/coloring/color_stops.tsx new file mode 100644 index 00000000000000..37197b232ddf55 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/coloring/color_stops.tsx @@ -0,0 +1,294 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, useCallback, useMemo } from 'react'; +import type { FocusEvent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFieldNumber, + EuiColorPicker, + EuiButtonIcon, + EuiFlexItem, + EuiFlexGroup, + EuiButtonEmpty, + EuiSpacer, + EuiScreenReaderOnly, + htmlIdGenerator, +} from '@elastic/eui'; +import useUnmount from 'react-use/lib/useUnmount'; +import { DEFAULT_COLOR } from './constants'; +import { getDataMinMax, getStepValue, isValidColor } from './utils'; +import { TooltipWrapper, useDebouncedValue } from '../index'; +import { ColorStop, CustomPaletteParams } from './types'; + +const idGeneratorFn = htmlIdGenerator(); + +function areStopsValid(colorStops: Array<{ color: string; stop: string }>) { + return colorStops.every( + ({ color, stop }) => isValidColor(color) && !Number.isNaN(parseFloat(stop)) + ); +} + +function shouldSortStops(colorStops: Array<{ color: string; stop: string | number }>) { + return colorStops.some(({ stop }, i) => { + const numberStop = Number(stop); + const prevNumberStop = Number(colorStops[i - 1]?.stop ?? -Infinity); + return numberStop < prevNumberStop; + }); +} + +export interface CustomStopsProps { + colorStops: ColorStop[]; + onChange: (colorStops: ColorStop[]) => void; + dataBounds: { min: number; max: number }; + paletteConfiguration: CustomPaletteParams | undefined; + 'data-test-prefix': string; +} +export const CustomStops = ({ + colorStops, + onChange, + paletteConfiguration, + dataBounds, + ['data-test-prefix']: dataTestPrefix, +}: CustomStopsProps) => { + const onChangeWithValidation = useCallback( + (newColorStops: Array<{ color: string; stop: string }>) => { + const areStopsValuesValid = areStopsValid(newColorStops); + const shouldSort = shouldSortStops(newColorStops); + if (areStopsValuesValid && !shouldSort) { + onChange(newColorStops.map(({ color, stop }) => ({ color, stop: Number(stop) }))); + } + }, + [onChange] + ); + + const memoizedValues = useMemo(() => { + return colorStops.map(({ color, stop }, i) => ({ + color, + stop: String(stop), + id: idGeneratorFn(), + })); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [paletteConfiguration?.name, paletteConfiguration?.reverse, paletteConfiguration?.rangeType]); + + const { inputValue: localColorStops, handleInputChange: setLocalColorStops } = useDebouncedValue({ + onChange: onChangeWithValidation, + value: memoizedValues, + }); + const [sortedReason, setSortReason] = useState(''); + const shouldEnableDelete = localColorStops.length > 2; + + const [popoverInFocus, setPopoverInFocus] = useState(false); + + // refresh on unmount: + // the onChange logic here is a bit different than the one above as it has to actively sort if required + useUnmount(() => { + const areStopsValuesValid = areStopsValid(localColorStops); + const shouldSort = shouldSortStops(localColorStops); + if (areStopsValuesValid && shouldSort) { + onChange( + localColorStops + .map(({ color, stop }) => ({ color, stop: Number(stop) })) + .sort(({ stop: stopA }, { stop: stopB }) => Number(stopA) - Number(stopB)) + ); + } + }); + + const rangeType = paletteConfiguration?.rangeType || 'percent'; + + return ( + <> + {sortedReason ? ( + +

+ {i18n.translate('xpack.lens.dynamicColoring.customPalette.sortReason', { + defaultMessage: 'Color stops have been sorted due to new stop value {value}', + values: { + value: sortedReason, + }, + })} +

+
+ ) : null} + + + {localColorStops.map(({ color, stop, id }, index) => { + const prevStopValue = Number(localColorStops[index - 1]?.stop ?? -Infinity); + const nextStopValue = Number(localColorStops[index + 1]?.stop ?? Infinity); + + return ( + ) => { + // sort the stops when the focus leaves the row container + const shouldSort = Number(stop) > nextStopValue || prevStopValue > Number(stop); + const isFocusStillInContent = + (e.currentTarget as Node)?.contains(e.relatedTarget as Node) || popoverInFocus; + const hasInvalidColor = !isValidColor(color); + if ((shouldSort && !isFocusStillInContent) || hasInvalidColor) { + // replace invalid color with previous valid one + const lastValidColor = hasInvalidColor ? colorStops[index].color : color; + const localColorStopsCopy = localColorStops.map((item, i) => + i === index ? { color: lastValidColor, stop, id } : item + ); + setLocalColorStops( + localColorStopsCopy.sort( + ({ stop: stopA }, { stop: stopB }) => Number(stopA) - Number(stopB) + ) + ); + setSortReason(stop); + } + }} + > + + + { + const newStopString = target.value.trim(); + const newColorStops = [...localColorStops]; + newColorStops[index] = { + color, + stop: newStopString, + id, + }; + setLocalColorStops(newColorStops); + }} + append={rangeType === 'percent' ? '%' : undefined} + aria-label={i18n.translate( + 'xpack.lens.dynamicColoring.customPalette.stopAriaLabel', + { + defaultMessage: 'Stop {index}', + values: { + index: index + 1, + }, + } + )} + /> + + + { + // make sure that the popover is closed + if (color === '' && !popoverInFocus) { + const newColorStops = [...localColorStops]; + newColorStops[index] = { color: colorStops[index].color, stop, id }; + setLocalColorStops(newColorStops); + } + }} + > + { + const newColorStops = [...localColorStops]; + newColorStops[index] = { color: newColor, stop, id }; + setLocalColorStops(newColorStops); + }} + secondaryInputDisplay="top" + color={color} + isInvalid={!isValidColor(color)} + showAlpha + compressed + onFocus={() => setPopoverInFocus(true)} + onBlur={() => { + setPopoverInFocus(false); + if (color === '') { + const newColorStops = [...localColorStops]; + newColorStops[index] = { color: colorStops[index].color, stop, id }; + setLocalColorStops(newColorStops); + } + }} + placeholder=" " + /> + + + + + { + const newColorStops = localColorStops.filter((_, i) => i !== index); + setLocalColorStops(newColorStops); + }} + data-test-subj={`${dataTestPrefix}_dynamicColoring_removeStop_${index}`} + isDisabled={!shouldEnableDelete} + /> + + + + + ); + })} + + + + + { + const newColorStops = [...localColorStops]; + const length = newColorStops.length; + const { max } = getDataMinMax(rangeType, dataBounds); + const step = getStepValue( + colorStops, + newColorStops.map(({ color, stop }) => ({ color, stop: Number(stop) })), + max + ); + const prevColor = localColorStops[length - 1].color || DEFAULT_COLOR; + const newStop = step + Number(localColorStops[length - 1].stop); + newColorStops.push({ + color: prevColor, + stop: String(newStop), + id: idGeneratorFn(), + }); + setLocalColorStops(newColorStops); + }} + > + {i18n.translate('xpack.lens.dynamicColoring.customPalette.addColorStop', { + defaultMessage: 'Add color stop', + })} + + + ); +}; diff --git a/x-pack/plugins/lens/public/shared_components/coloring/constants.ts b/x-pack/plugins/lens/public/shared_components/coloring/constants.ts new file mode 100644 index 00000000000000..5e6fc207656ac3 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/coloring/constants.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 { RequiredPaletteParamTypes } from './types'; + +export const DEFAULT_PALETTE_NAME = 'positive'; +export const FIXED_PROGRESSION = 'fixed' as const; +export const CUSTOM_PALETTE = 'custom'; +export const DEFAULT_CONTINUITY = 'above'; +export const DEFAULT_MIN_STOP = 0; +export const DEFAULT_MAX_STOP = 100; +export const DEFAULT_COLOR_STEPS = 5; +export const DEFAULT_COLOR = '#6092C0'; // Same as EUI ColorStops default for new stops +export const defaultPaletteParams: RequiredPaletteParamTypes = { + name: DEFAULT_PALETTE_NAME, + reverse: false, + rangeType: 'percent', + rangeMin: DEFAULT_MIN_STOP, + rangeMax: DEFAULT_MAX_STOP, + progression: FIXED_PROGRESSION, + stops: [], + steps: DEFAULT_COLOR_STEPS, + colorStops: [], + continuity: DEFAULT_CONTINUITY, +}; diff --git a/x-pack/plugins/lens/public/shared_components/coloring/index.ts b/x-pack/plugins/lens/public/shared_components/coloring/index.ts new file mode 100644 index 00000000000000..3b34c6662c6819 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/coloring/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { CustomizablePalette } from './palette_configuration'; +export { CustomStops } from './color_stops'; +export * from './types'; +export * from './utils'; +export * from './constants'; diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.scss b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.scss new file mode 100644 index 00000000000000..c6b14c5c5f9a35 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.scss @@ -0,0 +1,7 @@ +.lnsPalettePanel__section--shaded { + background-color: $euiColorLightestShade; +} + +.lnsPalettePanel__section { + padding: $euiSizeS; +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx new file mode 100644 index 00000000000000..28ba28a5801e40 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx @@ -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 React from 'react'; +import { EuiColorPalettePickerPaletteProps } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test/jest'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import { ReactWrapper } from 'enzyme'; +import { CustomPaletteParams } from './types'; +import { applyPaletteParams } from './utils'; +import { CustomizablePalette } from './palette_configuration'; + +describe('palette utilities', () => { + const paletteRegistry = chartPluginMock.createPaletteRegistry(); + describe('applyPaletteParams', () => { + it('should return a set of colors for a basic configuration', () => { + expect( + applyPaletteParams( + paletteRegistry, + { type: 'palette', name: 'positive' }, + { min: 0, max: 100 } + ) + ).toEqual([ + { color: 'blue', stop: 20 }, + { color: 'yellow', stop: 70 }, + ]); + }); + + it('should reverse the palette color stops correctly', () => { + expect( + applyPaletteParams( + paletteRegistry, + { + type: 'palette', + name: 'positive', + params: { reverse: true }, + }, + { min: 0, max: 100 } + ) + ).toEqual([ + { color: 'yellow', stop: 20 }, + { color: 'blue', stop: 70 }, + ]); + }); + }); +}); + +describe('palette panel', () => { + const paletteRegistry = chartPluginMock.createPaletteRegistry(); + let props: { + palettes: PaletteRegistry; + activePalette: PaletteOutput; + setPalette: (palette: PaletteOutput) => void; + dataBounds: { min: number; max: number }; + }; + + describe('palette picker', () => { + beforeEach(() => { + props = { + activePalette: { type: 'palette', name: 'positive' }, + palettes: paletteRegistry, + setPalette: jest.fn(), + dataBounds: { min: 0, max: 100 }, + }; + }); + + function changePaletteIn(instance: ReactWrapper, newPaletteName: string) { + return ((instance + .find('[data-test-subj="lnsDatatable_dynamicColoring_palette_picker"]') + .at(1) + .prop('onChange') as unknown) as (value: string) => void)?.(newPaletteName); + } + + it('should show only dynamic coloring enabled palette + custom option', () => { + const instance = mountWithIntl(); + const paletteOptions = instance + .find('[data-test-subj="lnsDatatable_dynamicColoring_palette_picker"]') + .at(1) + .prop('palettes') as EuiColorPalettePickerPaletteProps[]; + expect(paletteOptions.length).toEqual(2); + + expect(paletteOptions[paletteOptions.length - 1]).toEqual({ + title: 'Custom Mocked Palette', // <- picks the title of the custom palette + type: 'fixed', + value: 'custom', + palette: ['blue', 'yellow'], + 'data-test-subj': 'custom-palette', + }); + }); + + it('should set the colorStops and stops when selecting the Custom palette from the list', () => { + const instance = mountWithIntl(); + + changePaletteIn(instance, 'custom'); + + expect(props.setPalette).toHaveBeenCalledWith({ + type: 'palette', + name: 'custom', + params: expect.objectContaining({ + colorStops: [ + { color: 'blue', stop: 0 }, + { color: 'yellow', stop: 50 }, + ], + stops: [ + { color: 'blue', stop: 50 }, + { color: 'yellow', stop: 100 }, + ], + name: 'custom', + }), + }); + }); + + describe('reverse option', () => { + beforeEach(() => { + props = { + activePalette: { type: 'palette', name: 'positive' }, + palettes: paletteRegistry, + setPalette: jest.fn(), + dataBounds: { min: 0, max: 100 }, + }; + }); + + function toggleReverse(instance: ReactWrapper, checked: boolean) { + return instance + .find('[data-test-subj="lnsDatatable_dynamicColoring_reverse"]') + .first() + .prop('onClick')!({} as React.MouseEvent); + } + + it('should reverse the colorStops on click', () => { + const instance = mountWithIntl(); + + toggleReverse(instance, true); + + expect(props.setPalette).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ + reverse: true, + }), + }) + ); + }); + }); + + describe('custom stops', () => { + beforeEach(() => { + props = { + activePalette: { type: 'palette', name: 'positive' }, + palettes: paletteRegistry, + setPalette: jest.fn(), + dataBounds: { min: 0, max: 100 }, + }; + }); + it('should be visible for predefined palettes', () => { + const instance = mountWithIntl(); + expect( + instance.find('[data-test-subj="lnsDatatable_dynamicColoring_custom_stops"]').exists() + ).toEqual(true); + }); + + it('should be visible for custom palettes', () => { + const instance = mountWithIntl( + + ); + expect( + instance.find('[data-test-subj="lnsDatatable_dynamicColoring_custom_stops"]').exists() + ).toEqual(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx new file mode 100644 index 00000000000000..df01b3e57cd7df --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx @@ -0,0 +1,340 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import { + EuiFormRow, + htmlIdGenerator, + EuiButtonGroup, + EuiFlexGroup, + EuiFlexItem, + EuiSuperSelect, + EuiIcon, + EuiIconTip, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { PalettePicker } from './palette_picker'; + +import './palette_configuration.scss'; + +import { CustomStops } from './color_stops'; +import { defaultPaletteParams, CUSTOM_PALETTE, DEFAULT_COLOR_STEPS } from './constants'; +import { CustomPaletteParams, RequiredPaletteParamTypes } from './types'; +import { + getColorStops, + getPaletteStops, + mergePaletteParams, + getDataMinMax, + remapStopsByNewInterval, + getSwitchToCustomParams, + reversePalette, + roundStopValues, +} from './utils'; +const idPrefix = htmlIdGenerator()(); + +/** + * Some name conventions here: + * * `displayStops` => It's an additional transformation of `stops` into a [0, N] domain for the EUIPaletteDisplay component. + * * `stops` => final steps used to table coloring. It is a rightShift of the colorStops + * * `colorStops` => user's color stop inputs. Used to compute range min. + * + * When the user inputs the colorStops, they are designed to be the initial part of the color segment, + * so the next stops indicate where the previous stop ends. + * Both table coloring logic and EuiPaletteDisplay format implementation works differently than our current `colorStops`, + * by having the stop values at the end of each color segment rather than at the beginning: `stops` values are computed by a rightShift of `colorStops`. + * EuiPaletteDisplay has an additional requirement as it is always mapped against a domain [0, N]: from `stops` the `displayStops` are computed with + * some continuity enrichment and a remap against a [0, 100] domain to make the palette component work ok. + * + * These naming conventions would be useful to track the code flow in this feature as multiple transformations are happening + * for a single change. + */ + +export function CustomizablePalette({ + palettes, + activePalette, + setPalette, + dataBounds, +}: { + palettes: PaletteRegistry; + activePalette: PaletteOutput; + setPalette: (palette: PaletteOutput) => void; + dataBounds: { min: number; max: number }; +}) { + const isCurrentPaletteCustom = activePalette.params?.name === CUSTOM_PALETTE; + + const colorStopsToShow = roundStopValues( + getColorStops(palettes, activePalette?.params?.colorStops || [], activePalette, dataBounds) + ); + + return ( + <> +
+ + { + const isNewPaletteCustom = newPalette.name === CUSTOM_PALETTE; + const newParams: CustomPaletteParams = { + ...activePalette.params, + name: newPalette.name, + colorStops: undefined, + }; + + if (isNewPaletteCustom) { + newParams.colorStops = getColorStops(palettes, [], activePalette, dataBounds); + } + + newParams.stops = getPaletteStops(palettes, newParams, { + prevPalette: + isNewPaletteCustom || isCurrentPaletteCustom ? undefined : newPalette.name, + dataBounds, + }); + + setPalette({ + ...newPalette, + params: newParams, + }); + }} + showCustomPalette + showDynamicColorOnly + /> + + + ['continuity']) => + setPalette( + mergePaletteParams(activePalette, { + continuity, + }) + ) + } + /> + + + {i18n.translate('xpack.lens.table.dynamicColoring.rangeType.label', { + defaultMessage: 'Value type', + })}{' '} + + + } + display="rowCompressed" + > + { + const newRangeType = id.replace( + idPrefix, + '' + ) as RequiredPaletteParamTypes['rangeType']; + + const params: CustomPaletteParams = { rangeType: newRangeType }; + if (isCurrentPaletteCustom) { + const { min: newMin, max: newMax } = getDataMinMax(newRangeType, dataBounds); + const { min: oldMin, max: oldMax } = getDataMinMax( + activePalette.params?.rangeType, + dataBounds + ); + const newColorStops = remapStopsByNewInterval(colorStopsToShow, { + oldInterval: oldMax - oldMin, + newInterval: newMax - newMin, + newMin, + oldMin, + }); + const stops = getPaletteStops( + palettes, + { ...activePalette.params, colorStops: newColorStops, ...params }, + { dataBounds } + ); + params.colorStops = newColorStops; + params.stops = stops; + params.rangeMin = newColorStops[0].stop; + params.rangeMax = newColorStops[newColorStops.length - 1].stop; + } else { + params.stops = getPaletteStops( + palettes, + { ...activePalette.params, ...params }, + { prevPalette: activePalette.name, dataBounds } + ); + } + setPalette(mergePaletteParams(activePalette, params)); + }} + /> + + + { + const params: CustomPaletteParams = { reverse: !activePalette.params?.reverse }; + if (isCurrentPaletteCustom) { + params.colorStops = reversePalette(colorStopsToShow); + params.stops = getPaletteStops( + palettes, + { + ...(activePalette?.params || {}), + colorStops: params.colorStops, + }, + { dataBounds } + ); + } else { + params.stops = reversePalette( + activePalette?.params?.stops || + getPaletteStops( + palettes, + { ...activePalette.params, ...params }, + { prevPalette: activePalette.name, dataBounds } + ) + ); + } + setPalette(mergePaletteParams(activePalette, params)); + }} + > + + + + + + {i18n.translate('xpack.lens.table.dynamicColoring.reverse.label', { + defaultMessage: 'Reverse colors', + })} + + + + + } + > + { + const newParams = getSwitchToCustomParams( + palettes, + activePalette, + { + colorStops, + steps: activePalette.params!.steps || DEFAULT_COLOR_STEPS, + rangeMin: colorStops[0]?.stop, + rangeMax: colorStops[colorStops.length - 1]?.stop, + }, + dataBounds + ); + return setPalette(newParams); + }} + /> + +
+ + ); +} diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_picker.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_picker.tsx new file mode 100644 index 00000000000000..164ed9bf067a66 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_picker.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiColorPalettePicker, EuiColorPalettePickerPaletteProps } from '@elastic/eui'; +import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import { + CUSTOM_PALETTE, + DEFAULT_COLOR_STEPS, + FIXED_PROGRESSION, + defaultPaletteParams, +} from '../../shared_components/coloring/constants'; +import { CustomPaletteParams } from '../../shared_components/coloring/types'; +import { getStopsForFixedMode } from '../../shared_components/coloring/utils'; + +function getCustomPaletteConfig( + palettes: PaletteRegistry, + activePalette: PaletteOutput | undefined +) { + const { id, title } = palettes.get(CUSTOM_PALETTE); + + // Try to generate a palette from the current one + if (activePalette && activePalette.name !== CUSTOM_PALETTE) { + const currentPalette = palettes.get(activePalette.name); + if (currentPalette) { + const stops = currentPalette.getCategoricalColors(DEFAULT_COLOR_STEPS, activePalette?.params); + const palette = activePalette.params?.reverse ? stops.reverse() : stops; + return { + value: id, + title, + type: FIXED_PROGRESSION, + palette, + 'data-test-subj': `custom-palette`, + }; + } + } + // if not possible just show some text + if (!activePalette?.params?.stops) { + return { value: id, title, type: 'text' as const, 'data-test-subj': `custom-palette` }; + } + + // full custom palette + return { + value: id, + title, + type: FIXED_PROGRESSION, + 'data-test-subj': `custom-palette`, + palette: getStopsForFixedMode(activePalette.params.stops, activePalette.params.colorStops), + }; +} + +// Note: this is a special palette picker different from the one in the root shared folder +// ideally these should be merged together, but as for now this holds some custom logic hard to remove +export function PalettePicker({ + palettes, + activePalette, + setPalette, + showCustomPalette, + showDynamicColorOnly, + ...rest +}: { + palettes: PaletteRegistry; + activePalette?: PaletteOutput; + setPalette: (palette: PaletteOutput) => void; + showCustomPalette?: boolean; + showDynamicColorOnly?: boolean; +}) { + const palettesToShow: EuiColorPalettePickerPaletteProps[] = palettes + .getAll() + .filter(({ internal, canDynamicColoring }) => + showDynamicColorOnly ? canDynamicColoring : !internal + ) + .map(({ id, title, getCategoricalColors }) => { + const colors = getCategoricalColors( + DEFAULT_COLOR_STEPS, + id === activePalette?.name ? activePalette?.params : undefined + ); + return { + value: id, + title, + type: FIXED_PROGRESSION, + palette: activePalette?.params?.reverse ? colors.reverse() : colors, + 'data-test-subj': `${id}-palette`, + }; + }); + if (showCustomPalette) { + palettesToShow.push(getCustomPaletteConfig(palettes, activePalette)); + } + return ( + { + setPalette({ + type: 'palette', + name: newPalette, + }); + }} + valueOfSelected={activePalette?.name || defaultPaletteParams.name} + selectionDisplay="palette" + {...rest} + /> + ); +} diff --git a/x-pack/plugins/lens/public/shared_components/coloring/types.ts b/x-pack/plugins/lens/public/shared_components/coloring/types.ts new file mode 100644 index 00000000000000..d9a8edf0ccb62b --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/coloring/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export interface ColorStop { + color: string; + stop: number; +} + +export interface CustomPaletteParams { + name?: string; + reverse?: boolean; + rangeType?: 'number' | 'percent'; + continuity?: 'above' | 'below' | 'all' | 'none'; + progression?: 'fixed'; + rangeMin?: number; + rangeMax?: number; + stops?: ColorStop[]; + colorStops?: ColorStop[]; + steps?: number; +} + +export type RequiredPaletteParamTypes = Required; diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts new file mode 100644 index 00000000000000..8aaab0923584d8 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts @@ -0,0 +1,399 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { + applyPaletteParams, + getContrastColor, + getDataMinMax, + getPaletteStops, + getStepValue, + isValidColor, + mergePaletteParams, + remapStopsByNewInterval, + reversePalette, + roundStopValues, +} from './utils'; + +describe('applyPaletteParams', () => { + const paletteRegistry = chartPluginMock.createPaletteRegistry(); + it('should return a palette stops array only by the name', () => { + expect( + applyPaletteParams( + paletteRegistry, + { name: 'default', type: 'palette', params: { name: 'default' } }, + { min: 0, max: 100 } + ) + ).toEqual([ + // stops are 0 and 50 by with a 20 offset (100 divided by 5 steps) for display + // the mock palette service has only 2 colors so tests are a bit off by that + { color: 'red', stop: 20 }, + { color: 'black', stop: 70 }, + ]); + }); + + it('should return a palette stops array reversed', () => { + expect( + applyPaletteParams( + paletteRegistry, + { name: 'default', type: 'palette', params: { name: 'default', reverse: true } }, + { min: 0, max: 100 } + ) + ).toEqual([ + { color: 'black', stop: 20 }, + { color: 'red', stop: 70 }, + ]); + }); +}); + +describe('remapStopsByNewInterval', () => { + it('should correctly remap the current palette from 0..1 to 0...100', () => { + expect( + remapStopsByNewInterval( + [ + { color: 'black', stop: 0 }, + { color: 'green', stop: 0.5 }, + { color: 'red', stop: 0.9 }, + ], + { newInterval: 100, oldInterval: 1, newMin: 0, oldMin: 0 } + ) + ).toEqual([ + { color: 'black', stop: 0 }, + { color: 'green', stop: 50 }, + { color: 'red', stop: 90 }, + ]); + + // now test the other way around + expect( + remapStopsByNewInterval( + [ + { color: 'black', stop: 0 }, + { color: 'green', stop: 50 }, + { color: 'red', stop: 90 }, + ], + { newInterval: 1, oldInterval: 100, newMin: 0, oldMin: 0 } + ) + ).toEqual([ + { color: 'black', stop: 0 }, + { color: 'green', stop: 0.5 }, + { color: 'red', stop: 0.9 }, + ]); + }); + + it('should correctly handle negative numbers to/from', () => { + expect( + remapStopsByNewInterval( + [ + { color: 'black', stop: -100 }, + { color: 'green', stop: -50 }, + { color: 'red', stop: -1 }, + ], + { newInterval: 100, oldInterval: 100, newMin: 0, oldMin: -100 } + ) + ).toEqual([ + { color: 'black', stop: 0 }, + { color: 'green', stop: 50 }, + { color: 'red', stop: 99 }, + ]); + + // now map the other way around + expect( + remapStopsByNewInterval( + [ + { color: 'black', stop: 0 }, + { color: 'green', stop: 50 }, + { color: 'red', stop: 99 }, + ], + { newInterval: 100, oldInterval: 100, newMin: -100, oldMin: 0 } + ) + ).toEqual([ + { color: 'black', stop: -100 }, + { color: 'green', stop: -50 }, + { color: 'red', stop: -1 }, + ]); + + // and test also palettes that also contains negative values + expect( + remapStopsByNewInterval( + [ + { color: 'black', stop: -50 }, + { color: 'green', stop: 0 }, + { color: 'red', stop: 50 }, + ], + { newInterval: 100, oldInterval: 100, newMin: 0, oldMin: -50 } + ) + ).toEqual([ + { color: 'black', stop: 0 }, + { color: 'green', stop: 50 }, + { color: 'red', stop: 100 }, + ]); + }); +}); + +describe('getDataMinMax', () => { + it('should pick the correct min/max based on the current range type', () => { + expect(getDataMinMax('percent', { min: -100, max: 0 })).toEqual({ min: 0, max: 100 }); + }); + + it('should pick the correct min/max apply percent by default', () => { + expect(getDataMinMax(undefined, { min: -100, max: 0 })).toEqual({ min: 0, max: 100 }); + }); +}); + +describe('getPaletteStops', () => { + const paletteRegistry = chartPluginMock.createPaletteRegistry(); + it('should correctly compute a predefined palette stops definition from only the name', () => { + expect( + getPaletteStops(paletteRegistry, { name: 'mock' }, { dataBounds: { min: 0, max: 100 } }) + ).toEqual([ + { color: 'blue', stop: 20 }, + { color: 'yellow', stop: 70 }, + ]); + }); + + it('should correctly compute a predefined palette stops definition from explicit prevPalette (override)', () => { + expect( + getPaletteStops( + paletteRegistry, + { name: 'default' }, + { dataBounds: { min: 0, max: 100 }, prevPalette: 'mock' } + ) + ).toEqual([ + { color: 'blue', stop: 20 }, + { color: 'yellow', stop: 70 }, + ]); + }); + + it('should infer the domain from dataBounds but start from 0', () => { + expect( + getPaletteStops( + paletteRegistry, + { name: 'default', rangeType: 'number' }, + { dataBounds: { min: 1, max: 11 }, prevPalette: 'mock' } + ) + ).toEqual([ + { color: 'blue', stop: 2 }, + { color: 'yellow', stop: 7 }, + ]); + }); + + it('should override the minStop when requested', () => { + expect( + getPaletteStops( + paletteRegistry, + { name: 'default', rangeType: 'number' }, + { dataBounds: { min: 1, max: 11 }, mapFromMinValue: true } + ) + ).toEqual([ + { color: 'red', stop: 1 }, + { color: 'black', stop: 6 }, + ]); + }); + + it('should compute a display stop palette from custom colorStops defined by the user', () => { + expect( + getPaletteStops( + paletteRegistry, + { + name: 'custom', + rangeType: 'number', + colorStops: [ + { color: 'green', stop: 0 }, + { color: 'blue', stop: 40 }, + { color: 'red', stop: 80 }, + ], + }, + { dataBounds: { min: 0, max: 100 } } + ) + ).toEqual([ + { color: 'green', stop: 40 }, + { color: 'blue', stop: 80 }, + { color: 'red', stop: 100 }, + ]); + }); + + it('should compute a display stop palette from custom colorStops defined by the user - handle stop at the end', () => { + expect( + getPaletteStops( + paletteRegistry, + { + name: 'custom', + rangeType: 'number', + colorStops: [ + { color: 'green', stop: 0 }, + { color: 'blue', stop: 40 }, + { color: 'red', stop: 100 }, + ], + }, + { dataBounds: { min: 0, max: 100 } } + ) + ).toEqual([ + { color: 'green', stop: 40 }, + { color: 'blue', stop: 100 }, + { color: 'red', stop: 101 }, + ]); + }); + + it('should compute a display stop palette from custom colorStops defined by the user - handle stop at the end (fractional)', () => { + expect( + getPaletteStops( + paletteRegistry, + { + name: 'custom', + rangeType: 'number', + colorStops: [ + { color: 'green', stop: 0 }, + { color: 'blue', stop: 0.4 }, + { color: 'red', stop: 1 }, + ], + }, + { dataBounds: { min: 0, max: 1 } } + ) + ).toEqual([ + { color: 'green', stop: 0.4 }, + { color: 'blue', stop: 1 }, + { color: 'red', stop: 2 }, + ]); + }); + + it('should compute a display stop palette from custom colorStops defined by the user - stretch the stops to 100% percent', () => { + expect( + getPaletteStops( + paletteRegistry, + { + name: 'custom', + colorStops: [ + { color: 'green', stop: 0 }, + { color: 'blue', stop: 0.4 }, + { color: 'red', stop: 1 }, + ], + }, + { dataBounds: { min: 0, max: 1 } } + ) + ).toEqual([ + { color: 'green', stop: 0.4 }, + { color: 'blue', stop: 1 }, + { color: 'red', stop: 100 }, // default rangeType is percent, hence stretch to 100% + ]); + }); +}); + +describe('reversePalette', () => { + it('should correctly reverse color and stops', () => { + expect( + reversePalette([ + { color: 'red', stop: 0 }, + { color: 'green', stop: 0.5 }, + { color: 'blue', stop: 0.9 }, + ]) + ).toEqual([ + { color: 'blue', stop: 0 }, + { color: 'green', stop: 0.5 }, + { color: 'red', stop: 0.9 }, + ]); + }); +}); + +describe('mergePaletteParams', () => { + it('should return a full palette', () => { + expect(mergePaletteParams({ type: 'palette', name: 'myPalette' }, { reverse: true })).toEqual({ + type: 'palette', + name: 'myPalette', + params: { reverse: true }, + }); + }); +}); + +describe('isValidColor', () => { + it('should return ok for valid hex color notation', () => { + expect(isValidColor('#fff')).toBe(true); + expect(isValidColor('#ffffff')).toBe(true); + expect(isValidColor('#ffffffaa')).toBe(true); + }); + + it('should return false for non valid strings', () => { + expect(isValidColor('')).toBe(false); + expect(isValidColor('#')).toBe(false); + expect(isValidColor('#ff')).toBe(false); + expect(isValidColor('123')).toBe(false); + expect(isValidColor('rgb(1, 1, 1)')).toBe(false); + expect(isValidColor('rgba(1, 1, 1, 0)')).toBe(false); + expect(isValidColor('#ffffffgg')).toBe(false); + expect(isValidColor('#fff00')).toBe(false); + // this version of chroma does not support hex4 format + expect(isValidColor('#fffa')).toBe(false); + }); +}); + +describe('roundStopValues', () => { + it('should round very long values', () => { + expect(roundStopValues([{ color: 'red', stop: 0.1515 }])).toEqual([ + { color: 'red', stop: 0.15 }, + ]); + }); +}); + +describe('getStepValue', () => { + it('should compute the next step based on the last 2 stops', () => { + expect( + getStepValue( + // first arg is taken as max reference + [ + { color: 'red', stop: 0 }, + { color: 'red', stop: 50 }, + ], + [ + { color: 'red', stop: 0 }, + { color: 'red', stop: 50 }, + ], + 100 + ) + ).toBe(50); + + expect( + getStepValue( + // first arg is taken as max reference + [ + { color: 'red', stop: 0 }, + { color: 'red', stop: 80 }, + ], + [ + { color: 'red', stop: 0 }, + { color: 'red', stop: 50 }, + ], + 90 + ) + ).toBe(10); // 90 - 80 + + expect( + getStepValue( + // first arg is taken as max reference + [ + { color: 'red', stop: 0 }, + { color: 'red', stop: 100 }, + ], + [ + { color: 'red', stop: 0 }, + { color: 'red', stop: 50 }, + ], + 100 + ) + ).toBe(1); + }); +}); + +describe('getContrastColor', () => { + it('should pick the light color when the passed one is dark', () => { + expect(getContrastColor('#000', true)).toBe('#ffffff'); + expect(getContrastColor('#000', false)).toBe('#ffffff'); + }); + + it('should pick the dark color when the passed one is light', () => { + expect(getContrastColor('#fff', true)).toBe('#000000'); + expect(getContrastColor('#fff', false)).toBe('#000000'); + }); +}); diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts new file mode 100644 index 00000000000000..89fceec533493f --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts @@ -0,0 +1,308 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import chroma from 'chroma-js'; +import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps/theme'; +import { isColorDark } from '@elastic/eui'; +import { + CUSTOM_PALETTE, + defaultPaletteParams, + DEFAULT_COLOR_STEPS, + DEFAULT_MAX_STOP, + DEFAULT_MIN_STOP, +} from './constants'; +import { CustomPaletteParams, ColorStop } from './types'; + +/** + * Some name conventions here: + * * `displayStops` => It's an additional transformation of `stops` into a [0, N] domain for the EUIPaletteDisplay component. + * * `stops` => final steps used to table coloring. It is a rightShift of the colorStops + * * `colorStops` => user's color stop inputs. Used to compute range min. + * + * When the user inputs the colorStops, they are designed to be the initial part of the color segment, + * so the next stops indicate where the previous stop ends. + * Both table coloring logic and EuiPaletteDisplay format implementation works differently than our current `colorStops`, + * by having the stop values at the end of each color segment rather than at the beginning: `stops` values are computed by a rightShift of `colorStops`. + * EuiPaletteDisplay has an additional requirement as it is always mapped against a domain [0, N]: from `stops` the `displayStops` are computed with + * some continuity enrichment and a remap against a [0, 100] domain to make the palette component work ok. + * + * These naming conventions would be useful to track the code flow in this feature as multiple transformations are happening + * for a single change. + */ + +export function applyPaletteParams( + palettes: PaletteRegistry, + activePalette: PaletteOutput, + dataBounds: { min: number; max: number } +) { + // make a copy of it as they have to be manipulated later on + let displayStops = getPaletteStops(palettes, activePalette?.params || {}, { + dataBounds, + }); + + if (activePalette?.params?.reverse && activePalette?.params?.name !== CUSTOM_PALETTE) { + displayStops = reversePalette(displayStops); + } + return displayStops; +} + +// Need to shift the Custom palette in order to correctly visualize it when in display mode +function shiftPalette(stops: ColorStop[], max: number) { + // shift everything right and add an additional stop at the end + const result = stops.map((entry, i, array) => ({ + ...entry, + stop: i + 1 < array.length ? array[i + 1].stop : max, + })); + if (stops[stops.length - 1].stop === max) { + // extends the range by a fair amount to make it work the extra case for the last stop === max + const computedStep = getStepValue(stops, result, max) || 1; + // do not go beyond the unit step in this case + const step = Math.min(1, computedStep); + result[stops.length - 1].stop = max + step; + } + return result; +} + +// Utility to remap color stops within new domain +export function remapStopsByNewInterval( + controlStops: ColorStop[], + { + newInterval, + oldInterval, + newMin, + oldMin, + }: { newInterval: number; oldInterval: number; newMin: number; oldMin: number } +) { + return (controlStops || []).map(({ color, stop }) => { + return { + color, + stop: newMin + ((stop - oldMin) * newInterval) / oldInterval, + }; + }); +} + +function getOverallMinMax( + params: CustomPaletteParams | undefined, + dataBounds: { min: number; max: number } +) { + const { min: dataMin, max: dataMax } = getDataMinMax(params?.rangeType, dataBounds); + const minStopValue = params?.colorStops?.[0]?.stop ?? Infinity; + const maxStopValue = params?.colorStops?.[params.colorStops.length - 1]?.stop ?? -Infinity; + const overallMin = Math.min(dataMin, minStopValue); + const overallMax = Math.max(dataMax, maxStopValue); + return { min: overallMin, max: overallMax }; +} + +export function getDataMinMax( + rangeType: CustomPaletteParams['rangeType'] | undefined, + dataBounds: { min: number; max: number } +) { + const dataMin = rangeType === 'number' ? dataBounds.min : DEFAULT_MIN_STOP; + const dataMax = rangeType === 'number' ? dataBounds.max : DEFAULT_MAX_STOP; + return { min: dataMin, max: dataMax }; +} + +/** + * This is a generic function to compute stops from the current parameters. + */ +export function getPaletteStops( + palettes: PaletteRegistry, + activePaletteParams: CustomPaletteParams, + // used to customize color resolution + { + prevPalette, + dataBounds, + mapFromMinValue, + }: { prevPalette?: string; dataBounds: { min: number; max: number }; mapFromMinValue?: boolean } +) { + const { min: minValue, max: maxValue } = getOverallMinMax(activePaletteParams, dataBounds); + const interval = maxValue - minValue; + const { stops: currentStops, ...otherParams } = activePaletteParams || {}; + + if (activePaletteParams.name === 'custom' && activePaletteParams?.colorStops) { + // need to generate the palette from the existing controlStops + return shiftPalette(activePaletteParams.colorStops, maxValue); + } + // generate a palette from predefined ones and customize the domain + const colorStopsFromPredefined = palettes + .get(prevPalette || activePaletteParams?.name || defaultPaletteParams.name) + .getCategoricalColors(defaultPaletteParams.steps, otherParams); + + const newStopsMin = mapFromMinValue ? minValue : interval / defaultPaletteParams.steps; + + const stops = remapStopsByNewInterval( + colorStopsFromPredefined.map((color, index) => ({ color, stop: index })), + { + newInterval: interval, + oldInterval: colorStopsFromPredefined.length, + newMin: newStopsMin, + oldMin: 0, + } + ); + return stops; +} + +export function reversePalette(paletteColorRepresentation: ColorStop[] = []) { + const stops = paletteColorRepresentation.map(({ stop }) => stop); + return paletteColorRepresentation + .map(({ color }, i) => ({ + color, + stop: stops[paletteColorRepresentation.length - i - 1], + })) + .reverse(); +} + +export function mergePaletteParams( + activePalette: PaletteOutput, + newParams: CustomPaletteParams +): PaletteOutput { + return { + ...activePalette, + params: { + ...activePalette.params, + ...newParams, + }, + }; +} + +function isValidPonyfill(colorString: string) { + // we're using an old version of chroma without the valid function + try { + chroma(colorString); + return true; + } catch (e) { + return false; + } +} + +export function isValidColor(colorString: string) { + // chroma can handle also hex values with alpha channel/transparency + // chroma accepts also hex without #, so test for it + return colorString !== '' && /^#/.test(colorString) && isValidPonyfill(colorString); +} + +export function roundStopValues(colorStops: ColorStop[]) { + return colorStops.map(({ color, stop }) => { + const roundedStop = Number(stop.toFixed(2)); + return { color, stop: roundedStop }; + }); +} + +// very simple heuristic: pick last two stops and compute a new stop based on the same distance +// if the new stop is above max, then reduce the step to reach max, or if zero then just 1. +// +// it accepts two series of stops as the function is used also when computing stops from colorStops +export function getStepValue(colorStops: ColorStop[], newColorStops: ColorStop[], max: number) { + const length = newColorStops.length; + // workout the steps from the last 2 items + const dataStep = newColorStops[length - 1].stop - newColorStops[length - 2].stop || 1; + let step = Number(dataStep.toFixed(2)); + if (max < colorStops[length - 1].stop + step) { + const diffToMax = max - colorStops[length - 1].stop; + // if the computed step goes way out of bound, fallback to 1, otherwise reach max + step = diffToMax > 0 ? diffToMax : 1; + } + return step; +} + +export function getSwitchToCustomParams( + palettes: PaletteRegistry, + activePalette: PaletteOutput, + newParams: CustomPaletteParams, + dataBounds: { min: number; max: number } +) { + // if it's already a custom palette just return the params + if (activePalette?.params?.name === CUSTOM_PALETTE) { + const stops = getPaletteStops( + palettes, + { + steps: DEFAULT_COLOR_STEPS, + ...activePalette.params, + ...newParams, + }, + { + dataBounds, + } + ); + return mergePaletteParams(activePalette, { + ...newParams, + stops, + }); + } + // prepare everything to switch to custom palette + const newPaletteParams = { + steps: DEFAULT_COLOR_STEPS, + ...activePalette.params, + ...newParams, + name: CUSTOM_PALETTE, + }; + + const stops = getPaletteStops(palettes, newPaletteParams, { + prevPalette: newPaletteParams.colorStops ? undefined : activePalette.name, + dataBounds, + }); + return mergePaletteParams( + { name: CUSTOM_PALETTE, type: 'palette' }, + { + ...newPaletteParams, + stops, + } + ); +} + +export function getColorStops( + palettes: PaletteRegistry, + colorStops: Required['stops'], + activePalette: PaletteOutput, + dataBounds: { min: number; max: number } +) { + // just forward the current stops if custom + if (activePalette?.name === CUSTOM_PALETTE) { + return colorStops; + } + // for predefined palettes create some stops, then drop the last one. + // we're using these as starting point for the user + let freshColorStops = getPaletteStops( + palettes, + { ...activePalette?.params }, + // mapFromMinValue is a special flag to offset the stops values + // used here to avoid a new remap/left shift + { dataBounds, mapFromMinValue: true } + ); + if (activePalette?.params?.reverse) { + freshColorStops = reversePalette(freshColorStops); + } + return freshColorStops; +} + +export function getContrastColor(color: string, isDarkTheme: boolean) { + const darkColor = isDarkTheme ? euiDarkVars.euiColorInk : euiLightVars.euiColorInk; + const lightColor = isDarkTheme ? euiDarkVars.euiColorGhost : euiLightVars.euiColorGhost; + return isColorDark(...chroma(color).rgb()) ? lightColor : darkColor; +} + +/** + * Same as stops, but remapped against a range 0-100 + */ +export function getStopsForFixedMode(stops: ColorStop[], colorStops?: ColorStop[]) { + const referenceStops = + colorStops || stops.map(({ color }, index) => ({ color, stop: 20 * index })); + const fallbackStops = stops; + + // what happens when user set two stops with the same value? we'll fallback to the display interval + const oldInterval = + referenceStops[referenceStops.length - 1].stop - referenceStops[0].stop || + fallbackStops[fallbackStops.length - 1].stop - fallbackStops[0].stop; + + return remapStopsByNewInterval(stops, { + newInterval: 100, + oldInterval, + newMin: 0, + oldMin: referenceStops[0].stop, + }); +} diff --git a/x-pack/plugins/lens/public/shared_components/debounced_value.ts b/x-pack/plugins/lens/public/shared_components/debounced_value.ts index 5447384ce38ea5..1f8ba0fa765b2e 100644 --- a/x-pack/plugins/lens/public/shared_components/debounced_value.ts +++ b/x-pack/plugins/lens/public/shared_components/debounced_value.ts @@ -6,7 +6,7 @@ */ import { useState, useMemo, useEffect, useRef } from 'react'; -import _ from 'lodash'; +import { debounce } from 'lodash'; /** * Debounces value changes and updates inputValue on root state changes if no debounced changes @@ -27,7 +27,7 @@ export const useDebouncedValue = ({ const initialValue = useRef(value); const onChangeDebounced = useMemo(() => { - const callback = _.debounce((val: T) => { + const callback = debounce((val: T) => { onChange(val); unflushedChanges.current = false; }, 256); diff --git a/x-pack/plugins/lens/public/shared_components/helpers.ts b/x-pack/plugins/lens/public/shared_components/helpers.ts new file mode 100644 index 00000000000000..a9f35757c4cbfe --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/helpers.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRef } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; + +export const useDebounceWithOptions = ( + fn: Function, + { skipFirstRender }: { skipFirstRender: boolean } = { skipFirstRender: false }, + ms?: number | undefined, + deps?: React.DependencyList | undefined +) => { + const isFirstRender = useRef(true); + const newDeps = [...(deps || []), isFirstRender]; + + return useDebounce( + () => { + if (skipFirstRender && isFirstRender.current) { + isFirstRender.current = false; + return; + } + return fn(); + }, + ms, + newDeps + ); +}; diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index ae57da976a8817..cf8536884acdf8 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -9,4 +9,7 @@ export * from './empty_placeholder'; export { ToolbarPopoverProps, ToolbarPopover } from './toolbar_popover'; export { LegendSettingsPopover } from './legend_settings_popover'; export { PalettePicker } from './palette_picker'; +export { TooltipWrapper } from './tooltip_wrapper'; +export * from './coloring'; export { useDebouncedValue } from './debounced_value'; +export * from './helpers'; diff --git a/x-pack/plugins/lens/public/shared_components/palette_picker.tsx b/x-pack/plugins/lens/public/shared_components/palette_picker.tsx index b15a6749d4c2d1..6424dc8143f957 100644 --- a/x-pack/plugins/lens/public/shared_components/palette_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/palette_picker.tsx @@ -7,10 +7,9 @@ import React from 'react'; import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; -import { EuiColorPalettePicker } from '@elastic/eui'; +import { EuiColorPalettePicker, EuiColorPalettePickerPaletteProps } from '@elastic/eui'; import { EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { NativeRenderer } from '../native_renderer'; export function PalettePicker({ palettes, @@ -21,6 +20,20 @@ export function PalettePicker({ activePalette?: PaletteOutput; setPalette: (palette: PaletteOutput) => void; }) { + const palettesToShow: EuiColorPalettePickerPaletteProps[] = palettes + .getAll() + .filter(({ internal }) => !internal) + .map(({ id, title, getCategoricalColors }) => { + return { + value: id, + title, + type: 'fixed', + palette: getCategoricalColors( + 10, + id === activePalette?.name ? activePalette?.params : undefined + ), + }; + }); return ( !internal) - .map(({ id, title, getColors }) => { - return { - value: id, - title, - type: 'fixed', - palette: getColors( - 10, - id === activePalette?.name ? activePalette?.params : undefined - ), - }; - })} + palettes={palettesToShow} onChange={(newPalette) => { setPalette({ type: 'palette', @@ -56,21 +56,6 @@ export function PalettePicker({ valueOfSelected={activePalette?.name || 'default'} selectionDisplay={'palette'} /> - {activePalette && palettes.get(activePalette.name).renderEditor && ( - { - setPalette({ - type: 'palette', - name: activePalette.name, - params: updater(activePalette.params), - }); - }, - }} - /> - )} ); diff --git a/x-pack/plugins/lens/public/xy_visualization/tooltip_wrapper.tsx b/x-pack/plugins/lens/public/shared_components/tooltip_wrapper.tsx similarity index 100% rename from x-pack/plugins/lens/public/xy_visualization/tooltip_wrapper.tsx rename to x-pack/plugins/lens/public/shared_components/tooltip_wrapper.tsx diff --git a/x-pack/plugins/lens/public/state_management/app_slice.ts b/x-pack/plugins/lens/public/state_management/app_slice.ts new file mode 100644 index 00000000000000..29d5b0bee843f6 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/app_slice.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; +import { LensAppState } from './types'; + +export const initialState: LensAppState = { + searchSessionId: '', + filters: [], + query: { language: 'kuery', query: '' }, + resolvedDateRange: { fromDate: '', toDate: '' }, + + indexPatternsForTopNav: [], + isSaveable: false, + isAppLoading: false, + isLinkedToOriginatingApp: false, +}; + +export const appSlice = createSlice({ + name: 'app', + initialState, + reducers: { + setState: (state, { payload }: PayloadAction>) => { + return { + ...state, + ...payload, + }; + }, + onChangeFromEditorFrame: (state, { payload }: PayloadAction>) => { + return { + ...state, + ...payload, + }; + }, + onActiveDataChange: (state, { payload }: PayloadAction>) => { + if (!isEqual(state.activeData, payload?.activeData)) { + return { + ...state, + ...payload, + }; + } + return state; + }, + navigateAway: (state) => state, + }, +}); + +export const reducer = { + app: appSlice.reducer, +}; diff --git a/x-pack/plugins/lens/public/state_management/external_context_middleware.ts b/x-pack/plugins/lens/public/state_management/external_context_middleware.ts new file mode 100644 index 00000000000000..0743dce73eb33f --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/external_context_middleware.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { delay, finalize, switchMap, tap } from 'rxjs/operators'; +import { debounce, isEqual } from 'lodash'; +import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit'; +import { trackUiEvent } from '../lens_ui_telemetry'; + +import { + waitUntilNextSessionCompletes$, + DataPublicPluginStart, +} from '../../../../../src/plugins/data/public'; +import { setState, LensGetState, LensDispatch } from '.'; +import { LensAppState } from './types'; +import { getResolvedDateRange } from '../utils'; + +export const externalContextMiddleware = (data: DataPublicPluginStart) => ( + store: MiddlewareAPI +) => { + const unsubscribeFromExternalContext = subscribeToExternalContext( + data, + store.getState, + store.dispatch + ); + return (next: Dispatch) => (action: PayloadAction>) => { + if (action.type === 'app/navigateAway') { + unsubscribeFromExternalContext(); + } + next(action); + }; +}; + +function subscribeToExternalContext( + data: DataPublicPluginStart, + getState: LensGetState, + dispatch: LensDispatch +) { + const { query: queryService, search } = data; + const { filterManager } = queryService; + + const dispatchFromExternal = (searchSessionId = search.session.start()) => { + const globalFilters = filterManager.getFilters(); + const filters = isEqual(getState().app.filters, globalFilters) + ? null + : { filters: globalFilters }; + dispatch( + setState({ + searchSessionId, + ...filters, + resolvedDateRange: getResolvedDateRange(queryService.timefilter.timefilter), + }) + ); + }; + + const debounceDispatchFromExternal = debounce(dispatchFromExternal, 100); + + const sessionSubscription = search.session + .getSession$() + // wait for a tick to filter/timerange subscribers the chance to update the session id in the state + .pipe(delay(0)) + // then update if it didn't get updated yet + .subscribe((newSessionId?: string) => { + if (newSessionId && getState().app.searchSessionId !== newSessionId) { + debounceDispatchFromExternal(newSessionId); + } + }); + + const filterSubscription = filterManager.getUpdates$().subscribe({ + next: () => { + debounceDispatchFromExternal(); + trackUiEvent('app_filters_updated'); + }, + }); + + const timeSubscription = data.query.timefilter.timefilter.getTimeUpdate$().subscribe({ + next: () => { + debounceDispatchFromExternal(); + }, + }); + + const autoRefreshSubscription = data.query.timefilter.timefilter + .getAutoRefreshFetch$() + .pipe( + tap(() => { + debounceDispatchFromExternal(); + }), + switchMap((done) => + // best way in lens to estimate that all panels are updated is to rely on search session service state + waitUntilNextSessionCompletes$(search.session).pipe(finalize(done)) + ) + ) + .subscribe(); + return () => { + filterSubscription.unsubscribe(); + timeSubscription.unsubscribe(); + autoRefreshSubscription.unsubscribe(); + sessionSubscription.unsubscribe(); + }; +} diff --git a/x-pack/plugins/lens/public/state_management/index.ts b/x-pack/plugins/lens/public/state_management/index.ts new file mode 100644 index 00000000000000..429978e60756b8 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/index.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 { configureStore, DeepPartial, getDefaultMiddleware } from '@reduxjs/toolkit'; +import logger from 'redux-logger'; +import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; +import { appSlice, initialState } from './app_slice'; +import { timeRangeMiddleware } from './time_range_middleware'; +import { externalContextMiddleware } from './external_context_middleware'; + +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { LensAppState, LensState } from './types'; +export * from './types'; + +export const reducer = { + app: appSlice.reducer, +}; + +export const { + setState, + navigateAway, + onChangeFromEditorFrame, + onActiveDataChange, +} = appSlice.actions; + +export const getPreloadedState = (initializedState: Partial) => { + const state = { + app: { + ...initialState, + ...initializedState, + }, + } as DeepPartial; + return state; +}; + +type PreloadedState = ReturnType; + +export const makeConfigureStore = ( + preloadedState: PreloadedState, + { data }: { data: DataPublicPluginStart } +) => { + const middleware = [ + ...getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [ + 'app/setState', + 'app/onChangeFromEditorFrame', + 'app/onActiveDataChange', + 'app/navigateAway', + ], + }, + }), + timeRangeMiddleware(data), + externalContextMiddleware(data), + ]; + if (process.env.NODE_ENV === 'development') middleware.push(logger); + + return configureStore({ + reducer, + middleware, + preloadedState, + }); +}; + +export type LensRootStore = ReturnType; + +export type LensDispatch = LensRootStore['dispatch']; +export type LensGetState = LensRootStore['getState']; +export type LensRootState = ReturnType; + +export const useLensDispatch = () => useDispatch(); +export const useLensSelector: TypedUseSelectorHook = useSelector; diff --git a/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts b/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts new file mode 100644 index 00000000000000..4145f8ed5e52cf --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// /* +// * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// * or more contributor license agreements. Licensed under the Elastic License +// * 2.0; you may not use this file except in compliance with the Elastic License +// * 2.0. +// */ + +import { timeRangeMiddleware } from './time_range_middleware'; + +import { Observable, Subject } from 'rxjs'; +import { DataPublicPluginStart, esFilters } from '../../../../../src/plugins/data/public'; +import moment from 'moment'; +import { initialState } from './app_slice'; +import { LensAppState } from './types'; +import { PayloadAction } from '@reduxjs/toolkit'; +import { Document } from '../persistence'; + +const sessionIdSubject = new Subject(); + +function createMockSearchService() { + let sessionIdCounter = 1; + return { + session: { + start: jest.fn(() => `sessionId-${sessionIdCounter++}`), + clear: jest.fn(), + getSessionId: jest.fn(() => `sessionId-${sessionIdCounter}`), + getSession$: jest.fn(() => sessionIdSubject.asObservable()), + }, + }; +} + +function createMockFilterManager() { + const unsubscribe = jest.fn(); + + let subscriber: () => void; + let filters: unknown = []; + + return { + getUpdates$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + setFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), + setAppFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), + getFilters: () => filters, + getGlobalFilters: () => { + // @ts-ignore + return filters.filter(esFilters.isFilterPinned); + }, + removeAll: () => { + filters = []; + subscriber(); + }, + }; +} + +function createMockQueryString() { + return { + getQuery: jest.fn(() => ({ query: '', language: 'kuery' })), + setQuery: jest.fn(), + getDefaultQuery: jest.fn(() => ({ query: '', language: 'kuery' })), + }; +} + +function createMockTimefilter() { + const unsubscribe = jest.fn(); + + let timeFilter = { from: 'now-7d', to: 'now' }; + let subscriber: () => void; + return { + getTime: jest.fn(() => timeFilter), + setTime: jest.fn((newTimeFilter) => { + timeFilter = newTimeFilter; + if (subscriber) { + subscriber(); + } + }), + getTimeUpdate$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + calculateBounds: jest.fn(() => ({ + min: moment('2021-01-10T04:00:00.000Z'), + max: moment('2021-01-10T08:00:00.000Z'), + })), + getBounds: jest.fn(() => timeFilter), + getRefreshInterval: () => {}, + getRefreshIntervalDefaults: () => {}, + getAutoRefreshFetch$: () => new Observable(), + }; +} + +function makeDefaultData(): jest.Mocked { + return ({ + query: { + filterManager: createMockFilterManager(), + timefilter: { + timefilter: createMockTimefilter(), + }, + queryString: createMockQueryString(), + state$: new Observable(), + }, + indexPatterns: { + get: jest.fn((id) => { + return new Promise((resolve) => resolve({ id })); + }), + }, + search: createMockSearchService(), + nowProvider: { + get: jest.fn(), + }, + } as unknown) as DataPublicPluginStart; +} + +const createMiddleware = (data: DataPublicPluginStart) => { + const middleware = timeRangeMiddleware(data); + const store = { + getState: jest.fn(() => ({ app: initialState })), + dispatch: jest.fn(), + }; + const next = jest.fn(); + + const invoke = (action: PayloadAction>) => middleware(store)(next)(action); + + return { store, next, invoke }; +}; + +describe('timeRangeMiddleware', () => { + describe('time update', () => { + it('does update the searchSessionId when the state changes and too much time passed', () => { + const data = makeDefaultData(); + (data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000)); + (data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ + from: 'now-2m', + to: 'now', + }); + (data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({ + min: moment(Date.now() - 100000), + max: moment(Date.now() - 30000), + }); + const { next, invoke, store } = createMiddleware(data); + const action = { + type: 'app/setState', + payload: { lastKnownDoc: ('new' as unknown) as Document }, + }; + invoke(action); + expect(store.dispatch).toHaveBeenCalledWith({ + payload: { + resolvedDateRange: { + fromDate: '2021-01-10T04:00:00.000Z', + toDate: '2021-01-10T08:00:00.000Z', + }, + searchSessionId: 'sessionId-1', + }, + type: 'app/setState', + }); + expect(next).toHaveBeenCalledWith(action); + }); + it('does not update the searchSessionId when the state changes and too little time has passed', () => { + const data = makeDefaultData(); + // time range is 100,000ms ago to 300ms ago (that's a lag of .3 percent, not enough to trigger a session update) + (data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 300)); + (data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ + from: 'now-2m', + to: 'now', + }); + (data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({ + min: moment(Date.now() - 100000), + max: moment(Date.now() - 300), + }); + const { next, invoke, store } = createMiddleware(data); + const action = { + type: 'app/setState', + payload: { lastKnownDoc: ('new' as unknown) as Document }, + }; + invoke(action); + expect(store.dispatch).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(action); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/state_management/time_range_middleware.ts b/x-pack/plugins/lens/public/state_management/time_range_middleware.ts new file mode 100644 index 00000000000000..a6c868be605650 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/time_range_middleware.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEqual } from 'lodash'; +import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit'; +import moment from 'moment'; + +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { setState, LensDispatch } from '.'; +import { LensAppState } from './types'; +import { getResolvedDateRange, containsDynamicMath, TIME_LAG_PERCENTAGE_LIMIT } from '../utils'; + +export const timeRangeMiddleware = (data: DataPublicPluginStart) => (store: MiddlewareAPI) => { + return (next: Dispatch) => (action: PayloadAction>) => { + // if document was modified or sessionId check if too much time passed to update searchSessionId + if ( + action.payload?.lastKnownDoc && + !isEqual(action.payload?.lastKnownDoc, store.getState().app.lastKnownDoc) + ) { + updateTimeRange(data, store.dispatch); + } + next(action); + }; +}; +function updateTimeRange(data: DataPublicPluginStart, dispatch: LensDispatch) { + const timefilter = data.query.timefilter.timefilter; + const unresolvedTimeRange = timefilter.getTime(); + if ( + !containsDynamicMath(unresolvedTimeRange.from) && + !containsDynamicMath(unresolvedTimeRange.to) + ) { + return; + } + + const { min, max } = timefilter.getBounds(); + + if (!min || !max) { + // bounds not fully specified, bailing out + return; + } + + // calculate length of currently configured range in ms + const timeRangeLength = moment.duration(max.diff(min)).asMilliseconds(); + + // calculate lag of managed "now" for date math + const nowDiff = Date.now() - data.nowProvider.get().valueOf(); + + // if the lag is signifcant, start a new session to clear the cache + if (nowDiff > timeRangeLength * TIME_LAG_PERCENTAGE_LIMIT) { + dispatch( + setState({ + searchSessionId: data.search.session.start(), + resolvedDateRange: getResolvedDateRange(timefilter), + }) + ); + } +} diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts new file mode 100644 index 00000000000000..87045d15cc9946 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/types.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 { Filter, IndexPattern, Query, SavedQuery } from '../../../../../src/plugins/data/public'; +import { Document } from '../persistence'; + +import { TableInspectorAdapter } from '../editor_frame_service/types'; +import { DateRange } from '../../common'; + +export interface LensAppState { + persistedDoc?: Document; + lastKnownDoc?: Document; + + // index patterns used to determine which filters are available in the top nav. + indexPatternsForTopNav: IndexPattern[]; + // Determines whether the lens editor shows the 'save and return' button, and the originating app breadcrumb. + isLinkedToOriginatingApp?: boolean; + isSaveable: boolean; + activeData?: TableInspectorAdapter; + + isAppLoading: boolean; + query: Query; + filters: Filter[]; + savedQuery?: SavedQuery; + searchSessionId: string; + resolvedDateRange: DateRange; +} + +export type DispatchSetState = ( + state: Partial +) => { + payload: Partial; + type: string; +}; + +export interface LensState { + app: LensAppState; +} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 9cde4eb8a15616..984fbf5555949b 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -9,6 +9,7 @@ import { IconType } from '@elastic/eui/src/components/icon/icon'; import { CoreSetup } from 'kibana/public'; import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; import { SavedObjectReference } from 'kibana/public'; +import { MutableRefObject } from 'react'; import { RowClickContext } from '../../../../src/plugins/ui_actions/public'; import { ExpressionAstExpression, @@ -18,9 +19,8 @@ import { SerializedFieldFormat, } from '../../../../src/plugins/expressions/public'; import { DraggingIdentifier, DragDropIdentifier, DragContextState } from './drag_drop'; -import { Document } from './persistence'; import { DateRange } from '../common'; -import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public'; +import { Query, Filter, IFieldFormat } from '../../../../src/plugins/data/public'; import { VisualizeFieldContext } from '../../../../src/plugins/ui_actions/public'; import { RangeSelectContext, ValueClickContext } from '../../../../src/plugins/embeddable/public'; import { @@ -46,22 +46,7 @@ export interface PublicAPIProps { export interface EditorFrameProps { onError: ErrorCallback; - doc?: Document; - dateRange: DateRange; - query: Query; - filters: Filter[]; - savedQuery?: SavedQuery; - searchSessionId: string; initialContext?: VisualizeFieldContext; - - // Frame loader (app or embeddable) is expected to call this when it loads and updates - // This should be replaced with a top-down state - onChange: (newState: { - filterableIndexPatterns: string[]; - doc: Document; - isSaveable: boolean; - activeData?: Record; - }) => void; showNoDataPopover: () => void; } export interface EditorFrameInstance { @@ -407,13 +392,14 @@ export type VisualizationDimensionEditorProps = VisualizationConfig groupId: string; accessor: string; setState: (newState: T) => void; + panelRef: MutableRefObject; }; export interface AccessorConfig { columnId: string; triggerIcon?: 'color' | 'disabled' | 'colorBy' | 'none' | 'invisible'; color?: string; - palette?: string[]; + palette?: string[] | Array<{ color: string; stop: number }>; } export type VisualizationDimensionGroupConfig = SharedDimensionProps & { diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 2d8cfee2185fa2..c1aab4c18f5297 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { IndexPattern, IndexPatternsContract, TimefilterContract } from 'src/plugins/data/public'; import { LensFilterEvent } from './types'; /** replaces the value `(empty) to empty string for proper filtering` */ @@ -49,3 +50,32 @@ export function getVisualizeGeoFieldMessage(fieldType: string) { values: { fieldType }, }); } + +export const getResolvedDateRange = function (timefilter: TimefilterContract) { + const { from, to } = timefilter.getTime(); + const { min, max } = timefilter.calculateBounds({ + from, + to, + }); + return { fromDate: min?.toISOString() || from, toDate: max?.toISOString() || to }; +}; + +export function containsDynamicMath(dateMathString: string) { + return dateMathString.includes('now'); +} +export const TIME_LAG_PERCENTAGE_LIMIT = 0.02; + +export async function getAllIndexPatterns( + ids: string[], + indexPatternsService: IndexPatternsContract +): Promise<{ indexPatterns: IndexPattern[]; rejectedIds: string[] }> { + const responses = await Promise.allSettled(ids.map((id) => indexPatternsService.get(id))); + const fullfilled = responses.filter( + (response): response is PromiseFulfilledResult => response.status === 'fulfilled' + ); + const rejectedIds = responses + .map((_response, i) => ids[i]) + .filter((id, i) => responses[i].status === 'rejected'); + // return also the rejected ids in case we want to show something later on + return { indexPatterns: fullfilled.map((response) => response.value), rejectedIds }; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts index d2e87ece5b5ec8..ef0c350f209619 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts @@ -118,7 +118,7 @@ export function getAccessorColorConfig( ); const customColor = currentYConfig?.color || - paletteService.get(currentPalette.name).getColor( + paletteService.get(currentPalette.name).getCategoricalColor( [ { name: columnToLabel[accessor] || accessor, diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index e3b4565913ad87..608971d281981f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -798,7 +798,7 @@ export function XYChart({ ), }, ]; - return paletteService.get(palette.name).getColor( + return paletteService.get(palette.name).getCategoricalColor( seriesLayers, { maxDepth: 1, diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx index b07feb85892e53..843680e3f28ac6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx @@ -7,14 +7,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { ToolbarPopover } from '../../shared_components'; +import { ToolbarPopover, TooltipWrapper } from '../../shared_components'; import { MissingValuesOptions } from './missing_values_option'; import { LineCurveOption } from './line_curve_option'; import { FillOpacityOption } from './fill_opacity_option'; import { XYState } from '../types'; import { hasHistogramSeries } from '../state_helpers'; import { ValidLayer } from '../types'; -import { TooltipWrapper } from '../tooltip_wrapper'; import { FramePublicAPI } from '../../types'; function getValueLabelDisableReason({ diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index aa4b91b840db37..8fbc8e8b2ef7ab 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -481,12 +481,12 @@ describe('xy_visualization', () => { it('should query palette to fill in colors for other dimensions', () => { const palette = paletteServiceMock.get('default'); - (palette.getColor as jest.Mock).mockClear(); + (palette.getCategoricalColor as jest.Mock).mockClear(); const accessorConfig = callConfigAndFindYConfig({}, 'c'); expect(accessorConfig.triggerIcon).toEqual('color'); // black is the color returned from the palette mock expect(accessorConfig.color).toEqual('black'); - expect(palette.getColor).toHaveBeenCalledWith( + expect(palette.getCategoricalColor).toHaveBeenCalledWith( [ { name: 'c', @@ -505,9 +505,9 @@ describe('xy_visualization', () => { label: 'Overwritten label', }); const palette = paletteServiceMock.get('default'); - (palette.getColor as jest.Mock).mockClear(); + (palette.getCategoricalColor as jest.Mock).mockClear(); callConfigAndFindYConfig({}, 'c'); - expect(palette.getColor).toHaveBeenCalledWith( + expect(palette.getCategoricalColor).toHaveBeenCalledWith( [ expect.objectContaining({ name: 'Overwritten label', @@ -526,7 +526,7 @@ describe('xy_visualization', () => { }, 'c' ); - expect(palette.getColor).toHaveBeenCalled(); + expect(palette.getCategoricalColor).toHaveBeenCalled(); }); it('should not show any indicator as long as there is no data', () => { @@ -551,7 +551,7 @@ describe('xy_visualization', () => { it('should show current palette for break down by dimension', () => { const palette = paletteServiceMock.get('mock'); const customColors = ['yellow', 'green']; - (palette.getColors as jest.Mock).mockReturnValue(customColors); + (palette.getCategoricalColors as jest.Mock).mockReturnValue(customColors); const breakdownConfig = callConfigForBreakdownConfigs({ palette: { type: 'palette', name: 'mock', params: {} }, splitAccessor: 'd', @@ -570,9 +570,9 @@ describe('xy_visualization', () => { paletteGetter.mockReturnValue({ id: 'default', title: '', - getColors: jest.fn(), + getCategoricalColors: jest.fn(), toExpression: jest.fn(), - getColor: jest.fn().mockReturnValueOnce('blue').mockReturnValueOnce('green'), + getCategoricalColor: jest.fn().mockReturnValueOnce('blue').mockReturnValueOnce('green'), }); const yConfigs = callConfigForYConfigs({}); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index dda1a444f45448..fa9d46be11d686 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import _ from 'lodash'; +import { uniq } from 'lodash'; import { render } from 'react-dom'; import { Position } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; @@ -43,7 +43,7 @@ function getVisualizationType(state: State): VisualizationType | 'mixed' { ); } const visualizationType = visualizationTypes.find((t) => t.id === state.layers[0].seriesType); - const seriesTypes = _.uniq(state.layers.map((l) => l.seriesType)); + const seriesTypes = uniq(state.layers.map((l) => l.seriesType)); return visualizationType && seriesTypes.length === 1 ? visualizationType : 'mixed'; } @@ -111,7 +111,7 @@ export const getXyVisualization = ({ }, appendLayer(state, layerId) { - const usedSeriesTypes = _.uniq(state.layers.map((layer) => layer.seriesType)); + const usedSeriesTypes = uniq(state.layers.map((layer) => layer.seriesType)); return { ...state, layers: [ @@ -235,7 +235,7 @@ export const getXyVisualization = ({ triggerIcon: 'colorBy', palette: paletteService .get(layer.palette?.name || 'default') - .getColors(10, layer.palette?.params), + .getCategoricalColors(10, layer.palette?.params), }, ] : [], @@ -255,10 +255,11 @@ export const getXyVisualization = ({ }, setDimension({ prevState, layerId, columnId, groupId }) { - const newLayer = prevState.layers.find((l) => l.layerId === layerId); - if (!newLayer) { + const foundLayer = prevState.layers.find((l) => l.layerId === layerId); + if (!foundLayer) { return prevState; } + const newLayer = { ...foundLayer }; if (groupId === 'x') { newLayer.xAccessor = columnId; @@ -277,11 +278,11 @@ export const getXyVisualization = ({ }, removeDimension({ prevState, layerId, columnId }) { - const newLayer = prevState.layers.find((l) => l.layerId === layerId); - if (!newLayer) { + const foundLayer = prevState.layers.find((l) => l.layerId === layerId); + if (!foundLayer) { return prevState; } - + const newLayer = { ...foundLayer }; if (newLayer.xAccessor === columnId) { delete newLayer.xAccessor; } else if (newLayer.splitAccessor === columnId) { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 0bafbead7d5438..bc10236cf1977e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -260,6 +260,7 @@ describe('XY Config panels', () => { state={{ ...state, layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' }] }} formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} + panelRef={React.createRef()} /> ); @@ -283,6 +284,7 @@ describe('XY Config panels', () => { state={state} formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} + panelRef={React.createRef()} /> ); @@ -326,6 +328,7 @@ describe('XY Config panels', () => { }} formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} + panelRef={React.createRef()} /> ); @@ -365,6 +368,7 @@ describe('XY Config panels', () => { }} formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} + panelRef={React.createRef()} /> ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index a6517894654ede..48f0cacf75938a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -41,9 +41,8 @@ import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_h import { trackUiEvent } from '../lens_ui_telemetry'; import { LegendSettingsPopover } from '../shared_components'; import { AxisSettingsPopover } from './axis_settings_popover'; -import { TooltipWrapper } from './tooltip_wrapper'; import { getAxesConfiguration, GroupsConfiguration } from './axes_configuration'; -import { PalettePicker } from '../shared_components'; +import { PalettePicker, TooltipWrapper } from '../shared_components'; import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; import { getScaleType, getSortedAccessors } from './to_expression'; import { VisualOptionsPopover } from './visual_options_popover/visual_options_popover'; diff --git a/x-pack/plugins/lists/server/routes/index.ts b/x-pack/plugins/lists/server/routes/index.ts index aa6749d179971f..f8d4deea344b2f 100644 --- a/x-pack/plugins/lists/server/routes/index.ts +++ b/x-pack/plugins/lists/server/routes/index.ts @@ -36,6 +36,7 @@ export * from './read_list_index_route'; export * from './read_list_item_route'; export * from './read_list_route'; export * from './read_privileges_route'; +export * from './summary_exception_list_route'; export * from './update_endpoint_list_item_route'; export * from './update_exception_list_item_route'; export * from './update_exception_list_route'; diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts index 430dad953a32fe..2511596ca84630 100644 --- a/x-pack/plugins/lists/server/routes/init_routes.ts +++ b/x-pack/plugins/lists/server/routes/init_routes.ts @@ -39,6 +39,7 @@ import { readListItemRoute, readListRoute, readPrivilegesRoute, + summaryExceptionListRoute, updateEndpointListItemRoute, updateExceptionListItemRoute, updateExceptionListRoute, @@ -95,4 +96,7 @@ export const initRoutes = (router: ListsPluginRouter, config: ConfigType): void updateEndpointListItemRoute(router); deleteEndpointListItemRoute(router); findEndpointListItemRoute(router); + + // exception list items summary + summaryExceptionListRoute(router); }; diff --git a/x-pack/plugins/lists/server/routes/summary_exception_list_route.ts b/x-pack/plugins/lists/server/routes/summary_exception_list_route.ts new file mode 100644 index 00000000000000..0db189fb70759b --- /dev/null +++ b/x-pack/plugins/lists/server/routes/summary_exception_list_route.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 { validate } from '@kbn/securitysolution-io-ts-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { + SummaryExceptionListSchemaDecoded, + exceptionListSummarySchema, + summaryExceptionListSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; + +import type { ListsPluginRouter } from '../types'; + +import { + buildRouteValidation, + buildSiemResponse, + getErrorMessageExceptionList, + getExceptionListClient, +} from './utils'; + +export const summaryExceptionListRoute = (router: ListsPluginRouter): void => { + router.get( + { + options: { + tags: ['access:lists-summary'], + }, + path: `${EXCEPTION_LIST_URL}/summary`, + validate: { + query: buildRouteValidation< + typeof summaryExceptionListSchema, + SummaryExceptionListSchemaDecoded + >(summaryExceptionListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, namespace_type: namespaceType } = request.query; + const exceptionLists = getExceptionListClient(context); + if (id != null || listId != null) { + const exceptionListSummary = await exceptionLists.getExceptionListSummary({ + id, + listId, + namespaceType, + }); + if (exceptionListSummary == null) { + return siemResponse.error({ + body: getErrorMessageExceptionList({ id, listId }), + statusCode: 404, + }); + } else { + const [validated, errors] = validate(exceptionListSummary, exceptionListSummarySchema); + if (errors != null) { + return response.ok({ body: exceptionListSummary }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } else { + return siemResponse.error({ body: 'id or list_id required', statusCode: 400 }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/scripts/summary_exception_list.sh b/x-pack/plugins/lists/server/scripts/summary_exception_list.sh new file mode 100755 index 00000000000000..54daeee7cb3876 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/summary_exception_list.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e +./check_env_variables.sh + + +LIST_ID=${1:-endpoint_list} +NAMESPACE_TYPE=${2-agnostic} + +# First, post a exception list and two list items for the example to work +# ./post_exception_list.sh ./exception_lists/new/exception_list_agnostic.json +# ./post_exception_list_item.sh ./exception_lists/new/exception_list_item_agnostic.json + +# Retrieve exception list stats by os +# Example: ./summary_exception_list.sh endpoint_list agnostic +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/summary?list_id=${LIST_ID}&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index 803cd04c1d1b44..4ccff2dd000b9b 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -9,6 +9,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import type { ExceptionListItemSchema, ExceptionListSchema, + ExceptionListSummarySchema, FoundExceptionListItemSchema, FoundExceptionListSchema, } from '@kbn/securitysolution-io-ts-list-types'; @@ -31,11 +32,13 @@ import { GetEndpointListItemOptions, GetExceptionListItemOptions, GetExceptionListOptions, + GetExceptionListSummaryOptions, UpdateEndpointListItemOptions, UpdateExceptionListItemOptions, UpdateExceptionListOptions, } from './exception_list_client_types'; import { getExceptionList } from './get_exception_list'; +import { getExceptionListSummary } from './get_exception_list_summary'; import { createExceptionList } from './create_exception_list'; import { getExceptionListItem } from './get_exception_list_item'; import { createExceptionListItem } from './create_exception_list_item'; @@ -72,6 +75,15 @@ export class ExceptionListClient { return getExceptionList({ id, listId, namespaceType, savedObjectsClient }); }; + public getExceptionListSummary = async ({ + listId, + id, + namespaceType, + }: GetExceptionListSummaryOptions): Promise => { + const { savedObjectsClient } = this; + return getExceptionListSummary({ id, listId, namespaceType, savedObjectsClient }); + }; + public getExceptionListItem = async ({ itemId, id, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index cbbf7f15134444..b734d3a7b1a3b9 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -56,6 +56,12 @@ export interface GetExceptionListOptions { namespaceType: NamespaceType; } +export interface GetExceptionListSummaryOptions { + listId: ListIdOrUndefined; + id: IdOrUndefined; + namespaceType: NamespaceType; +} + export interface CreateExceptionListOptions { listId: ListId; namespaceType: NamespaceType; diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_summary.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_summary.ts new file mode 100644 index 00000000000000..f5722ea26ccf7d --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_summary.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + ExceptionListSummarySchema, + IdOrUndefined, + ListIdOrUndefined, + NamespaceType, +} from '@kbn/securitysolution-io-ts-list-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; + +import { + SavedObjectsClientContract, + SavedObjectsErrorHelpers, +} from '../../../../../../src/core/server/'; +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; + +interface GetExceptionListSummaryOptions { + id: IdOrUndefined; + listId: ListIdOrUndefined; + savedObjectsClient: SavedObjectsClientContract; + namespaceType: NamespaceType; +} + +interface ByOsAggBucketType { + key: string; + doc_count: number; +} +interface ByOsAggType { + by_os: { + buckets: ByOsAggBucketType[]; + }; +} + +export const getExceptionListSummary = async ({ + id, + listId, + savedObjectsClient, + namespaceType, +}: GetExceptionListSummaryOptions): Promise => { + const savedObjectType = getSavedObjectType({ namespaceType }); + let finalListId: string = listId ?? ''; + + // If id and no listId, get the list by id to use the list_id for the find below + if (listId === null && id != null) { + try { + const savedObject = await savedObjectsClient.get(savedObjectType, id); + finalListId = savedObject.attributes.list_id; + } catch (err) { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + return null; + } else { + throw err; + } + } + } + + const savedObject = await savedObjectsClient.find({ + aggs: { + by_os: { + terms: { + field: `${savedObjectType}.attributes.os_types`, + }, + }, + }, + filter: `${savedObjectType}.attributes.list_type: item`, + perPage: 0, + search: finalListId, + searchFields: ['list_id'], + sortField: 'tie_breaker_id', + sortOrder: 'desc', + type: savedObjectType, + }); + + if (!savedObject.aggregations) { + return null; + } + + const summary: ExceptionListSummarySchema = savedObject.aggregations.by_os.buckets.reduce( + (acc, item: ByOsAggBucketType) => ({ + ...acc, + [item.key]: item.doc_count, + total: acc.total + item.doc_count, + }), + { linux: 0, macos: 0, total: 0, windows: 0 } + ); + + return summary; +}; diff --git a/x-pack/plugins/lists/server/services/exception_lists/index.ts b/x-pack/plugins/lists/server/services/exception_lists/index.ts index 21041b9d5d9bd8..e6a6dd7ef8c3c6 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/index.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/index.ts @@ -15,5 +15,6 @@ export * from './find_exception_list_item'; export * from './find_exception_list_items'; export * from './get_exception_list'; export * from './get_exception_list_item'; +export * from './get_exception_list_summary'; export * from './update_exception_list'; export * from './update_exception_list_item'; diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 1652e78d3d2cbf..4fce4c276c3360 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -112,7 +112,7 @@ export async function getChartsPaletteServiceGetColor(): Promise< const chartConfiguration = { syncColors: true }; return (value: string) => { const series = [{ name: value, rankAtDepth: 0, totalSeriesAtDepth: 1 }]; - const color = paletteDefinition.getColor(series, chartConfiguration); + const color = paletteDefinition.getCategoricalColor(series, chartConfiguration); return color ? color : '#3d3d3d'; }; } diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index c7c3f3ae9b2804..2b05f231e509ff 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -112,6 +112,10 @@ export interface ExplorerAppState { viewByFieldName?: string; viewByPerPage?: number; viewByFromPage?: number; + /** + * Indicated severity threshold for both swim lanes + */ + severity?: number; }; mlExplorerFilter: { influencersFilterQuery?: InfluencersFilterQuery; diff --git a/x-pack/plugins/ml/public/alerting/ml_anomaly_alert_trigger.tsx b/x-pack/plugins/ml/public/alerting/ml_anomaly_alert_trigger.tsx index 12fbaece54fac1..719b5c4aa4ad58 100644 --- a/x-pack/plugins/ml/public/alerting/ml_anomaly_alert_trigger.tsx +++ b/x-pack/plugins/ml/public/alerting/ml_anomaly_alert_trigger.tsx @@ -13,7 +13,7 @@ import { JobSelectorControl } from './job_selector'; import { useMlKibana } from '../application/contexts/kibana'; import { jobsApiProvider } from '../application/services/ml_api_service/jobs'; import { HttpService } from '../application/services/http_service'; -import { SeverityControl } from './severity_control'; +import { SeverityControl } from '../application/components/severity_control'; import { ResultTypeSelector } from './result_type_selector'; import { alertingApiProvider } from '../application/services/ml_api_service/alerting'; import { PreviewAlertCondition } from './preview_alert_condition'; diff --git a/x-pack/plugins/ml/public/alerting/severity_control/severity_control.tsx b/x-pack/plugins/ml/public/alerting/severity_control/severity_control.tsx deleted file mode 100644 index b1cd808643ca2f..00000000000000 --- a/x-pack/plugins/ml/public/alerting/severity_control/severity_control.tsx +++ /dev/null @@ -1,84 +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, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFormRow, EuiRange, EuiRangeProps } from '@elastic/eui'; -import { SEVERITY_OPTIONS } from '../../application/components/controls/select_severity/select_severity'; -import { ANOMALY_THRESHOLD } from '../../../common'; -import './styles.scss'; - -export interface SeveritySelectorProps { - value: number | undefined; - onChange: (value: number) => void; -} - -const MAX_ANOMALY_SCORE = 100; - -export const SeverityControl: FC = React.memo(({ value, onChange }) => { - const levels: EuiRangeProps['levels'] = [ - { - min: ANOMALY_THRESHOLD.LOW, - max: ANOMALY_THRESHOLD.MINOR - 1, - color: 'success', - }, - { - min: ANOMALY_THRESHOLD.MINOR, - max: ANOMALY_THRESHOLD.MAJOR - 1, - color: 'primary', - }, - { - min: ANOMALY_THRESHOLD.MAJOR, - max: ANOMALY_THRESHOLD.CRITICAL, - color: 'warning', - }, - { - min: ANOMALY_THRESHOLD.CRITICAL, - max: MAX_ANOMALY_SCORE, - color: 'danger', - }, - ]; - - const toggleButtons = SEVERITY_OPTIONS.map((v) => ({ - value: v.val, - label: v.display, - })); - - return ( - - } - > - { - // @ts-ignore Property 'value' does not exist on type 'EventTarget' | (EventTarget & HTMLInputElement) - onChange(Number(e.target.value)); - }} - showLabels - showValue - aria-label={i18n.translate('xpack.ml.severitySelector.formControlLabel', { - defaultMessage: 'Select severity threshold', - })} - showTicks - ticks={toggleButtons} - levels={levels} - data-test-subj={'mlAnomalyAlertScoreSelection'} - /> - - ); -}); diff --git a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts index f8e9a3b44e7e83..9fb41d15dd94a5 100644 --- a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts @@ -25,7 +25,7 @@ export function checkGetManagementMlJobsResolver() { if (isManageML === true && isPlatinumOrTrialLicense === true) { return resolve({ mlFeatureEnabledInSpace }); } else { - return reject(); + return reject({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }); } }) .catch((e) => { diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 7339b50d4ab340..262daae9d6469c 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -232,6 +232,7 @@ class LinksMenuUI extends Component { } const categorizationFieldName = job.analysis_config.categorization_field_name; const datafeedIndices = job.datafeed_config.indices; + // Find the type of the categorization field i.e. text (preferred) or keyword. // Uses the first matching field found in the list of indices in the datafeed_config. // attempt to load the field type using each index. we have to do it this way as _field_caps @@ -349,7 +350,7 @@ class LinksMenuUI extends Component { getFieldTypeFromMapping(index, categorizationFieldName) .then((resp) => { if (resp !== '') { - createAndOpenUrl(index, resp); + createAndOpenUrl(datafeedIndices.join(), resp); } else { i++; if (i < datafeedIndices.length) { diff --git a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx index 348c400b6d5a9c..f1ef62ddc90d49 100644 --- a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx @@ -6,7 +6,7 @@ */ import React, { FC } from 'react'; -import { EuiSelect } from '@elastic/eui'; +import { EuiIcon, EuiSelect, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { usePageUrlState } from '../../../util/url_state'; @@ -78,8 +78,22 @@ export const SelectIntervalUI: FC = ({ interval, onChange return ( + + + } + compressed + id="selectInterval" options={OPTIONS} - className="ml-select-interval" value={interval.val} onChange={handleOnChange} /> diff --git a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.tsx b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.tsx index e8766ea16c0021..3fe50a8b46d550 100644 --- a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.tsx @@ -8,11 +8,11 @@ /* * React component for rendering a select element with threshold levels. */ -import React, { Fragment, FC } from 'react'; +import React, { Fragment, FC, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui'; +import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText, EuiSuperSelectProps } from '@elastic/eui'; import { getSeverityColor } from '../../../../../common/util/anomaly_utils'; import { usePageUrlState } from '../../../util/url_state'; @@ -124,23 +124,34 @@ export const SelectSeverity: FC = ({ classNames } = { classNames: '' }) = return ; }; -export const SelectSeverityUI: FC<{ - classNames?: string; - severity: TableSeverity; - onChange: (s: TableSeverity) => void; -}> = ({ classNames = '', severity, onChange }) => { +export const SelectSeverityUI: FC< + Omit, 'onChange' | 'options'> & { + classNames?: string; + severity: TableSeverity; + onChange: (s: TableSeverity) => void; + } +> = ({ classNames = '', severity, onChange, compressed }) => { const handleOnChange = (valueDisplay: string) => { onChange(optionValueToThreshold(optionsMap[valueDisplay])); }; + const options = useMemo(() => { + return getSeverityOptions(); + }, []); + return ( ); }; diff --git a/x-pack/plugins/ml/public/alerting/severity_control/index.ts b/x-pack/plugins/ml/public/application/components/severity_control/index.ts similarity index 100% rename from x-pack/plugins/ml/public/alerting/severity_control/index.ts rename to x-pack/plugins/ml/public/application/components/severity_control/index.ts diff --git a/x-pack/plugins/ml/public/application/components/severity_control/severity_control.tsx b/x-pack/plugins/ml/public/application/components/severity_control/severity_control.tsx new file mode 100644 index 00000000000000..7be72b84302334 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/severity_control/severity_control.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { i18n } from '@kbn/i18n'; +import { + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiRange, + EuiRangeProps, +} from '@elastic/eui'; +import { ANOMALY_THRESHOLD } from '../../../../common'; +import './styles.scss'; + +export interface SeveritySelectorProps { + value: number | undefined; + onChange: (value: number) => void; +} + +const MAX_ANOMALY_SCORE = 100; + +export const SeverityControl: FC = React.memo(({ value, onChange }) => { + const levels: EuiRangeProps['levels'] = [ + { + min: ANOMALY_THRESHOLD.LOW, + max: ANOMALY_THRESHOLD.MINOR - 1, + color: 'success', + }, + { + min: ANOMALY_THRESHOLD.MINOR, + max: ANOMALY_THRESHOLD.MAJOR - 1, + color: 'primary', + }, + { + min: ANOMALY_THRESHOLD.MAJOR, + max: ANOMALY_THRESHOLD.CRITICAL, + color: 'warning', + }, + { + min: ANOMALY_THRESHOLD.CRITICAL, + max: MAX_ANOMALY_SCORE, + color: 'danger', + }, + ]; + + const label = i18n.translate('xpack.ml.severitySelector.formControlLabel', { + defaultMessage: 'Severity', + }); + + const resultValue = value ?? ANOMALY_THRESHOLD.LOW; + + const onChangeCallback = ( + e: React.ChangeEvent | React.MouseEvent + ) => { + // @ts-ignore Property 'value' does not exist on type 'EventTarget' | (EventTarget & HTMLInputElement) + onChange(Number(e.target.value)); + }; + + const ticks = new Array(5).fill(null).map((x, i) => { + const v = i * 25; + return { value: v, label: v }; + }); + + return ( + + + + + + + + + + + ); +}); diff --git a/x-pack/plugins/ml/public/alerting/severity_control/styles.scss b/x-pack/plugins/ml/public/application/components/severity_control/styles.scss similarity index 100% rename from x-pack/plugins/ml/public/alerting/severity_control/styles.scss rename to x-pack/plugins/ml/public/application/components/severity_control/styles.scss diff --git a/x-pack/plugins/ml/public/application/explorer/_explorer.scss b/x-pack/plugins/ml/public/application/explorer/_explorer.scss index c08020325428dd..d9d60ecfae67df 100644 --- a/x-pack/plugins/ml/public/application/explorer/_explorer.scss +++ b/x-pack/plugins/ml/public/application/explorer/_explorer.scss @@ -40,14 +40,6 @@ $borderRadius: $euiBorderRadius / 2; font-size: $euiFontSizeXS; } } - - .ml-anomalies-controls { - padding-top: $euiSizeXS; - - #show_charts_checkbox_control { - padding-top: $euiSizeL; - } - } } .mlSwimLaneContainer { diff --git a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts index a5d50f1070f5ba..621ce442047309 100644 --- a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts +++ b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts @@ -83,6 +83,7 @@ export interface LoadExplorerDataConfig { viewByFromPage: number; viewByPerPage: number; swimlaneContainerWidth: number; + swimLaneSeverity: number; } export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfig => { @@ -135,6 +136,7 @@ const loadExplorerDataProvider = ( swimlaneContainerWidth, viewByFromPage, viewByPerPage, + swimLaneSeverity, } = config; const combinedJobRecords: Record = selectedJobs.reduce((acc, job) => { @@ -192,7 +194,13 @@ const loadExplorerDataProvider = ( influencersFilterQuery ) : Promise.resolve({}), - overallState: memoizedLoadOverallData(lastRefresh, selectedJobs, swimlaneContainerWidth), + overallState: memoizedLoadOverallData( + lastRefresh, + selectedJobs, + swimlaneContainerWidth, + undefined, + swimLaneSeverity + ), tableData: memoizedLoadAnomaliesTableData( lastRefresh, selectedCells, @@ -278,7 +286,9 @@ const loadExplorerDataProvider = ( viewByPerPage, viewByFromPage, swimlaneContainerWidth, - influencersFilterQuery + influencersFilterQuery, + undefined, + swimLaneSeverity ), }).pipe( map(({ viewBySwimlaneState, filteredTopInfluencers }) => { diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx index 1e8f54d10491d9..8375b0a0b1dfc5 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, useMemo, useState } from 'react'; +import React, { FC, useCallback, useMemo, useState } from 'react'; import { isEqual } from 'lodash'; import { EuiPanel, @@ -14,7 +14,6 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, - EuiFormRow, EuiSelect, EuiTitle, EuiSpacer, @@ -35,7 +34,9 @@ import { ExplorerNoInfluencersFound } from './components/explorer_no_influencers import { SwimlaneContainer } from './swimlane_container'; import { AppStateSelectedCells, OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils'; import { NoOverallData } from './components/no_overall_data'; +import { SeverityControl } from '../components/severity_control'; import { AnomalyTimelineHelpPopover } from './anomaly_timeline_help_popover'; +import { isDefined } from '../../../common/types/guards'; function mapSwimlaneOptionsToEuiOptions(options: string[]) { return options.map((option) => ({ @@ -76,10 +77,8 @@ export const AnomalyTimeline: FC = React.memo( filterActive, filteredFields, maskAll, - overallSwimlaneData, selectedCells, viewByLoadedForTimeFormatted, - viewBySwimlaneData, viewBySwimlaneDataLoading, viewBySwimlaneFieldName, viewBySwimlaneOptions, @@ -89,6 +88,9 @@ export const AnomalyTimeline: FC = React.memo( swimlaneLimit, loading, overallAnnotations, + swimLaneSeverity, + overallSwimlaneData, + viewBySwimlaneData, } = explorerState; const annotations = useMemo(() => overallAnnotations.annotationsData, [overallAnnotations]); @@ -128,7 +130,7 @@ export const AnomalyTimeline: FC = React.memo( return ( <> - +

@@ -139,68 +141,10 @@ export const AnomalyTimeline: FC = React.memo(

- {viewBySwimlaneOptions.length > 0 && ( - <> - - - - - } - display={'columnCompressed'} - > - explorerService.setViewBySwimlaneFieldName(e.target.value)} - /> - - - {selectedCells ? ( - - - - - - ) : null} - -
- {viewByLoadedForTimeFormatted && ( - - )} - {viewByLoadedForTimeFormatted === undefined && ( - - )} - {filterActive === true && viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL && ( - - )} -
-
- - )} + + + + {menuItems.length > 0 && ( @@ -226,10 +170,79 @@ export const AnomalyTimeline: FC = React.memo( )} +
+ + + + + {viewBySwimlaneOptions.length > 0 && ( + <> + + explorerService.setViewBySwimlaneFieldName(e.target.value)} + /> + + + )} + + + { + explorerService.setSwimLaneSeverity(update); + }, [])} + /> + + + + + - +
+ {viewByLoadedForTimeFormatted && ( + + )} + {isDefined(viewByLoadedForTimeFormatted) ? null : ( + + )} + {filterActive === true && viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL && ( + + )} +
+ + {selectedCells ? ( + + + + + + ) : null}
@@ -249,6 +262,7 @@ export const AnomalyTimeline: FC = React.memo( noDataWarning={} showTimeline={false} annotationsData={annotations} + showLegend={false} /> @@ -266,7 +280,7 @@ export const AnomalyTimeline: FC = React.memo( }) } timeBuckets={timeBuckets} - showLegend={true} + showLegend={false} swimlaneData={viewBySwimlaneData as ViewBySwimLaneData} swimlaneType={SWIMLANE_TYPE.VIEW_BY} selection={selectedCells} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 7cc1d0d86e2ff2..4b241c47a267b0 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -19,9 +19,7 @@ import { EuiCallOut, EuiFlexGroup, EuiFlexItem, - EuiFormRow, EuiHorizontalRule, - EuiIcon, EuiIconTip, EuiPage, EuiPageBody, @@ -29,7 +27,6 @@ import { EuiPageHeaderSection, EuiSpacer, EuiTitle, - EuiToolTip, EuiLoadingContent, EuiPanel, EuiAccordion, @@ -78,6 +75,7 @@ import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/ import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; import { ML_APP_URL_GENERATOR } from '../../../common/constants/ml_url_generator'; import { AnomalyContextMenu } from './anomaly_context_menu'; +import { isDefined } from '../../../common/types/guards'; const ExplorerPage = ({ children, @@ -263,6 +261,7 @@ export class ExplorerUI extends React.Component { selectedCells, selectedJobs, tableData, + swimLaneSeverity, } = this.props.explorerState; const { annotationsData, aggregations, error: annotationsError } = annotations; @@ -276,6 +275,8 @@ export class ExplorerUI extends React.Component { (hasResults && overallSwimlaneData.points.some((v) => v.value > 0)) || tableData.anomalies?.length > 0; + const hasActiveFilter = isDefined(swimLaneSeverity); + if (noJobsFound && !loading) { return ( @@ -284,7 +285,7 @@ export class ExplorerUI extends React.Component { ); } - if (hasResultsWithAnomalies === false && !loading) { + if (!hasResultsWithAnomalies && !loading && !hasActiveFilter) { return ( + + {annotationsError !== undefined && ( <> )} - {loading === false && tableData.anomalies?.length && ( + {loading === false && tableData.anomalies?.length ? ( - )} + ) : null} {annotationsData.length > 0 && ( <> @@ -476,47 +479,16 @@ export class ExplorerUI extends React.Component {
- - - - - + + + - - - - {i18n.translate('xpack.ml.explorer.intervalLabel', { - defaultMessage: 'Interval', - })} - - - - } - > - - + + {chartsData.seriesToPlot.length > 0 && selectedCells !== undefined && ( - - - - + + )} @@ -524,7 +496,7 @@ export class ExplorerUI extends React.Component {
- {showCharts && ( + {showCharts ? ( - )} + ) : null}
= ( }) => { return ( <> - - - - - + + + diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts index 4398a4b2c2be75..d737c4733b9cba 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts @@ -33,6 +33,7 @@ export const EXPLORER_ACTION = { SET_VIEW_BY_SWIMLANE_LOADING: 'setViewBySwimlaneLoading', SET_VIEW_BY_PER_PAGE: 'setViewByPerPage', SET_VIEW_BY_FROM_PAGE: 'setViewByFromPage', + SET_SWIM_LANE_SEVERITY: 'setSwimLaneSeverity', }; export const FILTER_ACTION = { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts index 343ba88655e4e0..7721532b34338f 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts @@ -79,6 +79,10 @@ const explorerAppState$: Observable = explorerState$.pipe( appState.mlExplorerSwimlane.viewByPerPage = state.viewByPerPage; } + if (state.swimLaneSeverity !== undefined) { + appState.mlExplorerSwimlane.severity = state.swimLaneSeverity; + } + if (state.filterActive) { appState.mlExplorerFilter.influencersFilterQuery = state.influencersFilterQuery; appState.mlExplorerFilter.filterActive = state.filterActive; @@ -161,6 +165,9 @@ export const explorerService = { setViewByPerPage: (payload: number) => { explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_PER_PAGE, payload }); }, + setSwimLaneSeverity: (payload: number) => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_SWIM_LANE_SEVERITY, payload }); + }, }; export type ExplorerService = typeof explorerService; diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts index 15e0caa29af39f..74867af5f89879 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts @@ -149,6 +149,15 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo }; break; + case EXPLORER_ACTION.SET_SWIM_LANE_SEVERITY: + nextState = { + ...state, + // reset current page on the page size change + viewByFromPage: 1, + swimLaneSeverity: payload, + }; + break; + default: nextState = state; } @@ -181,7 +190,9 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo ...nextState, swimlaneBucketInterval, viewByLoadedForTimeFormatted: timeRange - ? formatHumanReadableDateTime(timeRange.earliestMs) + ? `${formatHumanReadableDateTime(timeRange.earliestMs)} - ${formatHumanReadableDateTime( + timeRange.latestMs + )}` : null, viewBySwimlaneFieldName, viewBySwimlaneOptions, diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts index 2365e4e4689026..8a152ab1cadc31 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts @@ -58,6 +58,7 @@ export interface ExplorerState { viewByFromPage: number; viewBySwimlaneOptions: string[]; swimlaneLimit?: number; + swimLaneSeverity?: number; } function getDefaultIndexPattern() { diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 41bbe5b66a605f..d959328218a187 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -68,6 +68,10 @@ declare global { const RESIZE_THROTTLE_TIME_MS = 500; const CELL_HEIGHT = 30; const LEGEND_HEIGHT = 34; +/** + * Minimum container height to make sure "No data" message is displayed without overflow. + */ +const MIN_CONTAINER_HEIGHT = 40; const Y_AXIS_HEIGHT = 24; @@ -245,7 +249,10 @@ export const SwimlaneContainer: FC = ({ return isLoading ? containerHeightRef.current : // TODO update when elastic charts X label will be fixed - rowsCount * CELL_HEIGHT + LEGEND_HEIGHT + (true ? Y_AXIS_HEIGHT : 0); + Math.max( + rowsCount * CELL_HEIGHT + (showLegend ? LEGEND_HEIGHT : 0) + (true ? Y_AXIS_HEIGHT : 0), + MIN_CONTAINER_HEIGHT + ); }, [isLoading, rowsCount, showTimeline]); useEffect(() => { @@ -331,7 +338,7 @@ export const SwimlaneContainer: FC = ({ brushArea: { stroke: isDarkTheme ? 'rgb(255, 255, 255)' : 'rgb(105, 112, 125)', }, - maxLegendHeight: LEGEND_HEIGHT, + ...(showLegend ? { maxLegendHeight: LEGEND_HEIGHT } : {}), timeZone: 'UTC', }; }, [ @@ -463,7 +470,7 @@ export const SwimlaneContainer: FC = ({ )} {!isLoading && !showSwimlane && ( {noDataWarning}} /> diff --git a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js index 75bc93c8dc65ea..9da97f40f5ec66 100644 --- a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js @@ -15,6 +15,7 @@ import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../../src/plugins import { getPartitioningFieldNames } from '../../../../../common/util/job_utils'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { replaceTokensInUrlValue, isValidLabel } from '../../../util/custom_url_utils'; +import { getIndexPatternIdFromName } from '../../../util/index_utils'; import { ml } from '../../../services/ml_api_service'; import { mlJobService } from '../../../services/job_service'; import { escapeForElasticsearchQuery } from '../../../util/string_utils'; @@ -38,7 +39,7 @@ export function getNewCustomUrlDefaults(job, dashboards, indexPatterns) { } // For the Discover option, set the default index pattern to that - // which matches the (first) index configured in the job datafeed. + // which matches the indices configured in the job datafeed. const datafeedConfig = job.datafeed_config; if ( indexPatterns !== undefined && @@ -47,16 +48,9 @@ export function getNewCustomUrlDefaults(job, dashboards, indexPatterns) { datafeedConfig.indices !== undefined && datafeedConfig.indices.length > 0 ) { - const datafeedIndex = datafeedConfig.indices[0]; - let defaultIndexPattern = indexPatterns.find((indexPattern) => { - return indexPattern.title === datafeedIndex; - }); - - if (defaultIndexPattern === undefined) { - defaultIndexPattern = indexPatterns[0]; - } - - kibanaSettings.discoverIndexPatternId = defaultIndexPattern.id; + const defaultIndexPatternId = + getIndexPatternIdFromName(datafeedConfig.indices.join()) ?? indexPatterns[0].id; + kibanaSettings.discoverIndexPatternId = defaultIndexPatternId; } return { diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx index 18d8a42b76cb07..0d785f1918b0b5 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx @@ -31,8 +31,8 @@ export const AccessDeniedPage = () => (

@@ -42,7 +42,7 @@ export const AccessDeniedPage = () => ( = ({ basePath }) => ( + + + + + + +

+ +

+
+
+
+ + + + +

+ + + + ), + }} + /> +

+
+
+
+
+
+
+); 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 2dc46bcf8fb41a..ca62ef9aaf0af1 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 @@ -38,6 +38,7 @@ import { getDocLinks } from '../../../../util/dependency_cache'; import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view/index'; import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; import { AccessDeniedPage } from '../access_denied_page'; +import { InsufficientLicensePage } from '../insufficient_license_page'; import { SharePluginStart } from '../../../../../../../../../src/plugins/share/public'; import type { SpacesPluginStart } from '../../../../../../../spaces/public'; import { JobSpacesSyncFlyout } from '../../../../components/job_spaces_sync'; @@ -128,6 +129,7 @@ export const JobsListPage: FC<{ const spacesEnabled = spacesApi !== undefined; const [initialized, setInitialized] = useState(false); const [accessDenied, setAccessDenied] = useState(false); + const [isPlatinumOrTrialLicense, setIsPlatinumOrTrialLicense] = useState(true); const [showSyncFlyout, setShowSyncFlyout] = useState(false); const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false); const tabs = useTabs(isMlEnabledInSpace, spacesApi); @@ -139,7 +141,11 @@ export const JobsListPage: FC<{ const { mlFeatureEnabledInSpace } = await checkGetManagementMlJobsResolver(); setIsMlEnabledInSpace(mlFeatureEnabledInSpace); } catch (e) { - setAccessDenied(true); + if (e.mlFeatureEnabledInSpace && e.isPlatinumOrTrialLicense === false) { + setIsPlatinumOrTrialLicense(false); + } else { + setAccessDenied(true); + } } setInitialized(true); }; @@ -191,6 +197,10 @@ export const JobsListPage: FC<{ return ; } + if (isPlatinumOrTrialLicense === false) { + return ; + } + return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 3e5cf252230a26..a0a81f77b7b087 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -177,7 +177,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim explorerService.setFilterData(filterData); } - const { viewByFieldName, viewByFromPage, viewByPerPage } = + const { viewByFieldName, viewByFromPage, viewByPerPage, severity } = explorerUrlState?.mlExplorerSwimlane ?? {}; if (viewByFieldName !== undefined) { @@ -191,6 +191,10 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim if (viewByFromPage !== undefined) { explorerService.setViewByFromPage(viewByFromPage); } + + if (severity !== undefined) { + explorerService.setSwimLaneSeverity(severity); + } }, []); /** Sync URL state with {@link explorerService} state */ @@ -238,6 +242,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim swimlaneContainerWidth: explorerState.swimlaneContainerWidth, viewByPerPage: explorerState.viewByPerPage, viewByFromPage: explorerState.viewByFromPage, + swimLaneSeverity: explorerState.swimLaneSeverity, } : undefined; diff --git a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts index 54d9626edf26c0..e11eb4048c374b 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts @@ -98,7 +98,8 @@ export class AnomalyTimelineService { public async loadOverallData( selectedJobs: ExplorerJob[], chartWidth?: number, - bucketInterval?: TimeBucketsInterval + bucketInterval?: TimeBucketsInterval, + overallScore?: number ): Promise { const interval = bucketInterval ?? this.getSwimlaneBucketInterval(selectedJobs, chartWidth!); @@ -127,7 +128,8 @@ export class AnomalyTimelineService { 1, overallBucketsBounds.min.valueOf(), overallBucketsBounds.max.valueOf(), - interval.asSeconds() + 's' + interval.asSeconds() + 's', + overallScore ); const overallSwimlaneData = this.processOverallResults( resp.results, @@ -161,7 +163,8 @@ export class AnomalyTimelineService { fromPage: number, swimlaneContainerWidth?: number, influencersFilterQuery?: any, - bucketInterval?: TimeBucketsInterval + bucketInterval?: TimeBucketsInterval, + swimLaneSeverity?: number ): Promise { const timefilterBounds = this.getTimeBounds(); @@ -195,7 +198,8 @@ export class AnomalyTimelineService { searchBounds.max.valueOf(), intervalMs, perPage, - fromPage + fromPage, + swimLaneSeverity ); } else { response = await this.mlResultsService.getInfluencerValueMaxScoreByTime( @@ -208,7 +212,8 @@ export class AnomalyTimelineService { swimlaneLimit, perPage, fromPage, - influencersFilterQuery + influencersFilterQuery, + swimLaneSeverity ); } 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 bf6b752faa8da6..f8ec4b64883160 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 @@ -323,14 +323,22 @@ export function mlApiServicesProvider(httpService: HttpService) { bucketSpan, start, end, + overallScore, }: { jobId: string; topN: string; bucketSpan: string; start: number; end: number; + overallScore?: number; }) { - const body = JSON.stringify({ topN, bucketSpan, start, end }); + const body = JSON.stringify({ + topN, + bucketSpan, + start, + end, + ...(overallScore ? { overall_score: overallScore } : {}), + }); return httpService.http({ path: `${basePath()}/anomaly_detectors/${jobId}/results/overall_buckets`, method: 'POST', diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts index 6161eeb4e79408..ea07d32bfff1d0 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts @@ -22,7 +22,8 @@ export function resultsServiceProvider( latestMs: number, intervalMs: number, perPage?: number, - fromPage?: number + fromPage?: number, + swimLaneSeverity?: number ): Promise; getTopInfluencers( selectedJobIds: string[], @@ -40,7 +41,8 @@ export function resultsServiceProvider( topN: any, earliestMs: any, latestMs: any, - interval?: any + interval?: any, + overallScore?: number ): Promise; getInfluencerValueMaxScoreByTime( jobIds: string[], @@ -52,7 +54,8 @@ export function resultsServiceProvider( maxResults: number, perPage: number, fromPage: number, - influencersFilterQuery: InfluencersFilterQuery + influencersFilterQuery: InfluencersFilterQuery, + swimLaneSeverity?: number ): Promise; getRecordInfluencers(): Promise; getRecordsForDetector(): Promise; diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index 71be7bcd2b7eb7..bb6f6b5969ac4d 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -30,7 +30,15 @@ export function resultsServiceProvider(mlApiServices) { // Pass an empty array or ['*'] to search over all job IDs. // Returned response contains a results property, with a key for job // which has results for the specified time range. - getScoresByBucket(jobIds, earliestMs, latestMs, intervalMs, perPage = 10, fromPage = 1) { + getScoresByBucket( + jobIds, + earliestMs, + latestMs, + intervalMs, + perPage = 10, + fromPage = 1, + swimLaneSeverity = 0 + ) { return new Promise((resolve, reject) => { const obj = { success: true, @@ -49,6 +57,13 @@ export function resultsServiceProvider(mlApiServices) { }, }, }, + { + range: { + anomaly_score: { + gt: swimLaneSeverity, + }, + }, + }, ]; if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { @@ -463,7 +478,7 @@ export function resultsServiceProvider(mlApiServices) { // Obtains the overall bucket scores for the specified job ID(s). // Pass ['*'] to search over all job IDs. // Returned response contains a results property as an object of max score by time. - getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval) { + getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval, overallScore) { return new Promise((resolve, reject) => { const obj = { success: true, results: {} }; @@ -474,6 +489,7 @@ export function resultsServiceProvider(mlApiServices) { bucketSpan: interval, start: earliestMs, end: latestMs, + overallScore, }) .then((resp) => { const dataByTime = get(resp, ['overall_buckets'], []); @@ -507,7 +523,8 @@ export function resultsServiceProvider(mlApiServices) { maxResults = ANOMALY_SWIM_LANE_HARD_LIMIT, perPage = SWIM_LANE_DEFAULT_PAGE_SIZE, fromPage = 1, - influencersFilterQuery + influencersFilterQuery, + swimLaneSeverity ) { return new Promise((resolve, reject) => { const obj = { success: true, results: {} }; @@ -527,7 +544,7 @@ export function resultsServiceProvider(mlApiServices) { { range: { influencer_score: { - gt: 0, + gt: swimLaneSeverity !== undefined ? swimLaneSeverity : 0, }, }, }, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss b/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss index 33f6c65e03e77f..cfd521c882fb71 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss @@ -19,10 +19,6 @@ float: right; } - .ml-anomalies-controls { - padding-top: $euiSizeXS; - } - .ml-timeseries-chart { svg { font-size: $euiFontSizeXS; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index c33b780631f166..c2b806abcf2860 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -26,11 +26,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, - EuiIcon, EuiSpacer, EuiPanel, EuiTitle, - EuiToolTip, EuiAccordion, EuiBadge, } from '@elastic/eui'; @@ -1273,41 +1271,12 @@ export class TimeSeriesExplorer extends React.Component { /> - - - - - + + + - - - - {i18n.translate('xpack.ml.timeSeriesExplorer.intervalLabel', { - defaultMessage: 'Interval', - })} - - - - } - > - - + + diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index c9fde252fc26dd..1f41f0a1d25c30 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -53,6 +53,7 @@ import { } from '../../triggers_actions_ui/public'; import { FileDataVisualizerPluginStart } from '../../file_data_visualizer/public'; import { PluginSetupContract as AlertingSetup } from '../../alerting/public'; +import { registerManagementSection } from './application/management'; export interface MlStartDependencies { data: DataPublicPluginStart; @@ -133,6 +134,10 @@ export class MlPlugin implements Plugin { this.urlGenerator = registerUrlGenerator(pluginsSetup.share, core); } + if (pluginsSetup.management) { + registerManagementSection(pluginsSetup.management, core).enable(); + } + const licensing = pluginsSetup.licensing.license$.pipe(take(1)); licensing.subscribe(async (license) => { const [coreStart] = await core.getStartServices(); @@ -160,7 +165,6 @@ export class MlPlugin implements Plugin { // note including registerFeature in register_helper would cause the page bundle size to increase significantly const { registerEmbeddables, - registerManagementSection, registerMlUiActions, registerSearchLinks, registerMlAlerts, @@ -172,11 +176,6 @@ export class MlPlugin implements Plugin { registerSearchLinks(this.appUpdater$, fullLicense); if (fullLicense) { - const canManageMLJobs = - capabilities.management?.insightsAndAlerting?.jobsListLink ?? false; - if (canManageMLJobs && pluginsSetup.management !== undefined) { - registerManagementSection(pluginsSetup.management, core).enable(); - } registerEmbeddables(pluginsSetup.embeddable, core); registerMlUiActions(pluginsSetup.uiActions, core); diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 6adf6fa474cad0..5205ea7353ac61 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -522,6 +522,7 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { bucket_span: request.body.bucketSpan, start: request.body.start !== undefined ? String(request.body.start) : undefined, end: request.body.end !== undefined ? String(request.body.end) : undefined, + overall_score: request.body.overall_score ?? 0, }, }); return response.ok({ diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 4217002e61ef72..392c0d3514d648 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -186,6 +186,7 @@ export const getOverallBucketsSchema = schema.object({ bucketSpan: schema.string(), start: schema.number(), end: schema.number(), + overall_score: schema.maybe(schema.number()), }); export const getCategoriesSchema = schema.object({ diff --git a/x-pack/plugins/observability/public/components/shared/header_menu_portal.test.tsx b/x-pack/plugins/observability/public/components/shared/header_menu_portal.test.tsx new file mode 100644 index 00000000000000..4e9a1ae2c587f0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/header_menu_portal.test.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 { render } from '@testing-library/react'; +import React from 'react'; +import HeaderMenuPortal from './header_menu_portal'; + +describe('HeaderMenuPortal', () => { + describe('when unmounted', () => { + it('calls setHeaderActionMenu with undefined', () => { + const setHeaderActionMenu = jest.fn(); + + const { unmount } = render( + test + ); + + unmount(); + + expect(setHeaderActionMenu).toHaveBeenCalledWith(undefined); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx b/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx index 54949c1d860d83..6c3b558c5e61df 100644 --- a/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx +++ b/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx @@ -15,17 +15,14 @@ export default function HeaderMenuPortal({ children, setHeaderActionMenu }: Head const portalNode = useMemo(() => createPortalNode(), []); useEffect(() => { - let unmount = () => {}; - setHeaderActionMenu((element) => { const mount = toMountPoint(); - unmount = mount(element); - return unmount; + return mount(element); }); return () => { portalNode.unmount(); - unmount(); + setHeaderActionMenu(undefined); }; }, [portalNode, setHeaderActionMenu]); diff --git a/x-pack/plugins/observability/public/components/shared/page_template/README.md b/x-pack/plugins/observability/public/components/shared/page_template/README.md index e360e6d95a9d8a..104b365e94fe91 100644 --- a/x-pack/plugins/observability/public/components/shared/page_template/README.md +++ b/x-pack/plugins/observability/public/components/shared/page_template/README.md @@ -12,11 +12,48 @@ To register a solution's navigation structure you'll first need to ensure your s ], ``` -Now within your solution's **public** plugin `setup` lifecycle method you can call the `registerSections` method, this will register your solution's specific navigation structure with the overall Observability navigation registry. E.g. +Now within your solution's **public** plugin `setup` lifecycle method you can +call the `registerSections` method, this will register your solution's specific +navigation structure with the overall Observability navigation registry. + +The `registerSections` function takes an `Observable` of an array of +`NavigationSection`s. Each section can be defined as + +```typescript +export interface NavigationSection { + // the label of the section, should be translated + label: string | undefined; + // the key to sort by in ascending order relative to other entries + sortKey: number; + // the entries to render inside the section + entries: NavigationEntry[]; +} +``` + +Each entry inside of a navigation section is defined as + +```typescript +export interface NavigationEntry { + // the label of the menu entry, should be translated + label: string; + // the kibana app id + app: string; + // the path after the application prefix corresponding to this entry + path: string; + // whether to only match when the full path matches, defaults to `false` + matchFullPath?: boolean; + // whether to ignore trailing slashes, defaults to `true` + ignoreTrailingSlash?: boolean; +} +``` + +A registration might therefore look like the following: ```typescript // x-pack/plugins/example_plugin/public/plugin.ts +import { of } from 'rxjs'; + export class Plugin implements PluginClass { constructor(_context: PluginInitializerContext) {} @@ -27,6 +64,7 @@ export class Plugin implements PluginClass { label: 'A solution section', sortKey: 200, entries: [ + { label: 'Home Page', app: 'exampleA', path: '/', matchFullPath: true }, { label: 'Example Page', app: 'exampleA', path: '/example' }, { label: 'Another Example Page', app: 'exampleA', path: '/another-example' }, ], @@ -64,7 +102,7 @@ This can be accessed like so: ``` const [coreStart, pluginsStart] = await core.getStartServices(); -const pageTemplateComponent = pluginsStart.observability.navigation.PageTemplate; +const ObservabilityPageTemplate = pluginsStart.observability.navigation.PageTemplate; ``` Now that you have access to the component you can render your solution's content using it. @@ -101,4 +139,4 @@ The `` component is a wrapper around the ` '' } }, }, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), + ObservabilityPageTemplate: KibanaPageTemplate, } as unknown) as PluginContextValue } > diff --git a/x-pack/plugins/observability/public/services/navigation_registry.ts b/x-pack/plugins/observability/public/services/navigation_registry.ts index f42f34fcfe9bb3..79a36731f7ed1e 100644 --- a/x-pack/plugins/observability/public/services/navigation_registry.ts +++ b/x-pack/plugins/observability/public/services/navigation_registry.ts @@ -9,15 +9,25 @@ import { combineLatest, Observable, ReplaySubject } from 'rxjs'; import { map, scan, shareReplay, switchMap } from 'rxjs/operators'; export interface NavigationSection { + // the label of the section, should be translated label: string | undefined; + // the key to sort by in ascending order relative to other entries sortKey: number; + // the entries to render inside the section entries: NavigationEntry[]; } export interface NavigationEntry { + // the label of the menu entry, should be translated label: string; + // the kibana app id app: string; + // the path after the application prefix corresponding to this entry path: string; + // whether to only match when the full path matches, defaults to `false` + matchFullPath?: boolean; + // whether to ignore trailing slashes, defaults to `true` + ignoreTrailingSlash?: boolean; } export interface NavigationRegistry { diff --git a/x-pack/plugins/rule_registry/README.md b/x-pack/plugins/rule_registry/README.md index cfbde612b45a6d..e12c2b29ed3738 100644 --- a/x-pack/plugins/rule_registry/README.md +++ b/x-pack/plugins/rule_registry/README.md @@ -145,3 +145,6 @@ The following fields are defined in the technical field component template and s - `kibana.rac.alert.severity.value`: the severity of the alert, as a numerical value, which allows sorting. - `kibana.rac.alert.evaluation.value`: The measured (numerical value). - `kibana.rac.alert.threshold.value`: The threshold that was defined (or, in case of multiple thresholds, the one that was exceeded). +- `kibana.rac.alert.ancestors`: the array of ancestors (if any) for the alert. +- `kibana.rac.alert.depth`: the depth of the alert in the ancestral tree (default 0). +- `kibana.rac.alert.building_block_type`: the building block type of the alert (default undefined). diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index 9547f165cd7058..9eefc19f34670e 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -14,6 +14,7 @@ export { RuleDataClient } from './rule_data_client'; export { IRuleDataClient } from './rule_data_client/types'; export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data'; export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory'; +export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory'; export const plugin = (initContext: PluginInitializerContext) => new RuleRegistryPlugin(initContext); diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/index.ts b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts index 135c870f207272..43122ba49519a4 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts @@ -73,8 +73,8 @@ export class RuleDataClient implements IRuleDataClient { return clusterClient.bulk(requestWithDefaultParameters).then((response) => { if (response.body.errors) { if ( - response.body.items.length === 1 && - response.body.items[0]?.index?.error?.type === 'index_not_found_exception' + response.body.items.length > 0 && + response.body.items?.[0]?.index?.error?.type === 'index_not_found_exception' ) { return this.createOrUpdateWriteTarget({ namespace }).then(() => { return clusterClient.bulk(requestWithDefaultParameters); diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts new file mode 100644 index 00000000000000..0e244fbaa2ee35 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ESSearchRequest } from 'typings/elasticsearch'; +import v4 from 'uuid/v4'; +import { Logger } from '@kbn/logging'; + +import { AlertInstance } from '../../../alerting/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertTypeParams, +} from '../../../alerting/common'; +import { RuleDataClient } from '../rule_data_client'; +import { AlertTypeWithExecutor } from '../types'; + +type PersistenceAlertService> = ( + alerts: Array> +) => Array>; + +type PersistenceAlertQueryService = ( + query: ESSearchRequest +) => Promise>>; + +type CreatePersistenceRuleTypeFactory = (options: { + ruleDataClient: RuleDataClient; + logger: Logger; +}) => < + TParams extends AlertTypeParams, + TAlertInstanceContext extends AlertInstanceContext, + TServices extends { + alertWithPersistence: PersistenceAlertService; + findAlerts: PersistenceAlertQueryService; + } +>( + type: AlertTypeWithExecutor +) => AlertTypeWithExecutor; + +export const createPersistenceRuleTypeFactory: CreatePersistenceRuleTypeFactory = ({ + logger, + ruleDataClient, +}) => (type) => { + return { + ...type, + executor: async (options) => { + const { + services: { alertInstanceFactory, scopedClusterClient }, + } = options; + + const currentAlerts: Array> = []; + const timestamp = options.startedAt.toISOString(); + + const state = await type.executor({ + ...options, + services: { + ...options.services, + alertWithPersistence: (alerts) => { + alerts.forEach((alert) => currentAlerts.push(alert)); + return alerts.map((alert) => + alertInstanceFactory(alert['kibana.rac.alert.uuid']! as string) + ); + }, + findAlerts: async (query) => { + const { body } = await scopedClusterClient.asCurrentUser.search({ + ...query, + body: { + ...query.body, + }, + ignore_unavailable: true, + }); + return body.hits.hits + .map((event: { _source: any }) => event._source!) + .map((event: { [x: string]: any }) => { + const alertUuid = event['kibana.rac.alert.uuid']; + const isAlert = alertUuid != null; + return { + ...event, + 'event.kind': 'signal', + 'kibana.rac.alert.id': '???', + 'kibana.rac.alert.status': 'open', + 'kibana.rac.alert.uuid': v4(), + 'kibana.rac.alert.ancestors': isAlert + ? ((event['kibana.rac.alert.ancestors'] as string[]) ?? []).concat([ + alertUuid!, + ] as string[]) + : [], + 'kibana.rac.alert.depth': isAlert + ? ((event['kibana.rac.alert.depth'] as number) ?? 0) + 1 + : 0, + '@timestamp': timestamp, + }; + }); + }, + }, + }); + + const numAlerts = currentAlerts.length; + logger.debug(`Found ${numAlerts} alerts.`); + + if (ruleDataClient && numAlerts) { + await ruleDataClient.getWriter().bulk({ + body: currentAlerts.flatMap((event) => [{ index: {} }, event]), + }); + } + + return state; + }, + }; +}; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index effefdd438c5cc..91b48afdc4ed1e 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -25,6 +25,7 @@ export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults'; export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults'; export const DEFAULT_APP_TIME_RANGE = 'securitySolution:timeDefaults'; export const DEFAULT_APP_REFRESH_INTERVAL = 'securitySolution:refreshIntervalDefaults'; +export const DEFAULT_ALERTS_INDEX = '.alerts-security-solution'; export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; export const DEFAULT_LISTS_INDEX = '.lists'; export const DEFAULT_ITEMS_INDEX = '.items'; @@ -148,6 +149,18 @@ export const DEFAULT_TRANSFORMS_SETTING = JSON.stringify(defaultTransformsSettin */ export const SIGNALS_ID = `siem.signals`; +/** + * Id's for reference rule types + */ +export const REFERENCE_RULE_ALERT_TYPE_ID = `siem.referenceRule`; +export const REFERENCE_RULE_PERSISTENCE_ALERT_TYPE_ID = `siem.referenceRulePersistence`; + +export const CUSTOM_ALERT_TYPE_ID = `siem.customRule`; +export const EQL_ALERT_TYPE_ID = `siem.eqlRule`; +export const INDICATOR_ALERT_TYPE_ID = `siem.indicatorRule`; +export const ML_ALERT_TYPE_ID = `siem.mlRule`; +export const THRESHOLD_ALERT_TYPE_ID = `siem.thresholdRule`; + /** * Id for the notifications alerting type */ diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts index c0888a6c2a4bd4..35c976fbdfb1d1 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts @@ -9,6 +9,8 @@ import seedrandom from 'seedrandom'; import uuid from 'uuid'; const OS_FAMILY = ['windows', 'macos', 'linux']; +/** Array of 14 day offsets */ +const DAY_OFFSETS = Array.from({ length: 14 }, (_, i) => 8.64e7 * (i + 1)); /** * A generic base class to assist in creating domain specific data generators. It includes @@ -16,6 +18,7 @@ const OS_FAMILY = ['windows', 'macos', 'linux']; * public method named `generate()` which should be implemented by sub-classes. */ export class BaseDataGenerator { + /** A javascript seeded random number (float between 0 and 1). Don't use `Math.random()` */ protected random: seedrandom.prng; constructor(seed: string | seedrandom.prng = Math.random().toString()) { @@ -33,6 +36,23 @@ export class BaseDataGenerator { throw new Error('method not implemented!'); } + /** Returns a future ISO date string */ + protected randomFutureDate(from?: Date): string { + const now = from ? from.getTime() : Date.now(); + return new Date(now + this.randomChoice(DAY_OFFSETS)).toISOString(); + } + + /** Returns a past ISO date string */ + protected randomPastDate(from?: Date): string { + const now = from ? from.getTime() : Date.now(); + return new Date(now - this.randomChoice(DAY_OFFSETS)).toISOString(); + } + + /** Generate either `true` or `false` */ + protected randomBoolean(): boolean { + return this.random() < 0.5; + } + /** generate random OS family value */ protected randomOSFamily(): string { return this.randomChoice(OS_FAMILY); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts new file mode 100644 index 00000000000000..af799de782f48c --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.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 { DeepPartial } from 'utility-types'; +import { merge } from 'lodash'; +import { BaseDataGenerator } from './base_data_generator'; +import { EndpointAction, EndpointActionResponse, ISOLATION_ACTIONS } from '../types'; + +const ISOLATION_COMMANDS: ISOLATION_ACTIONS[] = ['isolate', 'unisolate']; + +export class FleetActionGenerator extends BaseDataGenerator { + /** Generate an Action */ + generate(overrides: DeepPartial = {}): EndpointAction { + const timeStamp = new Date(this.randomPastDate()); + + return merge( + { + action_id: this.randomUUID(), + '@timestamp': timeStamp.toISOString(), + expiration: this.randomFutureDate(timeStamp), + type: 'INPUT_ACTION', + input_type: 'endpoint', + agents: [this.randomUUID()], + user_id: 'elastic', + data: { + command: this.randomIsolateCommand(), + comment: this.randomString(15), + }, + }, + overrides + ); + } + + /** Generates an action response */ + generateResponse(overrides: DeepPartial = {}): EndpointActionResponse { + const timeStamp = new Date(); + + return merge( + { + action_data: { + command: this.randomIsolateCommand(), + comment: '', + }, + action_id: this.randomUUID(), + agent_id: this.randomUUID(), + started_at: this.randomPastDate(), + completed_at: timeStamp.toISOString(), + error: 'some error happen', + '@timestamp': timeStamp.toISOString(), + }, + overrides + ); + } + + protected randomIsolateCommand() { + return this.randomChoice(ISOLATION_COMMANDS); + } +} diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts index e29a121668bd3b..301a032fb47dfb 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts @@ -87,7 +87,9 @@ describe('data generator', () => { expect(event2.event?.sequence).toBe((firstNonNullValue(event1.event?.sequence) ?? 0) + 1); }); - it('creates the same documents with same random seed', () => { + // Lets run this one multiple times just to ensure that the randomness + // is truly predicable based on the seed passed + it.each([1, 2, 3, 4, 5])('[%#] creates the same documents with same random seed', () => { const generator1 = new EndpointDocGenerator('seed'); const generator2 = new EndpointDocGenerator('seed'); const timestamp = new Date().getTime(); diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index fa7ee84441a9bc..436f1573639c83 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -439,6 +439,8 @@ export class EndpointDocGenerator extends BaseDataGenerator { private createHostData(): HostInfo { const hostName = this.randomHostname(); + const isIsolated = this.randomBoolean(); + return { agent: { version: this.randomVersion(), @@ -465,10 +467,10 @@ export class EndpointDocGenerator extends BaseDataGenerator { applied: this.randomChoice(APPLIED_POLICIES), }, configuration: { - isolation: false, + isolation: isIsolated, }, state: { - isolation: false, + isolation: isIsolated, }, }, }; diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 0dc7891560c2d8..021b9bcb1ecccf 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -28,8 +28,10 @@ import { policyFactory as policyConfigFactory } from './models/policy_config'; import { HostMetadata } from './types'; import { KbnClientWithApiKeySupport } from '../../scripts/endpoint/kbn_client_with_api_key_support'; import { FleetAgentGenerator } from './data_generators/fleet_agent_generator'; +import { FleetActionGenerator } from './data_generators/fleet_action_generator'; const fleetAgentGenerator = new FleetAgentGenerator(); +const fleetActionGenerator = new FleetActionGenerator(); export async function indexHostsAndAlerts( client: Client, @@ -175,6 +177,9 @@ async function indexHostDocs({ }, }, }; + + // Create some actions for this Host + await indexFleetActionsForHost(client, hostMetadata); } await client.index({ @@ -397,3 +402,43 @@ const indexFleetAgentForHost = async ( return agentDoc; }; + +const indexFleetActionsForHost = async ( + esClient: Client, + endpointHost: HostMetadata +): Promise => { + const ES_INDEX_OPTIONS = { headers: { 'X-elastic-product-origin': 'fleet' } }; + const agentId = endpointHost.elastic.agent.id; + + for (let i = 0; i < 5; i++) { + // create an action + const isolateAction = fleetActionGenerator.generate({ + data: { comment: 'data generator: this host is bad' }, + }); + + isolateAction.agents = [agentId]; + + await esClient.index( + { + index: '.fleet-actions', + body: isolateAction, + }, + ES_INDEX_OPTIONS + ); + + // Create an action response for the above + const unIsolateAction = fleetActionGenerator.generateResponse({ + action_id: isolateAction.action_id, + agent_id: agentId, + action_data: isolateAction.data, + }); + + await esClient.index( + { + index: '.fleet-actions-results', + body: unIsolateAction, + }, + ES_INDEX_OPTIONS + ); + } +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 99dac5ea5cda67..fcfda9c9a30d94 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -24,6 +24,21 @@ export interface EndpointAction { }; } +export interface EndpointActionResponse { + '@timestamp': string; + /** The id of the action for which this response is associated with */ + action_id: string; + /** The agent id that sent this action response */ + agent_id: string; + started_at: string; + completed_at: string; + error: string; + action_data: { + command: ISOLATION_ACTIONS; + comment?: string; + }; +} + export type HostIsolationRequestBody = TypeOf; export interface HostIsolationResponse { diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index dd0ff540cb4af0..c084dd8ca76680 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -1095,3 +1095,13 @@ export interface GetAgentSummaryResponse { versions_count: { [key: string]: number }; }; } + +/** + * REST API response for retrieving exception summary + */ +export interface GetExceptionSummaryResponse { + total: number; + windows: number; + macos: number; + linux: number; +} diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 6195dd61a79841..02006fdb29d475 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -15,6 +15,7 @@ const allowedExperimentalValues = Object.freeze({ trustedAppsByPolicyEnabled: false, metricsEntitiesEnabled: false, hostIsolationEnabled: false, + ruleRegistryEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 50a5f62740271f..02dbc56bd33976 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -8,6 +8,7 @@ "actions", "alerting", "cases", + "ruleRegistry", "data", "dataEnhanced", "embeddable", diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 3d29650b750dc9..e4a015525dfb44 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -21,7 +21,7 @@ import type { CreateExceptionListItemSchema, UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; - +import { TestProviders } from '../../mock'; import { useAddOrUpdateException, UseAddOrUpdateExceptionProps, @@ -134,12 +134,16 @@ describe('useAddOrUpdateException', () => { addOrUpdateItemsArgs = [ruleId, itemsToAddOrUpdate]; render = () => - renderHook(() => - useAddOrUpdateException({ - http: mockKibanaHttpService, - onError, - onSuccess, - }) + renderHook( + () => + useAddOrUpdateException({ + http: mockKibanaHttpService, + onError, + onSuccess, + }), + { + wrapper: TestProviders, + } ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx index 5ba73ba2c90588..dbae0964b41a8c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx @@ -19,9 +19,11 @@ import { getUpdateAlertsQuery } from '../../../detections/components/alerts_tabl import { buildAlertStatusFilter, buildAlertsRuleIdFilter, + buildAlertStatusFilterRuleRegistry, } from '../../../detections/components/alerts_table/default_config'; import { getQueryFilter } from '../../../../common/detection_engine/get_query_filter'; import { Index } from '../../../../common/detection_engine/schemas/common/schemas'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { formatExceptionItemForUpdate, prepareExceptionItemsForBulkClose } from './helpers'; import { useKibana } from '../../lib/kibana'; @@ -82,6 +84,8 @@ export const useAddOrUpdateException = ({ }, [] ); + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); useEffect(() => { let isSubscribed = true; @@ -127,10 +131,15 @@ export const useAddOrUpdateException = ({ } if (bulkCloseIndex != null) { + // TODO: Once we are past experimental phase this code should be removed + const alertStatusFilter = ruleRegistryEnabled + ? buildAlertStatusFilterRuleRegistry('open') + : buildAlertStatusFilter('open'); + const filter = getQueryFilter( '', 'kuery', - [...buildAlertsRuleIdFilter(ruleId), ...buildAlertStatusFilter('open')], + [...buildAlertsRuleIdFilter(ruleId), ...alertStatusFilter], bulkCloseIndex, prepareExceptionItemsForBulkClose(exceptionItemsToAddOrUpdate), false @@ -176,7 +185,14 @@ export const useAddOrUpdateException = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [http, onSuccess, onError, updateExceptionListItem, addExceptionListItem]); + }, [ + addExceptionListItem, + http, + onSuccess, + onError, + ruleRegistryEnabled, + updateExceptionListItem, + ]); return [{ isLoading }, addOrUpdateException]; }; 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 af278b09e719c4..71e33c603b65b2 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 @@ -43,6 +43,7 @@ export const mockGlobalState: State = { trustedAppsByPolicyEnabled: false, metricsEntitiesEnabled: false, hostIsolationEnabled: false, + ruleRegistryEnabled: false, }, }, hosts: { diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 90526e84a2262b..9ac7ae0f243225 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -24,11 +24,12 @@ import { import { FieldHook } from '../../shared_imports'; import { SUB_PLUGINS_REDUCER } from './utils'; import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; +import { UserPrivilegesProvider } from '../../detections/components/user_privileges'; const state: State = mockGlobalState; interface Props { - children: React.ReactNode; + children?: React.ReactNode; store?: Store; onDragEnd?: (result: DropResult, provided: ResponderProvided) => void; } @@ -59,7 +60,30 @@ const TestProvidersComponent: React.FC = ({ ); +/** + * A utility for wrapping children in the providers required to run most tests + * WITH user privileges provider. + */ +const TestProvidersWithPrivilegesComponent: React.FC = ({ + children, + store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage), + onDragEnd = jest.fn(), +}) => ( + + + + ({ eui: euiDarkVars, darkMode: true })}> + + {children} + + + + + +); + export const TestProviders = React.memo(TestProvidersComponent); +export const TestProvidersWithPrivileges = React.memo(TestProvidersWithPrivilegesComponent); export const useFormFieldMock = (options?: Partial>): FieldHook => { return { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 478c8930b8dd3b..02a815bc59f3bd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -5,11 +5,12 @@ * 2.0. */ +import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { RowRendererId } from '../../../../common/types/timeline'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; -import { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { columns } from '../../configurations/security_solution_detections/columns'; @@ -124,3 +125,76 @@ export const requiredFieldsForActions = [ 'host.os.family', 'event.code', ]; + +// TODO: Once we are past experimental phase this code should be removed +export const buildAlertStatusFilterRuleRegistry = (status: Status): Filter[] => [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'kibana.rac.alert.status', + params: { + query: status, + }, + }, + query: { + term: { + 'kibana.rac.alert.status': status, + }, + }, + }, +]; + +export const buildShowBuildingBlockFilterRuleRegistry = ( + showBuildingBlockAlerts: boolean +): Filter[] => + showBuildingBlockAlerts + ? [] + : [ + { + meta: { + alias: null, + negate: true, + disabled: false, + type: 'exists', + key: 'kibana.rac.rule.building_block_type', + value: 'exists', + }, + // @ts-expect-error TODO: Rework parent typings to support ExistsFilter[] + exists: { field: 'kibana.rac.rule.building_block_type' }, + }, + ]; + +export const requiredFieldMappingsForActionsRuleRegistry = { + '@timestamp': '@timestamp', + 'alert.id': 'kibana.rac.alert.id', + 'event.kind': 'event.kind', + 'alert.start': 'kibana.rac.alert.start', + 'alert.uuid': 'kibana.rac.alert.uuid', + 'event.action': 'event.action', + 'alert.status': 'kibana.rac.alert.status', + 'alert.duration.us': 'kibana.rac.alert.duration.us', + 'rule.uuid': 'rule.uuid', + 'rule.id': 'rule.id', + 'rule.name': 'rule.name', + 'rule.category': 'rule.category', + producer: 'kibana.rac.alert.producer', + tags: 'tags', +}; + +export const alertsHeadersRuleRegistry: ColumnHeaderOptions[] = Object.entries( + requiredFieldMappingsForActionsRuleRegistry +).map(([alias, field]) => ({ + columnHeaderType: defaultColumnHeaderType, + displayAsText: alias, + id: field, +})); + +export const alertsDefaultModelRuleRegistry: SubsetTimelineModel = { + ...timelineDefaults, + columns: alertsHeadersRuleRegistry, + showCheckboxes: true, + excludedRowRendererIds: Object.values(RowRendererId), +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 9dc83d7898963d..f20754fc446d6e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -16,6 +16,7 @@ import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { HeaderSection } from '../../../common/components/header_section'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { combineQueries } from '../../../timelines/components/timeline/helpers'; import { useKibana } from '../../../common/lib/kibana'; import { inputsSelectors, State, inputsModel } from '../../../common/store'; @@ -29,6 +30,8 @@ import { requiredFieldsForActions, alertsDefaultModel, buildAlertStatusFilter, + alertsDefaultModelRuleRegistry, + buildAlertStatusFilterRuleRegistry, } from './default_config'; import { FILTER_OPEN, AlertsTableFilterGroup } from './alerts_filter_group'; import { AlertsUtilityBar } from './alerts_utility_bar'; @@ -104,6 +107,8 @@ export const AlertsTableComponent: React.FC = ({ const [, dispatchToaster] = useStateToaster(); const { addWarning } = useAppToasts(); const { initializeTimeline, setSelectAll } = useManageTimeline(); + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); const getGlobalQuery = useCallback( (customFilters: Filter[]) => { @@ -236,7 +241,11 @@ export const AlertsTableComponent: React.FC = ({ refetchQuery: inputsModel.Refetch, { status, selectedStatus }: UpdateAlertsStatusProps ) => { - const currentStatusFilter = buildAlertStatusFilter(status); + // TODO: Once we are past experimental phase this code should be removed + const currentStatusFilter = ruleRegistryEnabled + ? buildAlertStatusFilterRuleRegistry(status) + : buildAlertStatusFilter(status); + await updateAlertStatusAction({ query: showClearSelectionAction ? getGlobalQuery(currentStatusFilter)?.filterQuery @@ -258,6 +267,7 @@ export const AlertsTableComponent: React.FC = ({ showClearSelectionAction, onAlertStatusUpdateSuccess, onAlertStatusUpdateFailure, + ruleRegistryEnabled, ] ); @@ -301,18 +311,28 @@ export const AlertsTableComponent: React.FC = ({ ); const defaultFiltersMemo = useMemo(() => { + // TODO: Once we are past experimental phase this code should be removed + const alertStatusFilter = ruleRegistryEnabled + ? buildAlertStatusFilterRuleRegistry(filterGroup) + : buildAlertStatusFilter(filterGroup); + if (isEmpty(defaultFilters)) { - return buildAlertStatusFilter(filterGroup); + return alertStatusFilter; } else if (defaultFilters != null && !isEmpty(defaultFilters)) { - return [...defaultFilters, ...buildAlertStatusFilter(filterGroup)]; + return [...defaultFilters, ...alertStatusFilter]; } - }, [defaultFilters, filterGroup]); + }, [defaultFilters, filterGroup, ruleRegistryEnabled]); const { filterManager } = useKibana().services.data.query; + // TODO: Once we are past experimental phase this code should be removed + const defaultTimelineModel = ruleRegistryEnabled + ? alertsDefaultModelRuleRegistry + : alertsDefaultModel; + useEffect(() => { initializeTimeline({ defaultModel: { - ...alertsDefaultModel, + ...defaultTimelineModel, columns, }, documentType: i18n.ALERTS_DOCUMENT_TYPE, @@ -344,7 +364,7 @@ export const AlertsTableComponent: React.FC = ({ return ( ( - {children} -); - describe('useSignalIndex', () => { let appToastsMock: jest.Mocked>; @@ -33,7 +28,9 @@ describe('useSignalIndex', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useSignalIndex(), - { wrapper: Wrapper } + { + wrapper: TestProvidersWithPrivileges, + } ); await waitForNextUpdate(); expect(result.current).toEqual({ @@ -50,7 +47,9 @@ describe('useSignalIndex', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useSignalIndex(), - { wrapper: Wrapper } + { + wrapper: TestProvidersWithPrivileges, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -69,7 +68,9 @@ describe('useSignalIndex', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useSignalIndex(), - { wrapper: Wrapper } + { + wrapper: TestProvidersWithPrivileges, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -93,7 +94,9 @@ describe('useSignalIndex', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useSignalIndex(), - { wrapper: Wrapper } + { + wrapper: TestProvidersWithPrivileges, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -114,7 +117,9 @@ describe('useSignalIndex', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useSignalIndex(), - { wrapper: Wrapper } + { + wrapper: TestProvidersWithPrivileges, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -140,7 +145,9 @@ describe('useSignalIndex', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useSignalIndex(), - { wrapper: Wrapper } + { + wrapper: TestProvidersWithPrivileges, + } ); await waitForNextUpdate(); await waitForNextUpdate(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx index fdbeab26f11f3a..84eaf8e3aa93c3 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx @@ -6,8 +6,10 @@ */ import { useEffect, useState } from 'react'; +import { DEFAULT_ALERTS_INDEX } from '../../../../../common/constants'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; import { isSecurityAppError } from '../../../../common/utils/api'; @@ -38,6 +40,8 @@ export const useSignalIndex = (): ReturnSignalIndex => { }); const { addError } = useAppToasts(); const { hasIndexRead } = useAlertsPrivileges(); + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); useEffect(() => { let isSubscribed = true; @@ -48,10 +52,15 @@ export const useSignalIndex = (): ReturnSignalIndex => { setLoading(true); const signal = await getSignalIndex({ signal: abortCtrl.signal }); + // TODO: Once we are past experimental phase we can update `getSignalIndex` to return the space-aware DEFAULT_ALERTS_INDEX + const signalIndices = ruleRegistryEnabled + ? `${DEFAULT_ALERTS_INDEX},${signal.name}` + : signal.name; + if (isSubscribed && signal != null) { setSignalIndex({ signalIndexExists: true, - signalIndexName: signal.name, + signalIndexName: signalIndices, signalIndexMappingOutdated: signal.index_mapping_outdated, createDeSignalIndex: createIndex, }); @@ -115,7 +124,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { isSubscribed = false; abortCtrl.abort(); }; - }, [addError, hasIndexRead]); + }, [addError, hasIndexRead, ruleRegistryEnabled]); return { loading, ...signalIndex }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index c1c7e4688bbbe3..8ae7e4fb2852b5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -11,6 +11,7 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { SecurityPageName } from '../../../app/types'; @@ -51,6 +52,7 @@ import { timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { buildShowBuildingBlockFilter, + buildShowBuildingBlockFilterRuleRegistry, buildThreatMatchFilter, } from '../../components/alerts_table/default_config'; import { useSourcererScope } from '../../../common/containers/sourcerer'; @@ -81,6 +83,8 @@ const DetectionEnginePageComponent = () => { const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); const query = useDeepEqualSelector(getGlobalQuerySelector); const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); const { to, from, deleteQuery, setQuery } = useGlobalTime(); const { globalFullScreen } = useGlobalFullScreen(); @@ -134,19 +138,23 @@ const DetectionEnginePageComponent = () => { const alertsHistogramDefaultFilters = useMemo( () => [ ...filters, - ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...(ruleRegistryEnabled + ? buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts) // TODO: Once we are past experimental phase this code should be removed + : buildShowBuildingBlockFilter(showBuildingBlockAlerts)), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], - [filters, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] + [filters, ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] ); // AlertsTable manages global filters itself, so not including `filters` const alertsTableDefaultFilters = useMemo( () => [ - ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...(ruleRegistryEnabled + ? buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts) // TODO: Once we are past experimental phase this code should be removed + : buildShowBuildingBlockFilter(showBuildingBlockAlerts)), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], - [showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] + [ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] ); const onShowBuildingBlockAlertsChangedCallback = useCallback( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index d3793dad8ff1a4..8dac9e03514d1c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -36,6 +36,7 @@ import { useDeepEqualSelector, useShallowEqualSelector, } from '../../../../../common/hooks/use_selector'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { useKibana } from '../../../../../common/lib/kibana'; import { TimelineId } from '../../../../../../common/types/timeline'; import { UpdateDateRange } from '../../../../../common/components/charts/common'; @@ -64,6 +65,7 @@ import { StepScheduleRule } from '../../../../components/rules/step_schedule_rul import { buildAlertsRuleIdFilter, buildShowBuildingBlockFilter, + buildShowBuildingBlockFilterRuleRegistry, buildThreatMatchFilter, } from '../../../../components/alerts_table/default_config'; import { RuleSwitch } from '../../../../components/rules/rule_switch'; @@ -222,6 +224,9 @@ const RuleDetailsPageComponent = () => { const { formatUrl } = useFormatUrl(SecurityPageName.detections); const { globalFullScreen } = useGlobalFullScreen(); + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); + // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); const { @@ -307,10 +312,12 @@ const RuleDetailsPageComponent = () => { const alertDefaultFilters = useMemo( () => [ ...buildAlertsRuleIdFilter(ruleId), - ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...(ruleRegistryEnabled + ? buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts) // TODO: Once we are past experimental phase this code should be removed + : buildShowBuildingBlockFilter(showBuildingBlockAlerts)), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], - [ruleId, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] + [ruleId, ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] ); const alertMergedFilters = useMemo(() => [...alertDefaultFilters, ...filters], [ diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx index 4871cfcb069d2f..f1eab38c56db0a 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx @@ -155,7 +155,7 @@ describe('Hosts - rendering', () => { myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters })); wrapper.update(); expect(wrapper.find(HostsTabs).props().filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' ); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx index 3c92ab31680c20..5ace2b901da11c 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx @@ -25,7 +25,7 @@ export const SearchBar = memo(({ defaultValue = '', onSearch, pl const handleOnSearch = useCallback(() => onSearch(query), [query, onSearch]); return ( - + { + return (await this.httpWrapper()).get( + `${EXCEPTION_LIST_URL}/summary`, + { + query: { + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + namespace_type: 'agnostic', + }, + } + ); + } } diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts index b55a32a937c458..5229e4078eb0dc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts @@ -29,6 +29,7 @@ const createEventFiltersServiceMock = (): jest.Mocked => ({ getOne: jest.fn(), updateOne: jest.fn(), deleteOne: jest.fn(), + getSummary: jest.fn(), }); const createStoreSetup = (eventFiltersService: EventFiltersService) => { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts index be6689b7e5b57b..3bcf6a33693029 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts @@ -10,6 +10,7 @@ import type { CreateExceptionListItemSchema, ExceptionListItemSchema, UpdateExceptionListItemSchema, + ExceptionListSummarySchema, } from '@kbn/securitysolution-io-ts-list-types'; import { AsyncResourceState } from '../../state/async_resource_state'; import { Immutable } from '../../../../common/endpoint/types'; @@ -49,6 +50,7 @@ export interface EventFiltersService { getOne(id: string): Promise; updateOne(exception: Immutable): Promise; deleteOne(id: string): Promise; + getSummary(): Promise; } export interface EventFiltersListPageData { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/trusted_app_items_summary.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx similarity index 52% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/trusted_app_items_summary.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx index fae65def7e2f6e..f42304ffb89aed 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/trusted_app_items_summary.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx @@ -6,56 +6,45 @@ */ import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import React, { FC, memo, useEffect, useState } from 'react'; -import { CoreStart } from 'kibana/public'; +import React, { FC, memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; -import { TrustedAppsHttpService } from '../../../../../trusted_apps/service'; -import { GetTrustedAppsSummaryResponse } from '../../../../../../../../common/endpoint/types'; +import { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types'; -const SUMMARY_KEYS: Readonly> = [ +const SUMMARY_KEYS: Readonly> = [ 'windows', 'macos', 'linux', 'total', ]; -const SUMMARY_LABELS: Readonly<{ [key in keyof GetTrustedAppsSummaryResponse]: string }> = { +const SUMMARY_LABELS: Readonly<{ [key in keyof GetExceptionSummaryResponse]: string }> = { windows: i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.windows', + 'xpack.securitySolution.endpoint.fleetCustomExtension.exceptionItemsSummary.windows', { defaultMessage: 'Windows' } ), linux: i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.linux', + 'xpack.securitySolution.endpoint.fleetCustomExtension.exceptionItemsSummary.linux', { defaultMessage: 'Linux' } ), macos: i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.macos', + 'xpack.securitySolution.endpoint.fleetCustomExtension.exceptionItemsSummary.macos', { defaultMessage: 'Mac' } ), total: i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppItemsSummary.total', + 'xpack.securitySolution.endpoint.fleetCustomExtension.exceptionItemsSummary.total', { defaultMessage: 'Total' } ), }; const CSS_BOLD: Readonly = { fontWeight: 'bold' }; -export const TrustedAppItemsSummary = memo(() => { - const { - services: { http }, - } = useKibana(); - const [stats, setStats] = useState(); - const [trustedAppsApi] = useState(() => new TrustedAppsHttpService(http)); - - useEffect(() => { - trustedAppsApi.getTrustedAppsSummary().then((response) => { - setStats(response); - }); - }, [trustedAppsApi]); +interface ExceptionItemsSummaryProps { + stats: GetExceptionSummaryResponse | undefined; +} +export const ExceptionItemsSummary = memo(({ stats }) => { return ( - + {SUMMARY_KEYS.map((stat) => { return ( @@ -73,18 +62,13 @@ export const TrustedAppItemsSummary = memo(() => { ); }); -TrustedAppItemsSummary.displayName = 'TrustedAppItemsSummary'; +ExceptionItemsSummary.displayName = 'ExceptionItemsSummary'; const SummaryStat: FC<{ value: number; color?: EuiBadgeProps['color'] }> = memo( ({ children, value, color, ...commonProps }) => { return ( - + {children} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx new file mode 100644 index 00000000000000..6f368a89eb5f93 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo, useState, useEffect } from 'react'; +import { ApplicationStart, CoreStart } from 'kibana/public'; +import { EuiPanel, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + PackageCustomExtensionComponentProps, + pagePathGetters, +} from '../../../../../../../../../fleet/public'; +import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; +import { getEventFiltersListPath } from '../../../../../../common/routing'; +import { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types'; +import { PLUGIN_ID as FLEET_PLUGIN_ID } from '../../../../../../../../../fleet/common'; +import { MANAGEMENT_APP_ID } from '../../../../../../common/constants'; +import { useToasts } from '../../../../../../../common/lib/kibana'; +import { LinkWithIcon } from './link_with_icon'; +import { ExceptionItemsSummary } from './exception_items_summary'; +import { EventFiltersHttpService } from '../../../../../event_filters/service'; +import { StyledEuiFlexGridGroup, StyledEuiFlexGridItem } from './styled_components'; + +export const FleetEventFiltersCard = memo(({ pkgkey }) => { + const { + services: { + application: { getUrlForApp }, + http, + }, + } = useKibana(); + const toasts = useToasts(); + const [stats, setStats] = useState(); + const eventFiltersListUrlPath = getEventFiltersListPath(); + const eventFiltersApi = useMemo(() => new EventFiltersHttpService(http), [http]); + + useEffect(() => { + const fetchStats = async () => { + try { + const summary = await eventFiltersApi.getSummary(); + setStats(summary); + } catch (error) { + toasts.addDanger( + i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummaryError', + { + defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"', + values: { error }, + } + ) + ); + } + }; + fetchStats(); + }, [eventFiltersApi, toasts]); + + const eventFiltersRouteState = useMemo(() => { + const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details_custom({ pkgkey })}`; + return { + backButtonLabel: i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel', + { defaultMessage: 'Back to Endpoint Integration' } + ), + onBackButtonNavigateTo: [ + FLEET_PLUGIN_ID, + { + path: fleetPackageCustomUrlPath, + }, + ], + backButtonUrl: getUrlForApp(FLEET_PLUGIN_ID, { + path: fleetPackageCustomUrlPath, + }), + }; + }, [getUrlForApp, pkgkey]); + + return ( + + + + +

+ +

+
+
+ + + + + <> + + + + + +
+
+ ); +}); + +FleetEventFiltersCard.displayName = 'FleetEventFiltersCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx index fe6f82e632f734..ec1479643999a9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { memo, useMemo } from 'react'; -import { ApplicationStart } from 'kibana/public'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import React, { memo, useMemo, useState, useEffect } from 'react'; +import { ApplicationStart, CoreStart } from 'kibana/public'; +import { EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -16,19 +16,48 @@ import { } from '../../../../../../../../../fleet/public'; import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; import { getTrustedAppsListPath } from '../../../../../../common/routing'; -import { TrustedAppsListPageRouteState } from '../../../../../../../../common/endpoint/types'; +import { + TrustedAppsListPageRouteState, + GetExceptionSummaryResponse, +} from '../../../../../../../../common/endpoint/types'; import { PLUGIN_ID as FLEET_PLUGIN_ID } from '../../../../../../../../../fleet/common'; import { MANAGEMENT_APP_ID } from '../../../../../../common/constants'; +import { useToasts } from '../../../../../../../common/lib/kibana'; import { LinkWithIcon } from './link_with_icon'; -import { TrustedAppItemsSummary } from './trusted_app_items_summary'; +import { ExceptionItemsSummary } from './exception_items_summary'; +import { TrustedAppsHttpService } from '../../../../../trusted_apps/service'; +import { StyledEuiFlexGridGroup, StyledEuiFlexGridItem } from './styled_components'; export const FleetTrustedAppsCard = memo(({ pkgkey }) => { const { services: { application: { getUrlForApp }, + http, }, - } = useKibana<{ application: ApplicationStart }>(); + } = useKibana(); + const toasts = useToasts(); + const [stats, setStats] = useState(); + const trustedAppsApi = useMemo(() => new TrustedAppsHttpService(http), [http]); + useEffect(() => { + const fetchStats = async () => { + try { + const response = await trustedAppsApi.getTrustedAppsSummary(); + setStats(response); + } catch (error) { + toasts.addDanger( + i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummaryError', + { + defaultMessage: 'There was an error trying to fetch trusted apps stats: "{error}"', + values: { error }, + } + ) + ); + } + }; + fetchStats(); + }, [toasts, trustedAppsApi]); const trustedAppsListUrlPath = getTrustedAppsListPath(); const trustedAppRouteState = useMemo(() => { @@ -52,8 +81,8 @@ export const FleetTrustedAppsCard = memo(( return ( - - + +

(( />

-
- - - - - + + + + + + <> (( defaultMessage="Manage trusted applications" /> - - -
+ + +
); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx new file mode 100644 index 00000000000000..8791f7fa872831 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.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 styled from 'styled-components'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)` + display: grid; + grid-template-columns: 25% 45% 30%; + grid-template-areas: 'title summary link'; +`; + +export const StyledEuiFlexGridItem = styled(EuiFlexItem)<{ + gridArea: string; + alignItems?: string; +}>` + grid-area: ${({ gridArea }) => gridArea}; + align-items: ${({ alignItems }) => alignItems ?? 'center'}; + margin: 0px; + padding: 12px; +`; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx index c127a60d84ccff..094f1131d7034f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx @@ -5,15 +5,19 @@ * 2.0. */ +import { EuiSpacer } from '@elastic/eui'; import React, { memo } from 'react'; import { PackageCustomExtensionComponentProps } from '../../../../../../../../fleet/public'; import { FleetTrustedAppsCard } from './components/fleet_trusted_apps_card'; +import { FleetEventFiltersCard } from './components/fleet_event_filters_card'; export const EndpointPackageCustomExtension = memo( (props) => { return (
+ +
); } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap index 03acc9b2297adc..47728eacf4cddf 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap @@ -40,6 +40,26 @@ exports[`trusted_app_card TrustedAppCard should render correctly 1`] = ` /> } /> + + } + /> + + } + /> } /> + + } + /> + + } + /> ( } /> + + } + /> + + } + /> +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -762,6 +782,26 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -1031,6 +1071,26 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -1300,6 +1360,26 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -1569,6 +1649,26 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -1838,6 +1938,26 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -2107,6 +2227,26 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -2376,6 +2516,26 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -2645,6 +2805,26 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -2914,6 +3094,26 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -3488,6 +3688,26 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -3757,6 +3977,26 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -4026,6 +4266,26 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -4295,6 +4555,26 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -4564,6 +4844,26 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -4833,6 +5133,26 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -5102,6 +5422,26 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -5371,6 +5711,26 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -5640,6 +6000,26 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -5909,6 +6289,26 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -6440,6 +6840,26 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -6709,6 +7129,26 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -6978,6 +7418,26 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -7247,6 +7707,26 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -7516,6 +7996,26 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -7785,6 +8285,26 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -8054,6 +8574,26 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -8323,6 +8863,26 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -8592,6 +9152,26 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
@@ -8861,6 +9441,26 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 15f35705c7fdc3..c4f2a74ea173df 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -842,6 +842,26 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` +
+ Date Modified +
+
+ 1 minute ago +
+
+ Modified By +
+
+ someone +
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index 4dfe1d96a56c15..803e292c58eb5a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -77,10 +77,10 @@ export const PROPERTY_TITLES: Readonly< defaultMessage: 'Created By', }), updated_at: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.updatedAt', { - defaultMessage: 'Date Updated', + defaultMessage: 'Date Modified', }), updated_by: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.updatedBy', { - defaultMessage: 'Updated By', + defaultMessage: 'Modified By', }), description: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.description', { defaultMessage: 'Description', diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index 874d1a4a969c4c..3f02d505daea1e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -169,10 +169,15 @@ describe('When on the Trusted Apps Page', () => { it('should display a Add Trusted App button', async () => { const { getByTestId } = await renderWithListData(); - const addButton = await getByTestId('trustedAppsListAddButton'); + const addButton = getByTestId('trustedAppsListAddButton'); expect(addButton.textContent).toBe('Add Trusted Application'); }); + it('should display the searchbar', async () => { + const renderResult = await renderWithListData(); + expect(await renderResult.findByTestId('searchBar')).not.toBeNull(); + }); + describe('and the Grid view is being displayed', () => { describe('and the edit trusted app button is clicked', () => { let renderResult: ReturnType; @@ -555,7 +560,7 @@ describe('When on the Trusted Apps Page', () => { // to test the UI behaviours while the API call is in flight coreStart.http.post.mockImplementation( // @ts-ignore - async (path: string, options: HttpFetchOptions) => { + async (_, options: HttpFetchOptions) => { return new Promise((resolve, reject) => { httpPostBody = options.body as string; resolveHttpPost = resolve; @@ -861,6 +866,14 @@ describe('When on the Trusted Apps Page', () => { expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull(); }); + + it('should not display the searchbar', async () => { + const renderResult = render(); + await act(async () => { + await waitForAction('trustedAppsExistStateChanged'); + }); + expect(renderResult.queryByTestId('searchBar')).toBeNull(); + }); }); describe('and the search is dispatched', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index ac06254a531001..5603b8e2d61c90 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -96,34 +96,36 @@ export const TrustedAppsPage = memo(() => { /> )} - {doEntriesExist ? ( - - - - - + <> + + - - - - - {location.view_type === 'grid' && } - {location.view_type === 'list' && } - - + + + + + + + + + {location.view_type === 'grid' && } + {location.view_type === 'list' && } + + + ) : ( )} diff --git a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx index 862a4f1a56c12a..764b8fcd0444b1 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx @@ -159,7 +159,7 @@ describe('Network page - rendering', () => { myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters })); wrapper.update(); expect(wrapper.find(NetworkRoutes).props().filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' ); }); }); diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index c1f501d3f70945..2e41e291156aaa 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -44,6 +44,7 @@ import { APP_PATH, DEFAULT_INDEX_KEY, DETECTION_ENGINE_INDEX_URL, + DEFAULT_ALERTS_INDEX, } from '../common/constants'; import { SecurityPageName } from './app/types'; @@ -446,6 +447,9 @@ 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 }, @@ -474,9 +478,15 @@ export class Plugin implements IPlugin { isEventViewer, }) ).toEqual({ - filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + filterQuery: '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}', }); }); @@ -299,7 +299,7 @@ describe('Combined Queries', () => { }) ).toEqual({ filterQuery: - '{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', + '{"bool":{"must":[],"filter":[{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/containers/helpers.ts index 068f813e0fc40a..2f815cc44b3acb 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/helpers.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_ALERTS_INDEX } from '../../../common/constants'; import { TimelineId } from '../../../common/types/timeline'; export const detectionsTimelineIds = [ @@ -12,7 +13,14 @@ export const detectionsTimelineIds = [ TimelineId.detectionsRulesDetailsPage, ]; -export const skipQueryForDetectionsPage = (id: string, defaultIndex: string[]) => +// TODO: Once we are past experimental phase `useRuleRegistry` should be removed +export const skipQueryForDetectionsPage = ( + id: string, + defaultIndex: string[], + useRuleRegistry = false +) => id != null && detectionsTimelineIds.some((timelineId) => timelineId === id) && - !defaultIndex.some((di) => di.toLowerCase().startsWith('.siem-signals')); + !defaultIndex.some((di) => + di.toLowerCase().startsWith(useRuleRegistry ? DEFAULT_ALERTS_INDEX : '.siem-signals') + ); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx index 1032d0ec1672ac..62846eb01e60f6 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -9,6 +9,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { initSortDefault, TimelineArgs, useTimelineEvents, UseTimelineEventsProps } from '.'; import { SecurityPageName } from '../../../common/constants'; import { TimelineId } from '../../../common/types/timeline'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { mockTimelineData } from '../../common/mock'; import { useRouteSpy } from '../../common/utils/route/use_route_spy'; @@ -26,6 +27,9 @@ const mockEvents = mockTimelineData.filter((i, index) => index <= 11); const mockSearch = jest.fn(); +jest.mock('../../common/hooks/use_experimental_features'); +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; + jest.mock('../../common/lib/kibana', () => ({ useToasts: jest.fn().mockReturnValue({ addError: jest.fn(), @@ -93,6 +97,7 @@ mockUseRouteSpy.mockReturnValue([ ]); describe('useTimelineEvents', () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); beforeEach(() => { mockSearch.mockReset(); }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 92199336b978c1..17c107899d85ab 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -13,6 +13,7 @@ import { Subscription } from 'rxjs'; import { ESQuery } from '../../../common/typed_json'; import { isCompleteResponse, isErrorResponse } from '../../../../../../src/plugins/data/public'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { inputsModel, KueryFilterQueryKind } from '../../common/store'; import { useKibana } from '../../common/lib/kibana'; import { createFilter } from '../../common/containers/helpers'; @@ -197,6 +198,9 @@ export const useTimelineEvents = ({ }); const { addError, addWarning } = useAppToasts(); + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); + const timelineSearch = useCallback( (request: TimelineRequest | null) => { if (request == null || pageName === '' || skip) { @@ -305,7 +309,10 @@ export const useTimelineEvents = ({ ); useEffect(() => { - if (skipQueryForDetectionsPage(id, indexNames) || indexNames.length === 0) { + if ( + skipQueryForDetectionsPage(id, indexNames, ruleRegistryEnabled) || + indexNames.length === 0 + ) { return; } @@ -364,7 +371,10 @@ export const useTimelineEvents = ({ activeTimeline.setActivePage(newActivePage); } } - if (!skipQueryForDetectionsPage(id, indexNames) && !deepEqual(prevRequest, currentRequest)) { + if ( + !skipQueryForDetectionsPage(id, indexNames, ruleRegistryEnabled) && + !deepEqual(prevRequest, currentRequest) + ) { return currentRequest; } return prevRequest; @@ -380,6 +390,7 @@ export const useTimelineEvents = ({ id, language, limit, + ruleRegistryEnabled, startDate, sort, fields, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/rule_type.ts new file mode 100644 index 00000000000000..f7e0dd9eb36205 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/rule_type.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 { of } from 'rxjs'; +import { v4 } from 'uuid'; + +import { Logger } from 'kibana/server'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; + +import type { RuleDataClient } from '../../../../../../rule_registry/server'; +import { PluginSetupContract as AlertingPluginSetupContract } from '../../../../../../alerting/server'; +import { ConfigType } from '../../../../config'; + +export const createRuleTypeMocks = () => { + /* eslint-disable @typescript-eslint/no-explicit-any */ + let alertExecutor: (...args: any[]) => Promise; + + const mockedConfig$ = of({} as ConfigType); + + const loggerMock = ({ + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + } as unknown) as Logger; + + const alerting = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPluginSetupContract; + + const scheduleActions = jest.fn(); + + const services = { + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + findAlerts: jest.fn(), // TODO: does this stay? + alertWithPersistence: jest.fn(), + logger: loggerMock, + }; + + return { + dependencies: { + alerting, + config$: mockedConfig$, + logger: loggerMock, + ruleDataClient: ({ + getReader: () => { + return { + search: jest.fn(), + }; + }, + getWriter: () => { + return { + bulk: jest.fn(), + }; + }, + } as unknown) as RuleDataClient, + }, + services, + scheduleActions, + executor: async ({ params }: { params: Record }) => { + return alertExecutor({ + services, + params, + alertId: v4(), + startedAt: new Date(), + }); + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/threshold.ts new file mode 100644 index 00000000000000..40d2ed37a55769 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/threshold.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { sampleDocNoSortId } from '../../signals/__mocks__/es_results'; + +export const mockThresholdResults = { + rawResponse: { + body: { + is_partial: false, + is_running: false, + took: 527, + timed_out: false, + hits: { + total: { + value: 0, + relation: 'eq', + }, + hits: [], + }, + aggregations: { + 'threshold_0:source.ip': { + buckets: [ + { + key: '127.0.0.1', + doc_count: 5, + 'threshold_1:host.name': { + buckets: [ + { + key: 'tardigrade', + doc_count: 3, + top_threshold_hits: { + hits: { + total: { + value: 1, + relation: 'eq', + }, + hits: [ + { + ...sampleDocNoSortId(), + 'host.name': 'tardigrade', + }, + ], + }, + }, + cardinality_count: { + value: 3, + }, + }, + ], + }, + }, + ], + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts new file mode 100644 index 00000000000000..6529c594dd5a51 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { sequenceResponse } from '../../../search_strategy/timeline/eql/__mocks__'; + +import { createEqlAlertType } from './eql'; +import { createRuleTypeMocks } from './__mocks__/rule_type'; + +describe('EQL alerts', () => { + it('does not send an alert when sequence not found', async () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const eqlAlertType = createEqlAlertType(dependencies.ruleDataClient, dependencies.logger); + + dependencies.alerting.registerType(eqlAlertType); + + const params = { + eqlQuery: 'sequence by host.name↵[any where true]↵[any where true]↵[any where true]', + indexPatterns: ['*'], + }; + + services.scopedClusterClient.asCurrentUser.transport.request.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + sequences: [], + events: [], + total: { + relation: 'eq', + value: 0, + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + await executor({ params }); + expect(services.alertInstanceFactory).not.toBeCalled(); + }); + + it('sends a properly formatted alert when sequence is found', async () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const eqlAlertType = createEqlAlertType(dependencies.ruleDataClient, dependencies.logger); + + dependencies.alerting.registerType(eqlAlertType); + + const params = { + eqlQuery: 'sequence by host.name↵[any where true]↵[any where true]↵[any where true]', + indexPatterns: ['*'], + }; + + services.scopedClusterClient.asCurrentUser.transport.request.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: sequenceResponse.rawResponse.body.hits, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + await executor({ params }); + expect(services.alertInstanceFactory).toBeCalled(); + /* + expect(services.alertWithPersistence).toBeCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + 'event.kind': 'signal', + 'kibana.rac.alert.building_block_type': 'default', + }), + ]) + ); + */ + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.ts new file mode 100644 index 00000000000000..39d02c808d09e5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.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. + */ + +import moment from 'moment'; +import v4 from 'uuid/v4'; + +import { ApiResponse } from '@elastic/elasticsearch'; +import { schema } from '@kbn/config-schema'; +import { Logger } from '@kbn/logging'; + +import { + RuleDataClient, + createPersistenceRuleTypeFactory, +} from '../../../../../rule_registry/server'; +import { EQL_ALERT_TYPE_ID } from '../../../../common/constants'; +import { buildEqlSearchRequest } from '../../../../common/detection_engine/get_query_filter'; +import { BaseSignalHit, EqlSignalSearchResponse } from '../signals/types'; + +export const createEqlAlertType = (ruleDataClient: RuleDataClient, logger: Logger) => { + const createPersistenceRuleType = createPersistenceRuleTypeFactory({ + ruleDataClient, + logger, + }); + return createPersistenceRuleType({ + id: EQL_ALERT_TYPE_ID, + name: 'EQL Rule', + validate: { + params: schema.object({ + eqlQuery: schema.string(), + indexPatterns: schema.arrayOf(schema.string()), + }), + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + producer: 'security-solution', + async executor({ + startedAt, + services: { alertWithPersistence, findAlerts, scopedClusterClient }, + params: { indexPatterns, eqlQuery }, + }) { + const from = moment(startedAt).subtract(moment.duration(5, 'm')).toISOString(); // hardcoded 5-minute rule interval + const to = startedAt.toISOString(); + + const request = buildEqlSearchRequest( + eqlQuery, + indexPatterns, + from, + to, + 10, + undefined, + [], + undefined + ); + const { body: response } = (await scopedClusterClient.asCurrentUser.transport.request( + request + )) as ApiResponse; + + const buildSignalFromEvent = (event: BaseSignalHit) => { + return { + ...event, + 'event.kind': 'signal', + 'kibana.rac.alert.id': '???', + 'kibana.rac.alert.uuid': v4(), + '@timestamp': new Date().toISOString(), + }; + }; + + /* eslint-disable @typescript-eslint/no-explicit-any */ + let alerts: any[] = []; + if (response.hits.sequences !== undefined) { + alerts = response.hits.sequences.reduce((allAlerts: any[], sequence) => { + let previousAlertUuid: string | undefined; + return [ + ...allAlerts, + ...sequence.events.map((event, idx) => { + const alert = { + ...buildSignalFromEvent(event), + 'kibana.rac.alert.ancestors': previousAlertUuid != null ? [previousAlertUuid] : [], + 'kibana.rac.alert.building_block_type': 'default', + 'kibana.rac.alert.depth': idx, + }; + previousAlertUuid = alert['kibana.rac.alert.uuid']; + return alert; + }), + ]; + }, []); + } else if (response.hits.events !== undefined) { + alerts = response.hits.events.map((event) => { + return buildSignalFromEvent(event); + }, []); + } else { + throw new Error( + 'eql query response should have either `sequences` or `events` but had neither' + ); + } + + if (alerts.length > 0) { + alertWithPersistence(alerts).forEach((alert) => { + alert.scheduleActions('default', { server: 'server-test' }); + }); + } + + return { + lastChecked: new Date(), + }; + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/ml.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/ml.ts new file mode 100644 index 00000000000000..c07d0436cc90d0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/ml.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + +import { schema } from '@kbn/config-schema'; +import { KibanaRequest, Logger } from 'src/core/server'; +import { SavedObject } from 'src/core/types'; + +import { buildEsQuery, IIndexPattern } from '../../../../../../../src/plugins/data/common'; + +import { createPersistenceRuleTypeFactory } from '../../../../../rule_registry/server'; +import { ML_ALERT_TYPE_ID } from '../../../../common/constants'; +import { SecurityRuleRegistry } from '../../../plugin'; + +const createSecurityMlRuleType = createPersistenceRuleTypeFactory(); + +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../alerting/server'; +import { ListClient } from '../../../../../lists/server'; +import { isJobStarted } from '../../../../common/machine_learning/helpers'; +import { ExceptionListItemSchema } from '../../../../common/shared_imports'; +import { SetupPlugins } from '../../../plugin'; +import { RefreshTypes } from '../types'; +import { bulkCreateMlSignals } from '../signals/bulk_create_ml_signals'; +import { filterEventsAgainstList } from '../signals/filters/filter_events_against_list'; +import { findMlSignals } from '../signals/find_ml_signals'; +import { BuildRuleMessage } from '../signals/rule_messages'; +import { RuleStatusService } from '../signals/rule_status_service'; +import { MachineLearningRuleAttributes } from '../signals/types'; +import { createErrorsFromShard, createSearchAfterReturnType, mergeReturns } from '../signals/utils'; + +export const mlAlertType = createSecurityMlRuleType({ + id: ML_ALERT_TYPE_ID, + name: 'Machine Learning Rule', + validate: { + params: schema.object({ + indexPatterns: schema.arrayOf(schema.string()), + customQuery: schema.string(), + }), + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + producer: 'security-solution', + async executor({ + services: { alertWithPersistence, findAlerts }, + params: { indexPatterns, customQuery }, + }) { + return { + lastChecked: new Date(), + }; + }, +}); +*/ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.test.ts new file mode 100644 index 00000000000000..e8c45e9ab70568 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { v4 } from 'uuid'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { sampleDocNoSortId } from '../signals/__mocks__/es_results'; + +import { createQueryAlertType } from './query'; +import { createRuleTypeMocks } from './__mocks__/rule_type'; + +describe('Custom query alerts', () => { + it('does not send an alert when no events found', async () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const queryAlertType = createQueryAlertType(dependencies.ruleDataClient, dependencies.logger); + + dependencies.alerting.registerType(queryAlertType); + + const params = { + customQuery: 'dne:42', + indexPatterns: ['*'], + }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + sequences: [], + events: [], + total: { + relation: 'eq', + value: 0, + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + await executor({ params }); + expect(services.alertInstanceFactory).not.toBeCalled(); + }); + + it('sends a properly formatted alert when events are found', async () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const queryAlertType = createQueryAlertType(dependencies.ruleDataClient, dependencies.logger); + + dependencies.alerting.registerType(queryAlertType); + + const params = { + customQuery: '*:*', + indexPatterns: ['*'], + }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [sampleDocNoSortId(v4()), sampleDocNoSortId(v4()), sampleDocNoSortId(v4())], + total: { + relation: 'eq', + value: 3, + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + await executor({ params }); + expect(services.alertInstanceFactory).toBeCalled(); + /* + expect(services.alertWithPersistence).toBeCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + 'event.kind': 'signal', + }), + ]) + ); + */ + }); +}); 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 new file mode 100644 index 00000000000000..3911dcabc34de8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryContainer } from '@elastic/elasticsearch/api/types'; +import { schema } from '@kbn/config-schema'; +import { Logger } from '@kbn/logging'; +import { ESSearchRequest } from 'typings/elasticsearch'; + +import { buildEsQuery, IIndexPattern } from '../../../../../../../src/plugins/data/common'; + +import { + RuleDataClient, + createPersistenceRuleTypeFactory, +} from '../../../../../rule_registry/server'; +import { CUSTOM_ALERT_TYPE_ID } from '../../../../common/constants'; + +export const createQueryAlertType = (ruleDataClient: RuleDataClient, logger: Logger) => { + const createPersistenceRuleType = createPersistenceRuleTypeFactory({ + ruleDataClient, + logger, + }); + return createPersistenceRuleType({ + id: CUSTOM_ALERT_TYPE_ID, + name: 'Custom Query Rule', + validate: { + params: schema.object({ + indexPatterns: schema.arrayOf(schema.string()), + customQuery: schema.string(), + }), + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + producer: 'security-solution', + async executor({ + services: { alertWithPersistence, findAlerts }, + params: { indexPatterns, customQuery }, + }) { + try { + const indexPattern: IIndexPattern = { + fields: [], + title: indexPatterns.join(), + }; + + // TODO: kql or lucene? + + const esQuery = buildEsQuery( + indexPattern, + { query: customQuery, language: 'kuery' }, + [] + ) as QueryContainer; + const query: ESSearchRequest = { + body: { + query: esQuery, + fields: ['*'], + sort: { + '@timestamp': 'asc' as const, + }, + }, + }; + + const alerts = await findAlerts(query); + // console.log('alerts', alerts); + alertWithPersistence(alerts).forEach((alert) => { + alert.scheduleActions('default', { server: 'server-test' }); + }); + + return { + lastChecked: new Date(), + }; + } catch (error) { + logger.error(error); + } + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_eql.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_eql.sh new file mode 100755 index 00000000000000..25e247a08ef46d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_eql.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +curl -X POST http://localhost:5601/${BASE_PATH}/api/alerts/alert \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -H 'kbn-xsrf: true' \ + -H 'Content-Type: application/json' \ + --verbose \ + -d ' +{ + "params":{ + "indexPatterns": ["*"], + "eqlQuery": "sequence by host.name↵[any where true]↵[any where true]↵[any where true]" + }, + "consumer":"alerts", + "alertTypeId":"siem.eqlRule", + "schedule":{ + "interval":"1m" + }, + "actions":[], + "tags":[ + "eql", + "persistence" + ], + "notifyWhen":"onActionGroupChange", + "name":"Basic EQL rule" +}' + + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_query.sh new file mode 100755 index 00000000000000..c34af7dee4044d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_query.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +curl -X POST http://localhost:5601/${BASE_PATH}/api/alerts/alert \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -H 'kbn-xsrf: true' \ + -H 'Content-Type: application/json' \ + --verbose \ + -d ' +{ + "params":{ + "indexPatterns": ["*"], + "customQuery": "*:*" + }, + "consumer":"alerts", + "alertTypeId":"siem.customRule", + "schedule":{ + "interval":"1m" + }, + "actions":[], + "tags":[ + "custom", + "persistence" + ], + "notifyWhen":"onActionGroupChange", + "name":"Basic custom query rule" +}' + + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh new file mode 100755 index 00000000000000..8b486b165c34b3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +curl -X POST http://localhost:5601/${BASE_PATH}/api/alerts/alert \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -H 'kbn-xsrf: true' \ + -H 'Content-Type: application/json' \ + --verbose \ + -d ' +{ + "params":{ + "indexPatterns": ["*"], + "customQuery": "*:*", + "thresholdFields": ["source.ip", "destination.ip"], + "thresholdValue": 50, + "thresholdCardinality": [] + }, + "consumer":"alerts", + "alertTypeId":"siem.thresholdRule", + "schedule":{ + "interval":"1m" + }, + "actions":[], + "tags":[ + "persistence", + "threshold" + ], + "notifyWhen":"onActionGroupChange", + "name":"Basic Threshold rule" +}' + + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.test.ts new file mode 100644 index 00000000000000..36e53b8154e70f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.test.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { createRuleTypeMocks } from './__mocks__/rule_type'; +import { mockThresholdResults } from './__mocks__/threshold'; +import { createThresholdAlertType } from './threshold'; + +describe('Threshold alerts', () => { + it('does not send an alert when threshold is not met', async () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const thresholdAlertType = createThresholdAlertType( + dependencies.ruleDataClient, + dependencies.logger + ); + + dependencies.alerting.registerType(thresholdAlertType); + + const params = { + indexPatterns: ['*'], + customQuery: '*:*', + thresholdFields: ['source.ip', 'host.name'], + thresholdValue: 4, + }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + sequences: [], + events: [], + total: { + relation: 'eq', + value: 0, + }, + }, + aggregations: { + 'threshold_0:source.ip': { + buckets: [], + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + await executor({ params }); + expect(services.alertInstanceFactory).not.toBeCalled(); + }); + + it('sends a properly formatted alert when threshold is met', async () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const thresholdAlertType = createThresholdAlertType( + dependencies.ruleDataClient, + dependencies.logger + ); + + dependencies.alerting.registerType(thresholdAlertType); + + const params = { + indexPatterns: ['*'], + customQuery: '*:*', + thresholdFields: ['source.ip', 'host.name'], + thresholdValue: 4, + }; + + services.scopedClusterClient.asCurrentUser.search + .mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + total: { + relation: 'eq', + value: 0, + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ) + .mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + total: { + relation: 'eq', + value: 0, + }, + }, + aggregations: mockThresholdResults.rawResponse.body.aggregations, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + await executor({ params }); + expect(services.alertInstanceFactory).toBeCalled(); + /* + expect(services.alertWithPersistence).toBeCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + 'event.kind': 'signal', + }), + ]) + ); + */ + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.ts new file mode 100644 index 00000000000000..d4721e8bab11dc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.ts @@ -0,0 +1,206 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 v4 from 'uuid/v4'; + +import { schema } from '@kbn/config-schema'; +import { Logger } from '@kbn/logging'; + +import { AlertServices } from '../../../../../alerting/server'; +import { + RuleDataClient, + createPersistenceRuleTypeFactory, +} from '../../../../../rule_registry/server'; +import { THRESHOLD_ALERT_TYPE_ID } from '../../../../common/constants'; +import { SignalSearchResponse, ThresholdSignalHistory } from '../signals/types'; +import { + findThresholdSignals, + getThresholdBucketFilters, + getThresholdSignalHistory, + transformThresholdResultsToEcs, +} from '../signals/threshold'; +import { getFilter } from '../signals/get_filter'; +import { BuildRuleMessage } from '../signals/rule_messages'; + +interface RuleParams { + indexPatterns: string[]; + customQuery: string; + thresholdFields: string[]; + thresholdValue: number; + thresholdCardinality: Array<{ + field: string; + value: number; + }>; +} + +interface BulkCreateThresholdSignalParams { + results: SignalSearchResponse; + ruleParams: RuleParams; + services: AlertServices & { logger: Logger }; + inputIndexPattern: string[]; + ruleId: string; + startedAt: Date; + from: Date; + thresholdSignalHistory: ThresholdSignalHistory; + buildRuleMessage: BuildRuleMessage; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const formatThresholdSignals = (params: BulkCreateThresholdSignalParams): any[] => { + const thresholdResults = params.results; + const threshold = { + field: params.ruleParams.thresholdFields, + value: params.ruleParams.thresholdValue, + }; + const results = transformThresholdResultsToEcs( + thresholdResults, + params.ruleParams.indexPatterns.join(','), + params.startedAt, + params.from, + undefined, + params.services.logger, + threshold, + params.ruleId, + undefined, + params.thresholdSignalHistory + ); + return results.hits.hits.map((hit) => { + return { + ...hit, + 'event.kind': 'signal', + 'kibana.rac.alert.id': '???', + 'kibana.rac.alert.uuid': v4(), + '@timestamp': new Date().toISOString(), + }; + }); +}; + +export const createThresholdAlertType = (ruleDataClient: RuleDataClient, logger: Logger) => { + const createPersistenceRuleType = createPersistenceRuleTypeFactory({ + ruleDataClient, + logger, + }); + return createPersistenceRuleType({ + id: THRESHOLD_ALERT_TYPE_ID, + name: 'Threshold Rule', + validate: { + params: schema.object({ + indexPatterns: schema.arrayOf(schema.string()), + customQuery: schema.string(), + thresholdFields: schema.arrayOf(schema.string()), + thresholdValue: schema.number(), + thresholdCardinality: schema.arrayOf( + schema.object({ + field: schema.string(), + value: schema.number(), + }) + ), + }), + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + producer: 'security-solution', + async executor({ startedAt, services, params, alertId }) { + const fromDate = moment(startedAt).subtract(moment.duration(5, 'm')); // hardcoded 5-minute rule interval + const from = fromDate.toISOString(); + const to = startedAt.toISOString(); + + // TODO: how to get the output index? + const outputIndex = ['.kibana-madi-8-alerts-security-solution-8.0.0-000001']; + const buildRuleMessage = (...messages: string[]) => messages.join(); + const timestampOverride = undefined; + + const { + thresholdSignalHistory, + searchErrors: previousSearchErrors, + } = await getThresholdSignalHistory({ + indexPattern: outputIndex, + from, + to, + services: (services as unknown) as AlertServices, + logger, + ruleId: alertId, + bucketByFields: params.thresholdFields, + timestampOverride, + buildRuleMessage, + }); + + const bucketFilters = await getThresholdBucketFilters({ + thresholdSignalHistory, + timestampOverride, + }); + + const esFilter = await getFilter({ + type: 'threshold', + filters: bucketFilters, + language: 'kuery', + query: params.customQuery, + savedId: undefined, + services: (services as unknown) as AlertServices, + index: params.indexPatterns, + lists: [], + }); + + const { + searchResult: thresholdResults, + searchErrors, + searchDuration: thresholdSearchDuration, + } = await findThresholdSignals({ + inputIndexPattern: params.indexPatterns, + from, + to, + services: (services as unknown) as AlertServices, + logger, + filter: esFilter, + threshold: { + field: params.thresholdFields, + value: params.thresholdValue, + cardinality: params.thresholdCardinality, + }, + timestampOverride, + buildRuleMessage, + }); + + logger.info(`Threshold search took ${thresholdSearchDuration}ms`); // TODO: rule status service + + const alerts = formatThresholdSignals({ + results: thresholdResults, + ruleParams: params, + services: (services as unknown) as AlertServices & { logger: Logger }, + inputIndexPattern: ['TODO'], + ruleId: alertId, + startedAt, + from: fromDate.toDate(), + thresholdSignalHistory, + buildRuleMessage, + }); + + const errors = searchErrors.concat(previousSearchErrors); + if (errors.length === 0) { + services.alertWithPersistence(alerts).forEach((alert) => { + alert.scheduleActions('default', { server: 'server-test' }); + }); + } else { + throw new Error(errors.join('\n')); + } + + return { + lastChecked: new Date(), + }; + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 43377251019176..857762dec45e94 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -491,6 +491,72 @@ export const getFindResultStatus = (): SavedObjectsFindResponse => ({ + page: 1, + per_page: 6, + total: 2, + saved_objects: [], + aggregations: { + alertIds: { + buckets: [ + { + key: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + most_recent_statuses: { + hits: { + hits: [ + { + _source: { + 'siem-detection-engine-rule-status': { + alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + statusDate: '2020-02-18T15:26:49.783Z', + status: 'succeeded', + lastFailureAt: undefined, + lastSuccessAt: '2020-02-18T15:26:49.783Z', + lastFailureMessage: undefined, + lastSuccessMessage: 'succeeded', + lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), + gap: '500.32', + searchAfterTimeDurations: ['200.00'], + bulkCreateTimeDurations: ['800.43'], + }, + }, + }, + ], + }, + }, + }, + { + key: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', + most_recent_statuses: { + hits: { + hits: [ + { + _source: { + 'siem-detection-engine-rule-status': { + alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', + statusDate: '2020-02-18T15:15:58.806Z', + status: 'failed', + lastFailureAt: '2020-02-18T15:15:58.806Z', + lastSuccessAt: '2020-02-13T20:31:59.855Z', + lastFailureMessage: + 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', + lastSuccessMessage: 'succeeded', + lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), + gap: '500.32', + searchAfterTimeDurations: ['200.00'], + bulkCreateTimeDurations: ['800.43'], + }, + }, + }, + ], + }, + }, + }, + ], + }, + }, +}); + export const getEmptySignalsResponse = (): SignalSearchResponse => ({ took: 1, timed_out: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts index 6af4397a4193a6..3527e43c03d52b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts @@ -6,15 +6,17 @@ */ import { transformError, getIndexExists } from '@kbn/securitysolution-es-utils'; +import { parseExperimentalConfigValue } from '../../../../../common/experimental_features'; +import { ConfigType } from '../../../../config'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; +import { DEFAULT_ALERTS_INDEX, DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { buildSiemResponse } from '../utils'; import { SIGNALS_TEMPLATE_VERSION } from './get_signals_template'; import { getIndexVersion } from './get_index_version'; import { isOutdated } from '../../migrations/helpers'; -export const readIndexRoute = (router: SecuritySolutionPluginRouter) => { +export const readIndexRoute = (router: SecuritySolutionPluginRouter, config: ConfigType) => { router.get( { path: DETECTION_ENGINE_INDEX_URL, @@ -34,8 +36,16 @@ export const readIndexRoute = (router: SecuritySolutionPluginRouter) => { return siemResponse.error({ statusCode: 404 }); } + // TODO: Once we are past experimental phase this code should be removed + const { ruleRegistryEnabled } = parseExperimentalConfigValue(config.enableExperimental); + if (ruleRegistryEnabled) { + return response.ok({ + body: { name: DEFAULT_ALERTS_INDEX, index_mapping_outdated: false }, + }); + } + const index = siemClient.getSignalsIndex(); - const indexExists = await getIndexExists(esClient, index); + const indexExists = ruleRegistryEnabled ? true : await getIndexExists(esClient, index); if (indexExists) { let mappingOutdated: boolean | null = null; 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 9b7e7bb42f4236..993d9300e414f4 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 @@ -6,6 +6,7 @@ */ import { transformError, getIndexExists } from '@kbn/securitysolution-es-utils'; +import { RuleDataClient } from '../../../../../../rule_registry/server'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -24,7 +25,8 @@ import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters' export const createRulesRoute = ( router: SecuritySolutionPluginRouter, - ml: SetupPlugins['ml'] + ml: SetupPlugins['ml'], + ruleDataClient?: RuleDataClient | null ): void => { router.post( { 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 76fb9ac0c77e33..4b05f603b85b7c 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 @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { RuleDataClient } from '../../../../../../rule_registry/server'; import { queryRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/query_rules_type_dependents'; import { queryRulesSchema, @@ -22,7 +23,10 @@ import { deleteNotifications } from '../../notifications/delete_notifications'; import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; -export const deleteRulesRoute = (router: SecuritySolutionPluginRouter) => { +export const deleteRulesRoute = ( + router: SecuritySolutionPluginRouter, + ruleDataClient?: RuleDataClient | null +) => { router.delete( { path: DETECTION_ENGINE_RULES_URL, 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 434ef0f88b1969..06f3ca83c4722a 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 @@ -10,7 +10,7 @@ import { getAlertMock, getFindRequest, getFindResultWithSingleHit, - getFindResultStatus, + getFindBulkResultStatus, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { findRulesRoute } from './find_rules_route'; @@ -27,7 +27,7 @@ describe('find_rules', () => { clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); - clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.find.mockResolvedValue(getFindBulkResultStatus()); findRulesRoute(server.router); }); 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 ccf0a59e87c749..428978fe1d8207 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 @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { RuleDataClient } from '../../../../../../rule_registry/server'; import { findRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/find_rules_type_dependents'; import { findRulesSchema, @@ -15,13 +16,15 @@ import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { findRules } from '../../rules/find_rules'; import { buildSiemResponse } from '../utils'; - -import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { transformFindAlerts } from './utils'; +import { getBulkRuleActionsSavedObject } from '../../rule_actions/get_bulk_rule_actions_saved_object'; -export const findRulesRoute = (router: SecuritySolutionPluginRouter) => { +export const findRulesRoute = ( + router: SecuritySolutionPluginRouter, + ruleDataClient?: RuleDataClient | null +) => { router.get( { path: `${DETECTION_ENGINE_RULES_URL}/_find`, @@ -60,44 +63,11 @@ export const findRulesRoute = (router: SecuritySolutionPluginRouter) => { filter: query.filter, fields: query.fields, }); - - // if any rules attempted to execute but failed before the rule executor is called, - // an execution status will be written directly onto the rule via the kibana alerting framework, - // which we are filtering on and will write a failure status - // for any rules found to be in a failing state into our rule status saved objects - const failingRules = rules.data.filter( - (rule) => rule.executionStatus != null && rule.executionStatus.status === 'error' - ); - - const ruleStatuses = await Promise.all( - rules.data.map(async (rule) => { - const results = await ruleStatusClient.find({ - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], - }); - const failingRule = failingRules.find((badRule) => badRule.id === rule.id); - if (failingRule != null) { - if (results.saved_objects.length > 0) { - results.saved_objects[0].attributes.status = 'failed'; - results.saved_objects[0].attributes.lastFailureAt = failingRule.executionStatus.lastExecutionDate.toISOString(); - } - } - return results; - }) - ); - const ruleActions = await Promise.all( - rules.data.map(async (rule) => { - const results = await getRuleActionsSavedObject({ - savedObjectsClient, - ruleAlertId: rule.id, - }); - - return results; - }) - ); + const alertIds = rules.data.map((rule) => rule.id); + const [ruleStatuses, ruleActions] = await Promise.all([ + ruleStatusClient.findBulk(alertIds, 1), + getBulkRuleActionsSavedObject({ alertIds, savedObjectsClient }), + ]); const transformed = transformFindAlerts(rules, ruleActions, ruleStatuses); if (transformed == null) { return siemResponse.error({ statusCode: 500, body: 'Internal error transforming' }); 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 c3a53a1f393ec5..73f076649b72fe 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 @@ -7,9 +7,9 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { - getFindResultStatus, ruleStatusRequest, getAlertMock, + getFindBulkResultStatus, } from '../__mocks__/request_responses'; import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { findRulesStatusesRoute } from './find_rules_status_route'; @@ -26,7 +26,7 @@ describe('find_statuses', () => { beforeEach(async () => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // successful status search + clients.savedObjectsClient.find.mockResolvedValue(getFindBulkResultStatus()); // successful status search clients.alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); findRulesStatusesRoute(server.router); }); 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 bd6e8fc9e7aadb..aed8b80e4f1332 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 @@ -9,14 +9,13 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { RuleStatusResponse } from '../../rules/types'; import { buildSiemResponse, mergeStatuses, getFailingRules } from '../utils'; - import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { findRulesStatusesSchema, FindRulesStatusesSchemaDecoded, } from '../../../../../common/detection_engine/schemas/request/find_rule_statuses_schema'; +import { mergeAlertWithSidecarStatus } from '../../schemas/rule_converters'; /** * Given a list of rule ids, return the current status and @@ -51,45 +50,27 @@ export const findRulesStatusesRoute = (router: SecuritySolutionPluginRouter) => const ids = body.ids; try { const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); - const failingRules = await getFailingRules(ids, alertsClient); + const [statusesById, failingRules] = await Promise.all([ + ruleStatusClient.findBulk(ids, 6), + getFailingRules(ids, alertsClient), + ]); - const statuses = await ids.reduce(async (acc, id) => { - const accumulated = await acc; - const lastFiveErrorsForId = await ruleStatusClient.find({ - perPage: 6, - sortField: 'statusDate', - sortOrder: 'desc', - search: id, - searchFields: ['alertId'], - }); + const statuses = ids.reduce((acc, id) => { + const lastFiveErrorsForId = statusesById[id]; - if (lastFiveErrorsForId.saved_objects.length === 0) { - return accumulated; + if (lastFiveErrorsForId == null || lastFiveErrorsForId.length === 0) { + return acc; } const failingRule = failingRules[id]; - const lastFailureAt = lastFiveErrorsForId.saved_objects[0].attributes.lastFailureAt; - - if ( - failingRule != null && - (lastFailureAt == null || - new Date(failingRule.executionStatus.lastExecutionDate) > new Date(lastFailureAt)) - ) { - const currentStatus = lastFiveErrorsForId.saved_objects[0]; - currentStatus.attributes.lastFailureMessage = `Reason: ${failingRule.executionStatus.error?.reason} Message: ${failingRule.executionStatus.error?.message}`; - currentStatus.attributes.lastFailureAt = failingRule.executionStatus.lastExecutionDate.toISOString(); - currentStatus.attributes.statusDate = failingRule.executionStatus.lastExecutionDate.toISOString(); - currentStatus.attributes.status = 'failed'; - const updatedLastFiveErrorsSO = [ - currentStatus, - ...lastFiveErrorsForId.saved_objects.slice(1), - ]; - return mergeStatuses(id, updatedLastFiveErrorsSO, accumulated); + if (failingRule != null) { + const currentStatus = mergeAlertWithSidecarStatus(failingRule, lastFiveErrorsForId[0]); + const updatedLastFiveErrorsSO = [currentStatus, ...lastFiveErrorsForId.slice(1)]; + return mergeStatuses(id, updatedLastFiveErrorsSO, acc); } - return mergeStatuses(id, [...lastFiveErrorsForId.saved_objects], accumulated); - }, Promise.resolve({})); - + return mergeStatuses(id, [...lastFiveErrorsForId], acc); + }, {}); return response.ok({ body: statuses }); } catch (err) { const error = transformError(err); 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 780c248183ab96..eaaa44fcf19160 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 @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { RuleDataClient } from '../../../../../../rule_registry/server'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { patchRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/patch_rules_type_dependents'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; @@ -28,7 +29,11 @@ import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_s import { readRules } from '../../rules/read_rules'; import { PartialFilter } from '../../types'; -export const patchRulesRoute = (router: SecuritySolutionPluginRouter, ml: SetupPlugins['ml']) => { +export const patchRulesRoute = ( + router: SecuritySolutionPluginRouter, + ml: SetupPlugins['ml'], + ruleDataClient?: RuleDataClient | null +) => { router.patch( { 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 ac45e5d2ed3b20..917da6c9708d54 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 @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { RuleDataClient } from '../../../../../../rule_registry/server'; import { queryRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/query_rules_type_dependents'; import { queryRulesSchema, @@ -21,7 +22,10 @@ import { readRules } from '../../rules/read_rules'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; -export const readRulesRoute = (router: SecuritySolutionPluginRouter) => { +export const readRulesRoute = ( + router: SecuritySolutionPluginRouter, + ruleDataClient?: RuleDataClient | null +) => { router.get( { path: DETECTION_ENGINE_RULES_URL, 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 aad0068758f7dc..0ff6cb3cd2d0f0 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 @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { RuleDataClient } from '../../../../../../rule_registry/server'; import { updateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents'; import type { SecuritySolutionPluginRouter } from '../../../../types'; @@ -22,7 +23,11 @@ import { updateRulesNotifications } from '../../rules/update_rules_notifications import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -export const updateRulesRoute = (router: SecuritySolutionPluginRouter, ml: SetupPlugins['ml']) => { +export const updateRulesRoute = ( + router: SecuritySolutionPluginRouter, + ml: SetupPlugins['ml'], + ruleDataClient?: RuleDataClient | null +) => { router.put( { path: DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index f2788ab1bd4c99..29e322d7fcab5f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -27,7 +27,6 @@ import { PartialFilter } from '../../types'; import { BulkError, ImportSuccessError } from '../utils'; import { getOutputRuleAlertForRest } from '../__mocks__/utils'; import { PartialAlert } from '../../../../../../alerting/server'; -import { SanitizedAlert } from '../../../../../../alerting/server/types'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { RuleAlertType } from '../../rules/types'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; @@ -256,7 +255,7 @@ describe('utils', () => { describe('transformFindAlerts', () => { test('outputs empty data set when data set is empty correct', () => { - const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 }, []); + const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 }, {}, {}); expect(output).toEqual({ data: [], page: 1, perPage: 0, total: 0 }); }); @@ -268,7 +267,8 @@ describe('utils', () => { total: 0, data: [getAlertMock(getQueryRuleParams())], }, - [] + {}, + {} ); const expected = getOutputRuleAlertForRest(); expect(output).toEqual({ @@ -278,20 +278,6 @@ describe('utils', () => { data: [expected], }); }); - - test('returns 500 if the data is not of type siem alert', () => { - const unsafeCast = ([{ name: 'something else' }] as unknown) as SanitizedAlert[]; - const output = transformFindAlerts( - { - data: unsafeCast, - page: 1, - perPage: 1, - total: 1, - }, - [] - ); - expect(output).toBeNull(); - }); }); describe('transform', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 466b8dd1842276..dc0cd2e497215f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -6,7 +6,7 @@ */ import { countBy } from 'lodash/fp'; -import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; +import { SavedObject } from 'kibana/server'; import uuid from 'uuid'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; @@ -17,11 +17,10 @@ import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { RuleAlertType, isAlertType, - isAlertTypes, IRuleSavedAttributesSavedObjectAttributes, isRuleStatusFindType, - isRuleStatusFindTypes, isRuleStatusSavedObjectType, + IRuleStatusSOAttributes, } from '../../rules/types'; import { createBulkErrorObject, @@ -34,6 +33,7 @@ import { import { RuleActions } from '../../rule_actions/types'; import { internalRuleToAPIResponse } from '../../schemas/rule_converters'; import { RuleParams } from '../../schemas/rule_schemas'; +import { SanitizedAlert } from '../../../../../../alerting/common'; type PromiseFromStreams = ImportRulesSchemaDecoded | Error; @@ -103,11 +103,11 @@ export const transformTags = (tags: string[]): string[] => { // Transforms the data but will remove any null or undefined it encounters and not include // those on the export export const transformAlertToRule = ( - alert: RuleAlertType, + alert: SanitizedAlert, ruleActions?: RuleActions | null, ruleStatus?: SavedObject ): Partial => { - return internalRuleToAPIResponse(alert, ruleActions, ruleStatus); + return internalRuleToAPIResponse(alert, ruleActions, ruleStatus?.attributes); }; export const transformAlertsToRules = (alerts: RuleAlertType[]): Array> => { @@ -116,33 +116,24 @@ export const transformAlertsToRules = (alerts: RuleAlertType[]): Array, - ruleActions: Array, - ruleStatuses?: Array> + ruleActions: { [key: string]: RuleActions | undefined }, + ruleStatuses: { [key: string]: IRuleStatusSOAttributes[] | undefined } ): { page: number; perPage: number; total: number; data: Array>; } | null => { - if (!ruleStatuses && isAlertTypes(findResults.data)) { - return { - page: findResults.page, - perPage: findResults.perPage, - total: findResults.total, - data: findResults.data.map((alert, idx) => transformAlertToRule(alert, ruleActions[idx])), - }; - } else if (isAlertTypes(findResults.data) && isRuleStatusFindTypes(ruleStatuses)) { - return { - page: findResults.page, - perPage: findResults.perPage, - total: findResults.total, - data: findResults.data.map((alert, idx) => - transformAlertToRule(alert, ruleActions[idx], ruleStatuses[idx].saved_objects[0]) - ), - }; - } else { - return null; - } + return { + page: findResults.page, + perPage: findResults.perPage, + total: findResults.total, + data: findResults.data.map((alert) => { + const statuses = ruleStatuses[alert.id]; + const status = statuses ? statuses[0] : undefined; + return internalRuleToAPIResponse(alert, ruleActions[alert.id], status); + }), + }; }; export const transform = ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.test.ts index 909c94f145528c..d6b998e3142349 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -14,7 +14,7 @@ import { getSignalsAggsAndQueryRequest, getEmptySignalsResponse, } from '../__mocks__/request_responses'; -import { requestContextMock, serverMock, requestMock } from '../__mocks__'; +import { requestContextMock, serverMock, requestMock, createMockConfig } from '../__mocks__'; import { querySignalsRoute } from './query_signals_route'; describe('query for signal', () => { @@ -27,7 +27,7 @@ describe('query for signal', () => { clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptySignalsResponse()); - querySignalsRoute(server.router); + querySignalsRoute(server.router, createMockConfig()); }); describe('query and agg on signals index', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts index 91172a277bf54a..770c1a5da344f6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -6,8 +6,13 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { parseExperimentalConfigValue } from '../../../../../common/experimental_features'; +import { ConfigType } from '../../../../config'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; +import { + DEFAULT_ALERTS_INDEX, + DETECTION_ENGINE_QUERY_SIGNALS_URL, +} from '../../../../../common/constants'; import { buildSiemResponse } from '../utils'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; @@ -16,7 +21,7 @@ import { QuerySignalsSchemaDecoded, } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; -export const querySignalsRoute = (router: SecuritySolutionPluginRouter) => { +export const querySignalsRoute = (router: SecuritySolutionPluginRouter, config: ConfigType) => { router.post( { path: DETECTION_ENGINE_QUERY_SIGNALS_URL, @@ -48,9 +53,12 @@ export const querySignalsRoute = (router: SecuritySolutionPluginRouter) => { const clusterClient = context.core.elasticsearch.legacy.client; const siemClient = context.securitySolution!.getAppClient(); + // TODO: Once we are past experimental phase this code should be removed + const { ruleRegistryEnabled } = parseExperimentalConfigValue(config.enableExperimental); + try { const result = await clusterClient.callAsCurrentUser('search', { - index: siemClient.getSignalsIndex(), + index: ruleRegistryEnabled ? DEFAULT_ALERTS_INDEX : siemClient.getSignalsIndex(), body: { query, aggs, _source, track_total_hits, size }, ignoreUnavailable: true, }); 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 a09c4cd257618d..ce7d4b31733700 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 @@ -25,7 +25,7 @@ import { getFailingRules, } from './utils'; import { responseMock } from './__mocks__'; -import { exampleRuleStatus, exampleFindRuleStatusResponse } from '../signals/__mocks__/es_results'; +import { exampleRuleStatus } from '../signals/__mocks__/es_results'; import { getAlertMock } from './__mocks__/request_responses'; import { AlertExecutionStatusErrorReasons } from '../../../../../alerting/common'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; @@ -301,8 +301,8 @@ describe('utils', () => { const statusTwo = exampleRuleStatus(); statusTwo.attributes.status = 'failed'; const currentStatus = exampleRuleStatus(); - const foundRules = exampleFindRuleStatusResponse([currentStatus, statusOne, statusTwo]); - const res = mergeStatuses(currentStatus.attributes.alertId, foundRules.saved_objects, { + const foundRules = [currentStatus.attributes, statusOne.attributes, statusTwo.attributes]; + const res = mergeStatuses(currentStatus.attributes.alertId, foundRules, { 'myfakealertid-8cfac': { current_status: { alert_id: 'myfakealertid-8cfac', 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 130084da21591b..9ff75726322a17 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 @@ -14,11 +14,12 @@ import { RouteValidationFunction, KibanaResponseFactory, CustomHttpResponseOptions, - SavedObjectsFindResult, } from '../../../../../../../src/core/server'; import { AlertsClient } from '../../../../../alerting/server'; import { RuleStatusResponse, IRuleStatusSOAttributes } from '../rules/types'; +import { RuleParams } from '../schemas/rule_schemas'; + export interface OutputError { message: string; statusCode: number; @@ -277,7 +278,7 @@ export const convertToSnakeCase = >( */ export const mergeStatuses = ( id: string, - currentStatusAndFailures: Array>, + currentStatusAndFailures: IRuleStatusSOAttributes[], acc: RuleStatusResponse ): RuleStatusResponse => { if (currentStatusAndFailures.length === 0) { @@ -286,7 +287,7 @@ export const mergeStatuses = ( }; } const convertedCurrentStatus = convertToSnakeCase( - currentStatusAndFailures[0].attributes + currentStatusAndFailures[0] ); return { ...acc, @@ -294,12 +295,12 @@ export const mergeStatuses = ( current_status: convertedCurrentStatus, failures: currentStatusAndFailures .slice(1) - .map((errorItem) => convertToSnakeCase(errorItem.attributes)), + .map((errorItem) => convertToSnakeCase(errorItem)), }, } as RuleStatusResponse; }; -export type GetFailingRulesResult = Record; +export type GetFailingRulesResult = Record>; export const getFailingRules = async ( ids: string[], @@ -316,13 +317,11 @@ export const getFailingRules = async ( return errorRules .filter((rule) => rule.executionStatus.status === 'error') .reduce((acc, failingRule) => { - const accum = acc; - const theRule = failingRule; return { - [theRule.id]: { - ...theRule, + [failingRule.id]: { + ...failingRule, }, - ...accum, + ...acc, }; }, {}); } catch (exc) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/get_bulk_rule_actions_saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/get_bulk_rule_actions_saved_object.ts new file mode 100644 index 00000000000000..1abb16ba4612c1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/get_bulk_rule_actions_saved_object.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertServices } from '../../../../../alerting/server'; +import { ruleActionsSavedObjectType } from './saved_object_mappings'; +import { IRuleActionsAttributesSavedObjectAttributes } from './types'; +import { getRuleActionsFromSavedObject } from './utils'; +import { RulesActionsSavedObject } from './get_rule_actions_saved_object'; +import { buildChunkedOrFilter } from '../signals/utils'; + +interface GetBulkRuleActionsSavedObject { + alertIds: string[]; + savedObjectsClient: AlertServices['savedObjectsClient']; +} + +export const getBulkRuleActionsSavedObject = async ({ + alertIds, + savedObjectsClient, +}: GetBulkRuleActionsSavedObject): Promise> => { + const filter = buildChunkedOrFilter( + `${ruleActionsSavedObjectType}.attributes.ruleAlertId`, + alertIds + ); + const { + // eslint-disable-next-line @typescript-eslint/naming-convention + saved_objects, + } = await savedObjectsClient.find({ + type: ruleActionsSavedObjectType, + perPage: 10000, + filter, + }); + return saved_objects.reduce((acc: { [key: string]: RulesActionsSavedObject }, savedObject) => { + acc[savedObject.attributes.ruleAlertId] = getRuleActionsFromSavedObject(savedObject); + return acc; + }, {}); +}; 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 380eb085e0d5a8..601f3ebaa0f9e6 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 @@ -211,12 +211,6 @@ export const isRuleStatusFindType = ( return get('saved_objects', obj) != null; }; -export const isRuleStatusFindTypes = ( - obj: unknown[] | undefined -): obj is Array> => { - return obj ? obj.every((ruleStatus) => isRuleStatusFindType(ruleStatus)) : false; -}; - export interface CreateRulesOptions { alertsClient: AlertsClient; anomalyThreshold: AnomalyThresholdOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts index ee7ecaadfd95c0..a215da021d15ad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -6,7 +6,6 @@ */ import uuid from 'uuid'; -import { SavedObject } from 'kibana/server'; import { normalizeMachineLearningJobIds, normalizeThresholdObject, @@ -29,8 +28,8 @@ import { AppClient } from '../../../types'; import { addTags } from '../rules/add_tags'; import { DEFAULT_MAX_SIGNALS, SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { Alert } from '../../../../../alerting/common'; -import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; +import { SanitizedAlert } from '../../../../../alerting/common'; +import { IRuleStatusSOAttributes } from '../rules/types'; import { transformTags } from '../routes/rules/utils'; // These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema @@ -270,10 +269,11 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => { }; export const internalRuleToAPIResponse = ( - rule: Alert, + rule: SanitizedAlert, ruleActions?: RuleActions | null, - ruleStatus?: SavedObject + ruleStatus?: IRuleStatusSOAttributes ): FullResponseSchema => { + const mergedStatus = ruleStatus ? mergeAlertWithSidecarStatus(rule, ruleStatus) : undefined; return { // Alerting framework params id: rule.id, @@ -293,11 +293,30 @@ export const internalRuleToAPIResponse = ( throttle: ruleActions?.ruleThrottle || 'no_actions', actions: ruleActions?.actions ?? [], // Rule status - status: ruleStatus?.attributes.status ?? undefined, - status_date: ruleStatus?.attributes.statusDate ?? undefined, - last_failure_at: ruleStatus?.attributes.lastFailureAt ?? undefined, - last_success_at: ruleStatus?.attributes.lastSuccessAt ?? undefined, - last_failure_message: ruleStatus?.attributes.lastFailureMessage ?? undefined, - last_success_message: ruleStatus?.attributes.lastSuccessMessage ?? undefined, + status: mergedStatus?.status ?? undefined, + status_date: mergedStatus?.statusDate ?? undefined, + last_failure_at: mergedStatus?.lastFailureAt ?? undefined, + last_success_at: mergedStatus?.lastSuccessAt ?? undefined, + last_failure_message: mergedStatus?.lastFailureMessage ?? undefined, + last_success_message: mergedStatus?.lastSuccessMessage ?? undefined, }; }; + +export const mergeAlertWithSidecarStatus = ( + alert: SanitizedAlert, + status: IRuleStatusSOAttributes +): IRuleStatusSOAttributes => { + if ( + new Date(alert.executionStatus.lastExecutionDate) > new Date(status.statusDate) && + alert.executionStatus.status === 'error' + ) { + return { + ...status, + lastFailureMessage: `Reason: ${alert.executionStatus.error?.reason} Message: ${alert.executionStatus.error?.message}`, + lastFailureAt: alert.executionStatus.lastExecutionDate.toISOString(), + statusDate: alert.executionStatus.lastExecutionDate.toISOString(), + status: 'failed', + }; + } + return status; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts index b760cec9226db0..3dd328a9499386 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts @@ -9,6 +9,7 @@ import { RuleStatusSavedObjectsClient } from '../rule_status_saved_objects_clien const createMockRuleStatusSavedObjectsClient = (): jest.Mocked => ({ find: jest.fn(), + findBulk: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts index ff1b0e27019edb..25d315279ad60f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { get } from 'lodash'; import { SavedObjectsClientContract, SavedObject, @@ -14,11 +15,13 @@ import { } from '../../../../../../../src/core/server'; import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; import { IRuleStatusSOAttributes } from '../rules/types'; +import { buildChunkedOrFilter } from './utils'; export interface RuleStatusSavedObjectsClient { find: ( options?: Omit ) => Promise>; + findBulk: (ids: string[], statusesPerId: number) => Promise; create: (attributes: IRuleStatusSOAttributes) => Promise>; update: ( id: string, @@ -27,6 +30,10 @@ export interface RuleStatusSavedObjectsClient { delete: (id: string) => Promise<{}>; } +interface FindBulkResponse { + [key: string]: IRuleStatusSOAttributes[] | undefined; +} + export const ruleStatusSavedObjectsClientFactory = ( savedObjectsClient: SavedObjectsClientContract ): RuleStatusSavedObjectsClient => ({ @@ -35,6 +42,50 @@ export const ruleStatusSavedObjectsClientFactory = ( ...options, type: ruleStatusSavedObjectType, }), + findBulk: async (ids, statusesPerId) => { + if (ids.length === 0) { + return {}; + } + const filter = buildChunkedOrFilter(`${ruleStatusSavedObjectType}.attributes.alertId`, ids); + const order: 'desc' = 'desc'; + const aggs = { + alertIds: { + terms: { + field: `${ruleStatusSavedObjectType}.attributes.alertId`, + size: ids.length, + }, + aggs: { + most_recent_statuses: { + top_hits: { + sort: [ + { + [`${ruleStatusSavedObjectType}.statusDate`]: { + order, + }, + }, + ], + size: statusesPerId, + }, + }, + }, + }, + }; + const results = await savedObjectsClient.find({ + filter, + aggs, + type: ruleStatusSavedObjectType, + perPage: 0, + }); + const buckets = get(results, 'aggregations.alertIds.buckets'); + return buckets.reduce((acc: Record, bucket: unknown) => { + const key = get(bucket, 'key'); + const hits = get(bucket, 'most_recent_statuses.hits.hits'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const statuses = hits.map((hit: any) => hit._source['siem-detection-engine-rule-status']); + acc[key] = statuses; + return acc; + }, {}); + }, create: (attributes) => savedObjectsClient.create(ruleStatusSavedObjectType, attributes), update: (id, attributes) => savedObjectsClient.update(ruleStatusSavedObjectType, id, attributes), delete: (id) => savedObjectsClient.delete(ruleStatusSavedObjectType, id), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts index 986393d6d34546..ca7f22e4a75707 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts @@ -138,7 +138,7 @@ export const findThresholdSignals = async ({ logger, // @ts-expect-error refactor to pass type explicitly instead of unknown filter, - pageSize: 1, + pageSize: 0, sortOrder: 'desc', buildRuleMessage, }); 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 b04eab1496e960..f49492939eeb10 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 @@ -39,6 +39,7 @@ import { createTotalHitsFromSearchResult, lastValidDate, calculateThresholdSignalUuid, + buildChunkedOrFilter, } from './utils'; import { BulkResponseErrorAggregation, SearchAfterAndBulkCreateReturnType } from './types'; import { @@ -1473,4 +1474,26 @@ describe('utils', () => { expect(signalUuid).toEqual('ee8870dc-45ff-5e6c-a2f9-80886651ce03'); }); }); + + describe('buildChunkedOrFilter', () => { + test('should return undefined if no values are provided', () => { + const filter = buildChunkedOrFilter('field.name', []); + expect(filter).toEqual(undefined); + }); + + test('should return a filter with a single value', () => { + const filter = buildChunkedOrFilter('field.name', ['id-1']); + expect(filter).toEqual('field.name: ("id-1")'); + }); + + test('should return a filter with a multiple values', () => { + const filter = buildChunkedOrFilter('field.name', ['id-1', 'id-2']); + expect(filter).toEqual('field.name: ("id-1" OR "id-2")'); + }); + + test('should return a filter with a multiple values chunked', () => { + const filter = buildChunkedOrFilter('field.name', ['id-1', 'id-2', 'id-3'], 2); + expect(filter).toEqual('field.name: ("id-1" OR "id-2") OR field.name: ("id-3")'); + }); + }); }); 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 03a067af6066d4..cc4ed6a45807b1 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 @@ -10,7 +10,7 @@ import moment from 'moment'; import uuidv5 from 'uuid/v5'; import dateMath from '@elastic/datemath'; import type { estypes } from '@elastic/elasticsearch'; -import { isEmpty, partition } from 'lodash'; +import { chunk, isEmpty, partition } from 'lodash'; import { ApiResponse, Context } from '@elastic/elasticsearch/lib/Transport'; import { SortResults } from '@elastic/elasticsearch/api/types'; @@ -868,3 +868,16 @@ export const getSafeSortIds = (sortIds: SortResults | undefined) => { return sortId; }); }; + +export const buildChunkedOrFilter = (field: string, values: string[], chunkSize: number = 1024) => { + if (values.length === 0) { + return undefined; + } + const chunkedValues = chunk(values, chunkSize); + return chunkedValues + .map((subArray) => { + const joinedValues = subArray.map((value) => `"${value}"`).join(' OR '); + return `${field}: (${joinedValues})`; + }) + .join(' OR '); +}; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index aa37a0dc1f6270..2507475592e888 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { once } from 'lodash'; import { Observable } from 'rxjs'; import { i18n } from '@kbn/i18n'; import LRU from 'lru-cache'; @@ -27,8 +28,18 @@ import { PluginSetupContract as AlertingSetup, PluginStartContract as AlertPluginStartContract, } from '../../alerting/server'; + import { PluginStartContract as CasesPluginStartContract } from '../../cases/server'; +import { + ECS_COMPONENT_TEMPLATE_NAME, + TECHNICAL_COMPONENT_TEMPLATE_NAME, +} from '../../rule_registry/common/assets'; import { SecurityPluginSetup as SecuritySetup, SecurityPluginStart } from '../../security/server'; +import { + RuleDataClient, + RuleRegistryPluginSetupContract, + RuleRegistryPluginStartContract, +} from '../../rule_registry/server'; import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { MlPluginSetup as MlSetup } from '../../ml/server'; import { ListPluginSetup } from '../../lists/server'; @@ -38,6 +49,9 @@ import { ILicense, LicensingPluginStart } from '../../licensing/server'; import { FleetStartContract } from '../../fleet/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { compose } from './lib/compose/kibana'; +import { createQueryAlertType } from './lib/detection_engine/reference_rules/query'; +import { createEqlAlertType } from './lib/detection_engine/reference_rules/eql'; +import { createThresholdAlertType } from './lib/detection_engine/reference_rules/threshold'; import { initRoutes } from './routes'; import { isAlertExecutor } from './lib/detection_engine/signals/types'; import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; @@ -54,6 +68,8 @@ import { SecurityPageName, SIGNALS_ID, NOTIFICATIONS_ID, + REFERENCE_RULE_ALERT_TYPE_ID, + REFERENCE_RULE_PERSISTENCE_ALERT_TYPE_ID, } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency'; @@ -87,6 +103,7 @@ export interface SetupPlugins { features: FeaturesSetup; lists?: ListPluginSetup; ml?: MlSetup; + ruleRegistry: RuleRegistryPluginSetupContract; security?: SecuritySetup; spaces?: SpacesSetup; taskManager?: TaskManagerSetupContract; @@ -99,6 +116,7 @@ export interface StartPlugins { data: DataPluginStart; fleet?: FleetStartContract; licensing: LicensingPluginStart; + ruleRegistry: RuleRegistryPluginStartContract; taskManager?: TaskManagerStartContract; telemetry?: TelemetryPluginStart; security: SecurityPluginStart; @@ -135,6 +153,7 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { this.logger.debug('plugin setup'); + this.setupPlugins = plugins; const config = this.config; const globalConfig = this.context.config.legacy.get(); @@ -195,13 +215,75 @@ export class Plugin implements IPlugin core.getStartServices().then(([coreStart]) => coreStart); + + const ready = once(async () => { + const componentTemplateName = ruleDataService.getFullAssetName( + 'security-solution-mappings' + ); + + if (!ruleDataService.isWriteEnabled()) { + return; + } + + await ruleDataService.createOrUpdateComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + mappings: {}, // TODO: Add mappings here via `mappingFromFieldMap()` + }, + }, + }); + + await ruleDataService.createOrUpdateIndexTemplate({ + name: ruleDataService.getFullAssetName('security-solution-index-template'), + body: { + index_patterns: [ruleDataService.getFullAssetName('security-solution*')], + composed_of: [ + ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME), + ruleDataService.getFullAssetName(ECS_COMPONENT_TEMPLATE_NAME), + componentTemplateName, + ], + }, + }); + }); + + ready().catch((err) => { + this.logger!.error(err); + }); + + ruleDataClient = new RuleDataClient({ + alias: plugins.ruleRegistry.ruleDataService.getFullAssetName('security-solution'), + getClusterClient: async () => { + const coreStart = await start(); + return coreStart.elasticsearch.client.asInternalUser; + }, + ready, + }); + + // Register reference rule types via rule-registry + this.setupPlugins.alerting.registerType(createQueryAlertType(ruleDataClient, this.logger)); + this.setupPlugins.alerting.registerType(createEqlAlertType(ruleDataClient, this.logger)); + this.setupPlugins.alerting.registerType( + createThresholdAlertType(ruleDataClient, this.logger) + ); + } + // TO DO We need to get the endpoint routes inside of initRoutes initRoutes( router, config, plugins.encryptedSavedObjects?.canEncrypt === true, plugins.security, - plugins.ml + plugins.ml, + ruleDataClient ); registerEndpointRoutes(router, endpointContext); registerLimitedConcurrencyRoutes(core); @@ -210,6 +292,16 @@ export class Plugin implements IPlugin { // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules // All REST rule creation, deletion, updating, etc...... - createRulesRoute(router, ml); - readRulesRoute(router); - updateRulesRoute(router, ml); - patchRulesRoute(router, ml); - deleteRulesRoute(router); - findRulesRoute(router); + createRulesRoute(router, ml, ruleDataClient); + readRulesRoute(router, ruleDataClient); + updateRulesRoute(router, ml, ruleDataClient); + patchRulesRoute(router, ml, ruleDataClient); + deleteRulesRoute(router, ruleDataClient); + findRulesRoute(router, ruleDataClient); + + // TODO: pass ruleDataClient to all relevant routes addPrepackedRulesRoute(router, config, security); getPrepackagedRulesStatusRoute(router, config, security); @@ -102,7 +107,7 @@ export const initRoutes = ( // POST /api/detection_engine/signals/status // Example usage can be found in security_solution/server/lib/detection_engine/scripts/signals setSignalsStatusRoute(router); - querySignalsRoute(router); + querySignalsRoute(router, config); getSignalsMigrationStatusRoute(router); createSignalsMigrationRoute(router, security); finalizeSignalsMigrationRoute(router, security); @@ -111,7 +116,7 @@ export const initRoutes = ( // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index // All REST index creation, policy management for spaces createIndexRoute(router); - readIndexRoute(router); + readIndexRoute(router, config); deleteIndexRoute(router); // Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx index e43db6b86f8b90..f489fd0c16455b 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx @@ -33,6 +33,7 @@ const mockDeps = { trustedAppsByPolicyEnabled: false, metricsEntitiesEnabled: false, hostIsolationEnabled: false, + ruleRegistryEnabled: false, }, service: {} as EndpointAppContextService, } as EndpointAppContext, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d829c8eb22a98b..56548ea7602a73 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -125,133 +125,6 @@ "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", "advancedSettings.searchBarAriaLabel": "高度な設定を検索", "advancedSettings.voiceAnnouncement.ariaLabel": "詳細設定結果情報", - "apmOss.tutorial.apmAgents.statusCheck.btnLabel": "エージェントステータスを確認", - "apmOss.tutorial.apmAgents.statusCheck.errorMessage": "エージェントからまだデータを受け取っていません", - "apmOss.tutorial.apmAgents.statusCheck.successMessage": "1 つまたは複数のエージェントからデータを受け取りました", - "apmOss.tutorial.apmAgents.statusCheck.text": "アプリケーションが実行されていてエージェントがデータを送信していることを確認してください。", - "apmOss.tutorial.apmAgents.statusCheck.title": "エージェントステータス", - "apmOss.tutorial.apmAgents.title": "APM エージェント", - "apmOss.tutorial.apmServer.callOut.message": "ご使用の APM Server を 7.0 以上に更新してあることを確認してください。 Kibana の管理セクションにある移行アシスタントで 6.x データを移行することもできます。", - "apmOss.tutorial.apmServer.callOut.title": "重要:7.0 以上に更新中", - "apmOss.tutorial.apmServer.statusCheck.btnLabel": "APM Server ステータスを確認", - "apmOss.tutorial.apmServer.statusCheck.errorMessage": "APM Server が検出されました。7.0 以上に更新され、動作中であることを確認してください。", - "apmOss.tutorial.apmServer.statusCheck.successMessage": "APM Server が正しくセットアップされました", - "apmOss.tutorial.apmServer.statusCheck.text": "APM エージェントの導入を開始する前に、APM Server が動作していることを確認してください。", - "apmOss.tutorial.apmServer.statusCheck.title": "APM Server ステータス", - "apmOss.tutorial.apmServer.title": "APM Server", - "apmOss.tutorial.djangoClient.configure.commands.addAgentComment": "インストールされたアプリにエージェントを追加します", - "apmOss.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment": "パフォーマンスメトリックを送信するには、追跡ミドルウェアを追加します。", - "apmOss.tutorial.djangoClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース", - "apmOss.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト:{defaultApmServerUrl}) を設定します", - "apmOss.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment": "任意のサービス名を設定します。使用できる文字:", - "apmOss.tutorial.djangoClient.configure.commands.setServiceEnvironmentComment": "サービス環境を設定します", - "apmOss.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server でシークレットトークンが必要な場合に使います", - "apmOss.tutorial.djangoClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション] ({documentationLink}) をご覧ください。", - "apmOss.tutorial.djangoClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。", - "apmOss.tutorial.djangoClient.configure.title": "エージェントの構成", - "apmOss.tutorial.djangoClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。", - "apmOss.tutorial.djangoClient.install.title": "APM エージェントのインストール", - "apmOss.tutorial.dotNetClient.configureAgent.textPost": "エージェントに「IConfiguration」インスタンスが渡されていない場合、 (例:非 ASP.NET Core アプリケーションの場合) 、エージェントを環境変数で構成することもできます。\n 高度な用途に関しては [ドキュメンテーション] ({documentationLink}) をご覧ください。", - "apmOss.tutorial.dotNetClient.configureAgent.title": "appsettings.json ファイルの例:", - "apmOss.tutorial.dotNetClient.configureApplication.textPost": "「IConfiguration」インスタンスを渡すのは任意であり、これにより、エージェントはこの「IConfiguration」インスタンス (例:「appsettings.json」ファイル) から構成を読み込みます。", - "apmOss.tutorial.dotNetClient.configureApplication.textPre": "「Elastic.Apm.NetCoreAll」パッケージの ASP.NET Core の場合、「Startup.cs」ファイル内の「Configure」メソドの「UseElasticApm」メソドを呼び出します。", - "apmOss.tutorial.dotNetClient.configureApplication.title": "エージェントをアプリケーションに追加", - "apmOss.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}) パッケージを使用してください。", - "apmOss.tutorial.dotNetClient.download.title": "APM エージェントのダウンロード", - "apmOss.tutorial.downloadServer.title": "APM Server をダウンロードして展開します", - "apmOss.tutorial.downloadServerRpm": "32 ビットパッケージをお探しですか?[ダウンロードページ] ({downloadPageLink}) をご覧ください。", - "apmOss.tutorial.downloadServerTitle": "32 ビットパッケージをお探しですか?[ダウンロードページ] ({downloadPageLink}) をご覧ください。", - "apmOss.tutorial.editConfig.textPre": "Elastic Stack の X-Pack セキュアバージョンをご使用の場合、「apm-server.yml」構成ファイルで認証情報を指定する必要があります。", - "apmOss.tutorial.editConfig.title": "構成を編集する", - "apmOss.tutorial.flaskClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース", - "apmOss.tutorial.flaskClient.configure.commands.configureElasticApmComment": "またはアプリケーションの設定で ELASTIC_APM を使用するよう構成します。", - "apmOss.tutorial.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します", - "apmOss.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト:{defaultApmServerUrl}) を設定します", - "apmOss.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment": "任意のサービス名を設定します。使用できる文字:", - "apmOss.tutorial.flaskClient.configure.commands.setServiceEnvironmentComment": "サービス環境を設定します", - "apmOss.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server でシークレットトークンが必要な場合に使います", - "apmOss.tutorial.flaskClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション] ({documentationLink}) をご覧ください。", - "apmOss.tutorial.flaskClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。", - "apmOss.tutorial.flaskClient.configure.title": "エージェントの構成", - "apmOss.tutorial.flaskClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。", - "apmOss.tutorial.flaskClient.install.title": "APM エージェントのインストール", - "apmOss.tutorial.goClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します:", - "apmOss.tutorial.goClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト:{defaultApmServerUrl}) を設定します", - "apmOss.tutorial.goClient.configure.commands.setServiceEnvironment": "サービス環境を設定します", - "apmOss.tutorial.goClient.configure.commands.setServiceNameComment": "サービス名を設定します。使用できる文字は # a-z、A-Z、0-9、-、_、スペースです。", - "apmOss.tutorial.goClient.configure.commands.usedExecutableNameComment": "ELASTIC_APM_SERVICE_NAME が指定されていない場合、実行ファイルの名前が使用されます。", - "apmOss.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment": "APM Server でシークレットトークンが必要な場合に使います", - "apmOss.tutorial.goClient.configure.textPost": "高度な構成に関しては [ドキュメンテーション] ({documentationLink}) をご覧ください。", - "apmOss.tutorial.goClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは実行ファイル名または「ELASTIC_APM_SERVICE_NAME」環境変数に基づいてプログラムで作成されます。", - "apmOss.tutorial.goClient.configure.title": "エージェントの構成", - "apmOss.tutorial.goClient.install.textPre": "Go の APM エージェントパッケージをインストールします。", - "apmOss.tutorial.goClient.install.title": "APM エージェントのインストール", - "apmOss.tutorial.goClient.instrument.textPost": "Go のソースコードのインストルメンテーションの詳細ガイドは、[ドキュメンテーション] ({documentationLink}) をご覧ください。", - "apmOss.tutorial.goClient.instrument.textPre": "提供されたインストルメンテーションモジュールの 1 つ、またはトレーサー API を直接使用して、Go アプリケーションにインストルメンテーションを設定します。", - "apmOss.tutorial.goClient.instrument.title": "アプリケーションのインストルメンテーション", - "apmOss.tutorial.introduction": "アプリケーション内から詳細なパフォーマンスメトリックやエラーを収集します。", - "apmOss.tutorial.javaClient.download.textPre": "[Maven Central] ({mavenCentralLink}) からエージェントをダウンロードします。アプリケーションにエージェントを依存関係として「追加しない」でください。", - "apmOss.tutorial.javaClient.download.title": "APM エージェントのダウンロード", - "apmOss.tutorial.javaClient.startApplication.textPost": "構成オプションと高度な用途に関しては、[ドキュメンテーション] ({documentationLink}) をご覧ください。", - "apmOss.tutorial.javaClient.startApplication.textPre": "「-javaagent」フラグを追加し、システムプロパティを使用してエージェントを構成します。\n\n * 任意のサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです) \n * カスタム APM Server URL (デフォルト:{customApmServerUrl}) を設定します\n * APM Server シークレットトークンを設定します\n * サービス環境を設定します\n * アプリケーションのベースパッケージを設定します", - "apmOss.tutorial.javaClient.startApplication.title": "javaagent フラグでアプリケーションを起動", - "apmOss.tutorial.jsClient.enableRealUserMonitoring.textPre": "デフォルトでは、APM Server を実行すると RUM サポートは無効になります。RUM サポートを有効にする手順については、[ドキュメンテーション] ({documentationLink}) をご覧ください。", - "apmOss.tutorial.jsClient.enableRealUserMonitoring.title": "APM Server のリアルユーザー監視サポートを有効にする", - "apmOss.tutorial.jsClient.installDependency.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト:{defaultApmServerUrl}) を設定します", - "apmOss.tutorial.jsClient.installDependency.commands.setRequiredServiceNameComment": "任意のサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです) ", - "apmOss.tutorial.jsClient.installDependency.commands.setServiceEnvironmentComment": "サービス環境を設定します", - "apmOss.tutorial.jsClient.installDependency.commands.setServiceVersionComment": "サービスバージョンを設定します (ソースマップ機能に必要) ", - "apmOss.tutorial.jsClient.installDependency.textPost": "React や Angular などのフレームワーク統合には、カスタム依存関係があります。詳細は [統合ドキュメント] ({docLink}) をご覧ください。", - "apmOss.tutorial.jsClient.installDependency.textPre": "「npm install @elastic/apm-rum --save」でエージェントをアプリケーションへの依存関係としてインストールできます。\n\nその後で以下のようにアプリケーションでエージェントを初期化して構成できます。", - "apmOss.tutorial.jsClient.installDependency.title": "エージェントを依存関係としてセットアップ", - "apmOss.tutorial.jsClient.scriptTags.textPre": "または、スクリプトタグを使用してエージェントのセットアップと構成ができます。` を追加