From e111c2ab3ec734e8f1c31844b77220a2f4b8804a Mon Sep 17 00:00:00 2001 From: ncheckin <68351161+ncheckin@users.noreply.github.com> Date: Mon, 28 Sep 2020 10:57:13 -0400 Subject: [PATCH 1/8] Update tutorial-define-index.asciidoc (#76973) * Update tutorial-define-index.asciidoc * Update docs/getting-started/tutorial-define-index.asciidoc Co-authored-by: Kaarina Tungseth Co-authored-by: Raya Fratkina Co-authored-by: Kaarina Tungseth --- docs/getting-started/tutorial-define-index.asciidoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/getting-started/tutorial-define-index.asciidoc b/docs/getting-started/tutorial-define-index.asciidoc index cb3f6c9ff0c9bf..5a05d565ab3f81 100644 --- a/docs/getting-started/tutorial-define-index.asciidoc +++ b/docs/getting-started/tutorial-define-index.asciidoc @@ -24,7 +24,7 @@ index named `shakespeare,` and the accounts data set, which has an index named . In the *Index pattern name* field, enter `shakes*`. + [role="screenshot"] -image::images/tutorial-pattern-1.png[shakes* index patterns] +image::images/tutorial-pattern-1.png[Image showing how to enter shakes* in Index Pattern Name field] . Click *Next step*. @@ -54,4 +54,3 @@ available, open the menu, go to *Dev Tools > Console*, then enter `GET _cat/indi For Windows, run `Invoke-RestMethod -Uri "http://localhost:9200/_cat/indices"` in Powershell. - From db78d70df359f19a20d41d06975e780ea1cff998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Mon, 28 Sep 2020 16:13:21 +0100 Subject: [PATCH 2/8] [Usage Collection] [schema] Explicit "array" definition (#78141) Co-authored-by: Elastic Machine --- .../src/tools/__fixture__/mock_schema.json | 17 +- .../__fixture__/parsed_working_collector.ts | 33 +-- .../extract_collectors.test.ts.snap | 40 ++-- .../src/tools/manage_schema.ts | 18 +- .../src/tools/serializer.test.ts | 4 +- .../src/tools/serializer.ts | 2 +- .../telemetry_collectors/working_collector.ts | 11 +- .../services/sample_data/usage/collector.ts | 4 +- src/plugins/telemetry/schema/oss_plugins.json | 10 +- src/plugins/usage_collection/README.md | 22 ++ .../server/collector/collector.test.ts | 20 +- .../server/collector/collector.ts | 2 +- .../server/collectors/register.ts | 9 +- .../collectors/get_usage_collector.test.ts | 55 ++--- .../collectors/get_usage_collector.ts | 109 ++++----- .../security_usage_collector.ts | 10 +- .../server/usage/collector.ts | 11 +- .../schema/xpack_plugins.json | 217 ++++++++++-------- .../telemetry/kibana_telemetry_adapter.ts | 8 +- 19 files changed, 362 insertions(+), 240 deletions(-) diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json index 2e69d3625d7ff0..51e5df9bf7dc09 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json @@ -35,16 +35,19 @@ } }, "my_array": { - "properties": { - "total": { - "type": "number" - }, - "type": { - "type": "boolean" + "type": "array", + "items": { + "properties": { + "total": { + "type": "number" + }, + "type": { + "type": "boolean" + } } } }, - "my_str_array": { "type": "keyword" } + "my_str_array": { "type": "array", "items": { "type": "keyword" } } } } } diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts index 54983278726eb2..acf984b7d10eed 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts @@ -55,12 +55,15 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ }, }, my_array: { - total: { - type: 'number', + type: 'array', + items: { + total: { + type: 'number', + }, + type: { type: 'boolean' }, }, - type: { type: 'boolean' }, }, - my_str_array: { type: 'keyword' }, + my_str_array: { type: 'array', items: { type: 'keyword' } }, }, }, fetch: { @@ -91,18 +94,22 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ }, }, my_array: { - total: { - kind: SyntaxKind.NumberKeyword, - type: 'NumberKeyword', - }, - type: { - kind: SyntaxKind.BooleanKeyword, - type: 'BooleanKeyword', + items: { + total: { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', + }, + type: { + kind: SyntaxKind.BooleanKeyword, + type: 'BooleanKeyword', + }, }, }, my_str_array: { - kind: SyntaxKind.StringKeyword, - type: 'StringKeyword', + items: { + kind: SyntaxKind.StringKeyword, + type: 'StringKeyword', + }, }, }, }, diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap index 9868a7d31d4988..206f573b0af782 100644 --- a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap +++ b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap @@ -153,13 +153,15 @@ Array [ "type": "StringKeyword", }, "my_array": Object { - "total": Object { - "kind": 143, - "type": "NumberKeyword", - }, - "type": Object { - "kind": 131, - "type": "BooleanKeyword", + "items": Object { + "total": Object { + "kind": 143, + "type": "NumberKeyword", + }, + "type": Object { + "kind": 131, + "type": "BooleanKeyword", + }, }, }, "my_index_signature_prop": Object { @@ -183,8 +185,10 @@ Array [ "type": "StringKeyword", }, "my_str_array": Object { - "kind": 146, - "type": "StringKeyword", + "items": Object { + "kind": 146, + "type": "StringKeyword", + }, }, }, "typeName": "Usage", @@ -195,12 +199,15 @@ Array [ "type": "keyword", }, "my_array": Object { - "total": Object { - "type": "number", - }, - "type": Object { - "type": "boolean", + "items": Object { + "total": Object { + "type": "number", + }, + "type": Object { + "type": "boolean", + }, }, + "type": "array", }, "my_index_signature_prop": Object { "avg": Object { @@ -228,7 +235,10 @@ Array [ "type": "text", }, "my_str_array": Object { - "type": "keyword", + "items": Object { + "type": "keyword", + }, + "type": "array", }, }, }, diff --git a/packages/kbn-telemetry-tools/src/tools/manage_schema.ts b/packages/kbn-telemetry-tools/src/tools/manage_schema.ts index d422837140d802..7721492fdb691a 100644 --- a/packages/kbn-telemetry-tools/src/tools/manage_schema.ts +++ b/packages/kbn-telemetry-tools/src/tools/manage_schema.ts @@ -28,7 +28,7 @@ export type AllowedSchemaTypes = | 'date' | 'float'; -export function compatibleSchemaTypes(type: AllowedSchemaTypes) { +export function compatibleSchemaTypes(type: AllowedSchemaTypes | 'array') { switch (type) { case 'keyword': case 'text': @@ -40,6 +40,8 @@ export function compatibleSchemaTypes(type: AllowedSchemaTypes) { case 'float': case 'long': return 'number'; + case 'array': + return 'array'; default: throw new Error(`Unknown schema type ${type}`); } @@ -66,10 +68,22 @@ export function isObjectMapping(entity: any) { return false; } +function isArrayMapping(entity: any): entity is { type: 'array'; items: object } { + return typeof entity === 'object' && entity.type === 'array' && typeof entity.items === 'object'; +} + +function getValueMapping(value: any) { + return isObjectMapping(value) ? transformToEsMapping(value) : value; +} + function transformToEsMapping(usageMappingValue: any) { const fieldMapping: any = { properties: {} }; for (const [key, value] of Object.entries(usageMappingValue)) { - fieldMapping.properties[key] = isObjectMapping(value) ? transformToEsMapping(value) : value; + if (isArrayMapping(value)) { + fieldMapping.properties[key] = { ...value, items: getValueMapping(value.items) }; + } else { + fieldMapping.properties[key] = getValueMapping(value); + } } return fieldMapping; } diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.test.ts b/packages/kbn-telemetry-tools/src/tools/serializer.test.ts index 67421172263688..02d663f4d29eb4 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.test.ts @@ -84,8 +84,8 @@ describe('getDescriptor', () => { expect(descriptor).toEqual({ prop1: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' }, prop2: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' }, - prop3: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' }, - prop4: { kind: TelemetryKinds.Date, type: 'Date' }, + prop3: { items: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' } }, + prop4: { items: { kind: TelemetryKinds.Date, type: 'Date' } }, }); }); diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.ts b/packages/kbn-telemetry-tools/src/tools/serializer.ts index 6fe02e3824ba7d..422b298c58374d 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.ts @@ -139,7 +139,7 @@ export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | } if (ts.isArrayTypeNode(node)) { - return getDescriptor(node.elementType, program); + return { items: getDescriptor(node.elementType, program) }; } if (ts.isLiteralTypeNode(node)) { diff --git a/src/fixtures/telemetry_collectors/working_collector.ts b/src/fixtures/telemetry_collectors/working_collector.ts index bdf10b5e549193..0a3bf49638a7b3 100644 --- a/src/fixtures/telemetry_collectors/working_collector.ts +++ b/src/fixtures/telemetry_collectors/working_collector.ts @@ -90,12 +90,15 @@ export const myCollector = makeUsageCollector({ type: { type: 'boolean' }, }, my_array: { - total: { - type: 'number', + type: 'array', + items: { + total: { + type: 'number', + }, + type: { type: 'boolean' }, }, - type: { type: 'boolean' }, }, - my_str_array: { type: 'keyword' }, + my_str_array: { type: 'array', items: { type: 'keyword' } }, my_index_signature_prop: { count: { type: 'number' }, avg: { type: 'number' }, diff --git a/src/plugins/home/server/services/sample_data/usage/collector.ts b/src/plugins/home/server/services/sample_data/usage/collector.ts index d819d67a8d4324..1cece375ce59be 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector.ts @@ -38,12 +38,12 @@ export async function makeSampleDataUsageCollector( fetch: fetchProvider(index), isReady: () => true, schema: { - installed: { type: 'keyword' }, + installed: { type: 'array', items: { type: 'keyword' } }, last_install_date: { type: 'date' }, last_install_set: { type: 'keyword' }, last_uninstall_date: { type: 'date' }, last_uninstall_set: { type: 'keyword' }, - uninstalled: { type: 'keyword' }, + uninstalled: { type: 'array', items: { type: 'keyword' } }, }, }); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 3ee0c181203aaa..6531262b6f1da9 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -29,7 +29,10 @@ "sample-data": { "properties": { "installed": { - "type": "keyword" + "type": "array", + "items": { + "type": "keyword" + } }, "last_install_date": { "type": "date" @@ -44,7 +47,10 @@ "type": "keyword" }, "uninstalled": { - "type": "keyword" + "type": "array", + "items": { + "type": "keyword" + } } } }, diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index d8edc5bb8d18a7..9955f9fac81ca0 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -140,6 +140,14 @@ The `AllowedSchemaTypes` is the list of allowed schema types for the usage field 'keyword', 'text', 'number', 'boolean', 'long', 'date', 'float' ``` +### Arrays + +If any of your properties is an array, the schema definition must follow the convention below: + +``` +{ type: 'array', items: {...mySchemaDefinitionOfTheEntriesInTheArray} } +``` + ### Example ```ts @@ -152,6 +160,8 @@ export const myCollector = makeUsageCollector({ some_obj: { total: 123, }, + some_array: ['value1', 'value2'], + some_array_of_obj: [{total: 123}], }; }, schema: { @@ -163,6 +173,18 @@ export const myCollector = makeUsageCollector({ type: 'number', }, }, + some_array: { + type: 'array', + items: { type: 'keyword' } + }, + some_array_of_obj: { + type: 'array', + items: { + total: { + type: 'number', + }, + }, + }, }, }); ``` diff --git a/src/plugins/usage_collection/server/collector/collector.test.ts b/src/plugins/usage_collection/server/collector/collector.test.ts index a3e2425c1f1227..375fe4f7686c0e 100644 --- a/src/plugins/usage_collection/server/collector/collector.test.ts +++ b/src/plugins/usage_collection/server/collector/collector.test.ts @@ -153,7 +153,10 @@ describe('collector', () => { isReady: () => false, fetch: () => ({ testPass: [{ name: 'a', value: 100 }] }), schema: { - testPass: { name: { type: 'keyword' }, value: { type: 'long' } }, + testPass: { + type: 'array', + items: { name: { type: 'keyword' }, value: { type: 'long' } }, + }, }, }); expect(collector).toBeDefined(); @@ -166,7 +169,10 @@ describe('collector', () => { fetch: () => ({ testPass: [{ name: 'a', value: 100 }], otherProp: 1 }), // @ts-expect-error schema: { - testPass: { name: { type: 'keyword' }, value: { type: 'long' } }, + testPass: { + type: 'array', + items: { name: { type: 'keyword' }, value: { type: 'long' } }, + }, }, }); expect(collector).toBeDefined(); @@ -185,7 +191,10 @@ describe('collector', () => { }, // @ts-expect-error schema: { - testPass: { name: { type: 'keyword' }, value: { type: 'long' } }, + testPass: { + type: 'array', + items: { name: { type: 'keyword' }, value: { type: 'long' } }, + }, }, }); expect(collector).toBeDefined(); @@ -203,7 +212,10 @@ describe('collector', () => { return { otherProp: 1 }; }, schema: { - testPass: { name: { type: 'keyword' }, value: { type: 'long' } }, + testPass: { + type: 'array', + items: { name: { type: 'keyword' }, value: { type: 'long' } }, + }, otherProp: { type: 'long' }, }, }); diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 365e1ce2013370..b0bc18a0cf0ebb 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -40,7 +40,7 @@ export type RecursiveMakeSchemaFrom = U extends object export type MakeSchemaFrom = { [Key in keyof Base]: Base[Key] extends Array - ? RecursiveMakeSchemaFrom + ? { type: 'array'; items: RecursiveMakeSchemaFrom } : RecursiveMakeSchemaFrom; }; diff --git a/x-pack/plugins/ingest_manager/server/collectors/register.ts b/x-pack/plugins/ingest_manager/server/collectors/register.ts index 2be8eb22bc98ce..cb39e6a5be579e 100644 --- a/x-pack/plugins/ingest_manager/server/collectors/register.ts +++ b/x-pack/plugins/ingest_manager/server/collectors/register.ts @@ -50,9 +50,12 @@ export function registerIngestManagerUsageCollector( offline: { type: 'long' }, }, packages: { - name: { type: 'keyword' }, - version: { type: 'keyword' }, - enabled: { type: 'boolean' }, + type: 'array', + items: { + name: { type: 'keyword' }, + version: { type: 'keyword' }, + enabled: { type: 'boolean' }, + }, }, }, }); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts index 17caf80eef22bd..2f63a878b0cde3 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts @@ -80,33 +80,36 @@ describe('getMonitoringUsageCollector', () => { expect(args[0].schema).toStrictEqual({ hasMonitoringData: { type: 'boolean' }, clusters: { - license: { type: 'keyword' }, - clusterUuid: { type: 'keyword' }, - metricbeatUsed: { type: 'boolean' }, - elasticsearch: { - enabled: { type: 'boolean' }, - count: { type: 'long' }, - metricbeatUsed: { type: 'boolean' }, - }, - kibana: { - enabled: { type: 'boolean' }, - count: { type: 'long' }, - metricbeatUsed: { type: 'boolean' }, - }, - logstash: { - enabled: { type: 'boolean' }, - count: { type: 'long' }, - metricbeatUsed: { type: 'boolean' }, - }, - beats: { - enabled: { type: 'boolean' }, - count: { type: 'long' }, - metricbeatUsed: { type: 'boolean' }, - }, - apm: { - enabled: { type: 'boolean' }, - count: { type: 'long' }, + type: 'array', + items: { + license: { type: 'keyword' }, + clusterUuid: { type: 'keyword' }, metricbeatUsed: { type: 'boolean' }, + elasticsearch: { + enabled: { type: 'boolean' }, + count: { type: 'long' }, + metricbeatUsed: { type: 'boolean' }, + }, + kibana: { + enabled: { type: 'boolean' }, + count: { type: 'long' }, + metricbeatUsed: { type: 'boolean' }, + }, + logstash: { + enabled: { type: 'boolean' }, + count: { type: 'long' }, + metricbeatUsed: { type: 'boolean' }, + }, + beats: { + enabled: { type: 'boolean' }, + count: { type: 'long' }, + metricbeatUsed: { type: 'boolean' }, + }, + apm: { + enabled: { type: 'boolean' }, + count: { type: 'long' }, + metricbeatUsed: { type: 'boolean' }, + }, }, }, }); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts index b743a5f8e0b4fa..278a6c163c0adf 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts @@ -28,68 +28,71 @@ export function getMonitoringUsageCollector( type: 'boolean', }, clusters: { - license: { - type: 'keyword', - }, - clusterUuid: { - type: 'keyword', - }, - metricbeatUsed: { - type: 'boolean', - }, - elasticsearch: { - enabled: { - type: 'boolean', + type: 'array', + items: { + license: { + type: 'keyword', }, - count: { - type: 'long', + clusterUuid: { + type: 'keyword', }, metricbeatUsed: { type: 'boolean', }, - }, - kibana: { - enabled: { - type: 'boolean', - }, - count: { - type: 'long', + elasticsearch: { + enabled: { + type: 'boolean', + }, + count: { + type: 'long', + }, + metricbeatUsed: { + type: 'boolean', + }, }, - metricbeatUsed: { - type: 'boolean', + kibana: { + enabled: { + type: 'boolean', + }, + count: { + type: 'long', + }, + metricbeatUsed: { + type: 'boolean', + }, }, - }, - logstash: { - enabled: { - type: 'boolean', + logstash: { + enabled: { + type: 'boolean', + }, + count: { + type: 'long', + }, + metricbeatUsed: { + type: 'boolean', + }, }, - count: { - type: 'long', - }, - metricbeatUsed: { - type: 'boolean', + beats: { + enabled: { + type: 'boolean', + }, + count: { + type: 'long', + }, + metricbeatUsed: { + type: 'boolean', + }, }, - }, - beats: { - enabled: { - type: 'boolean', - }, - count: { - type: 'long', - }, - metricbeatUsed: { - type: 'boolean', - }, - }, - apm: { - enabled: { - type: 'boolean', - }, - count: { - type: 'long', - }, - metricbeatUsed: { - type: 'boolean', + apm: { + enabled: { + type: 'boolean', + }, + count: { + type: 'long', + }, + metricbeatUsed: { + type: 'boolean', + }, }, }, }, diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts index 11e58f7f95fc2a..90483d7c0a4d53 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts @@ -62,10 +62,16 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens type: 'number', }, enabledAuthProviders: { - type: 'keyword', + type: 'array', + items: { + type: 'keyword', + }, }, httpAuthSchemes: { - type: 'keyword', + type: 'array', + items: { + type: 'keyword', + }, }, }, fetch: () => { diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 6fadf956ccaf1e..9514233bdfa86d 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -59,10 +59,13 @@ export const registerCollector: RegisterCollector = ({ total_installed: { type: 'long' }, active_within_last_24_hours: { type: 'long' }, os: { - full_name: { type: 'keyword' }, - platform: { type: 'keyword' }, - version: { type: 'keyword' }, - count: { type: 'long' }, + type: 'array', + items: { + full_name: { type: 'keyword' }, + platform: { type: 'keyword' }, + version: { type: 'keyword' }, + count: { type: 'long' }, + }, }, policies: { malware: { diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 1236f2ad9b5596..bc89d9e0c812d1 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -141,15 +141,18 @@ } }, "packages": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" + "type": "array", + "items": { + "properties": { + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + } } } } @@ -546,78 +549,81 @@ "type": "boolean" }, "clusters": { - "properties": { - "license": { - "type": "keyword" - }, - "clusterUuid": { - "type": "keyword" - }, - "metricbeatUsed": { - "type": "boolean" - }, - "elasticsearch": { - "properties": { - "enabled": { - "type": "boolean" - }, - "count": { - "type": "long" - }, - "metricbeatUsed": { - "type": "boolean" + "type": "array", + "items": { + "properties": { + "license": { + "type": "keyword" + }, + "clusterUuid": { + "type": "keyword" + }, + "metricbeatUsed": { + "type": "boolean" + }, + "elasticsearch": { + "properties": { + "enabled": { + "type": "boolean" + }, + "count": { + "type": "long" + }, + "metricbeatUsed": { + "type": "boolean" + } } - } - }, - "kibana": { - "properties": { - "enabled": { - "type": "boolean" - }, - "count": { - "type": "long" - }, - "metricbeatUsed": { - "type": "boolean" + }, + "kibana": { + "properties": { + "enabled": { + "type": "boolean" + }, + "count": { + "type": "long" + }, + "metricbeatUsed": { + "type": "boolean" + } } - } - }, - "logstash": { - "properties": { - "enabled": { - "type": "boolean" - }, - "count": { - "type": "long" - }, - "metricbeatUsed": { - "type": "boolean" + }, + "logstash": { + "properties": { + "enabled": { + "type": "boolean" + }, + "count": { + "type": "long" + }, + "metricbeatUsed": { + "type": "boolean" + } } - } - }, - "beats": { - "properties": { - "enabled": { - "type": "boolean" - }, - "count": { - "type": "long" - }, - "metricbeatUsed": { - "type": "boolean" + }, + "beats": { + "properties": { + "enabled": { + "type": "boolean" + }, + "count": { + "type": "long" + }, + "metricbeatUsed": { + "type": "boolean" + } } - } - }, - "apm": { - "properties": { - "enabled": { - "type": "boolean" - }, - "count": { - "type": "long" - }, - "metricbeatUsed": { - "type": "boolean" + }, + "apm": { + "properties": { + "enabled": { + "type": "boolean" + }, + "count": { + "type": "long" + }, + "metricbeatUsed": { + "type": "boolean" + } } } } @@ -720,18 +726,21 @@ "type": "long" }, "os": { - "properties": { - "full_name": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "count": { - "type": "long" + "type": "array", + "items": { + "properties": { + "full_name": { + "type": "keyword" + }, + "platform": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "count": { + "type": "long" + } } } }, @@ -771,10 +780,16 @@ "type": "number" }, "enabledAuthProviders": { - "type": "keyword" + "type": "array", + "items": { + "type": "keyword" + } }, "httpAuthSchemes": { - "type": "keyword" + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -906,16 +921,28 @@ "type": "boolean" }, "autorefreshInterval": { - "type": "long" + "type": "array", + "items": { + "type": "long" + } }, "dateRangeEnd": { - "type": "date" + "type": "array", + "items": { + "type": "date" + } }, "dateRangeStart": { - "type": "date" + "type": "array", + "items": { + "type": "date" + } }, "monitor_frequency": { - "type": "long" + "type": "array", + "items": { + "type": "long" + } }, "monitor_name_stats": { "properties": { diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts index 149dbb4244c869..106aab35154702 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts @@ -47,10 +47,10 @@ export class KibanaTelemetryAdapter { autoRefreshEnabled: { type: 'boolean', }, - autorefreshInterval: { type: 'long' }, - dateRangeEnd: { type: 'date' }, - dateRangeStart: { type: 'date' }, - monitor_frequency: { type: 'long' }, + autorefreshInterval: { type: 'array', items: { type: 'long' } }, + dateRangeEnd: { type: 'array', items: { type: 'date' } }, + dateRangeStart: { type: 'array', items: { type: 'date' } }, + monitor_frequency: { type: 'array', items: { type: 'long' } }, monitor_name_stats: { avg_length: { type: 'float' }, max_length: { type: 'long' }, From 0ebaf92a6a8a3a4626eb362a9005c4674b965923 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 28 Sep 2020 11:22:03 -0400 Subject: [PATCH 3/8] [Lens] Fieldless operations (#78080) * [Lens] Fieldless operations * Overhaul types * Fix invalid state and add tests * Fix types * Small cleanup * Add additional error message * Reset field selector to empty state when invalid Co-authored-by: Elastic Machine --- .../dimension_panel/dimension_editor.tsx | 233 +++++---- .../dimension_panel/dimension_panel.test.tsx | 446 ++++++++---------- .../dimension_panel/dimension_panel.tsx | 46 +- .../dimension_panel/field_select.tsx | 24 +- .../indexpattern_suggestions.ts | 12 +- .../operations/definitions/cardinality.tsx | 9 +- .../operations/definitions/column_types.ts | 20 +- .../operations/definitions/count.tsx | 12 +- .../operations/definitions/date_histogram.tsx | 8 +- .../definitions/filters/filters.test.tsx | 25 +- .../definitions/filters/filters.tsx | 30 +- .../operations/definitions/index.ts | 98 ++-- .../operations/definitions/metrics.tsx | 12 +- .../operations/definitions/ranges/ranges.tsx | 3 +- .../operations/definitions/terms.tsx | 39 +- .../operations/index.ts | 2 +- .../operations/operations.test.ts | 27 +- .../operations/operations.ts | 75 +-- .../indexpattern_datasource/state_helpers.ts | 3 +- .../test/functional/apps/lens/smokescreen.ts | 3 + 20 files changed, 569 insertions(+), 558 deletions(-) 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 2f64a36e0462ee..2572f732aa1b3f 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 @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { EuiFormLabel } from '@elastic/eui'; import { IndexPatternColumn, OperationType } from '../indexpattern'; -import { IndexPatternDimensionEditorProps, OperationFieldSupportMatrix } from './dimension_panel'; +import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from './dimension_panel'; import { operationDefinitionMap, getOperationDisplay, @@ -36,7 +36,7 @@ const operationPanels = getOperationDisplay(); export interface DimensionEditorProps extends IndexPatternDimensionEditorProps { selectedColumn?: IndexPatternColumn; - operationFieldSupportMatrix: OperationFieldSupportMatrix; + operationSupportMatrix: OperationSupportMatrix; currentIndexPattern: IndexPattern; } @@ -90,7 +90,7 @@ const LabelInput = ({ value, onChange }: { value: string; onChange: (value: stri export function DimensionEditor(props: DimensionEditorProps) { const { selectedColumn, - operationFieldSupportMatrix, + operationSupportMatrix, state, columnId, setState, @@ -98,14 +98,16 @@ export function DimensionEditor(props: DimensionEditorProps) { currentIndexPattern, hideGrouping, } = props; - const { operationByField, fieldByOperation } = operationFieldSupportMatrix; + const { operationByField, fieldByOperation } = operationSupportMatrix; const [ incompatibleSelectedOperationType, setInvalidOperationType, ] = useState(null); - const ParamEditor = - selectedColumn && operationDefinitionMap[selectedColumn.operationType].paramEditor; + const selectedOperationDefinition = + selectedColumn && operationDefinitionMap[selectedColumn.operationType]; + + const ParamEditor = selectedOperationDefinition?.paramEditor; const fieldMap: Record = useMemo(() => { const fields: Record = {}; @@ -129,6 +131,10 @@ export function DimensionEditor(props: DimensionEditorProps) { [ ...asOperationOptions(validOperationTypes, true), ...asOperationOptions(possibleOperationTypes, false), + ...asOperationOptions( + operationSupportMatrix.operationWithoutField, + !selectedColumn || !hasField(selectedColumn) + ), ], 'operationType' ); @@ -166,12 +172,30 @@ export function DimensionEditor(props: DimensionEditorProps) { compatibleWithCurrentField ? '' : ' incompatible' }`, onClick() { - // todo: when moving from terms agg to filters, we want to create a filter `$field.name : *` - // it probably has to be re-thought when removing the field name. - const isTermsToFilters = - selectedColumn?.operationType === 'terms' && operationType === 'filters'; - - if (!selectedColumn || !compatibleWithCurrentField) { + if (operationDefinitionMap[operationType].input === 'none') { + // Clear invalid state because we are creating a valid column + setInvalidOperationType(null); + if (selectedColumn?.operationType === operationType) { + return; + } + setState( + changeColumn({ + state, + layerId, + columnId, + newColumn: buildColumn({ + columns: props.state.layers[props.layerId].columns, + suggestedPriority: props.suggestedPriority, + layerId: props.layerId, + op: operationType, + indexPattern: currentIndexPattern, + previousColumn: selectedColumn, + }), + }) + ); + trackUiEvent(`indexpattern_dimension_operation_${operationType}`); + return; + } else if (!selectedColumn || !compatibleWithCurrentField) { const possibleFields = fieldByOperation[operationType] || []; if (possibleFields.length === 1) { @@ -197,19 +221,20 @@ export function DimensionEditor(props: DimensionEditorProps) { trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } - if (incompatibleSelectedOperationType && !isTermsToFilters) { - setInvalidOperationType(null); - } - if (selectedColumn.operationType === operationType) { + + setInvalidOperationType(null); + + if (selectedColumn?.operationType === operationType) { return; } + const newColumn: IndexPatternColumn = buildColumn({ columns: props.state.layers[props.layerId].columns, suggestedPriority: props.suggestedPriority, layerId: props.layerId, op: operationType, indexPattern: currentIndexPattern, - field: fieldMap[selectedColumn.sourceField], + field: hasField(selectedColumn) ? fieldMap[selectedColumn.sourceField] : undefined, previousColumn: selectedColumn, }); @@ -244,93 +269,101 @@ export function DimensionEditor(props: DimensionEditorProps) {
- - { - setState( - deleteColumn({ - state, - layerId, - columnId, - }) - ); - }} - onChoose={(choice) => { - let column: IndexPatternColumn; - if ( - !incompatibleSelectedOperationType && - selectedColumn && - 'field' in choice && - choice.operationType === selectedColumn.operationType - ) { - // If we just changed the field are not in an error state and the operation didn't change, - // we use the operations onFieldChange method to calculate the new column. - column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); - } else { - // Otherwise we'll use the buildColumn method to calculate a new column - const compatibleOperations = - ('field' in choice && - operationFieldSupportMatrix.operationByField[choice.field]) || - []; - let operation; - if (compatibleOperations.length > 0) { - operation = - incompatibleSelectedOperationType && - compatibleOperations.includes(incompatibleSelectedOperationType) - ? incompatibleSelectedOperationType - : compatibleOperations[0]; - } else if ('field' in choice) { - operation = choice.operationType; - } - column = buildColumn({ - columns: props.state.layers[props.layerId].columns, - field: fieldMap[choice.field], - indexPattern: currentIndexPattern, - layerId: props.layerId, - suggestedPriority: props.suggestedPriority, - op: operation as OperationType, - previousColumn: selectedColumn, - }); + > + { + setState( + deleteColumn({ + state, + layerId, + columnId, + }) + ); + }} + onChoose={(choice) => { + let column: IndexPatternColumn; + if ( + !incompatibleSelectedOperationType && + selectedColumn && + 'field' in choice && + choice.operationType === selectedColumn.operationType + ) { + // If we just changed the field are not in an error state and the operation didn't change, + // we use the operations onFieldChange method to calculate the new column. + column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); + } else { + // Otherwise we'll use the buildColumn method to calculate a new column + const compatibleOperations = + ('field' in choice && operationSupportMatrix.operationByField[choice.field]) || + []; + let operation; + if (compatibleOperations.length > 0) { + operation = + incompatibleSelectedOperationType && + compatibleOperations.includes(incompatibleSelectedOperationType) + ? incompatibleSelectedOperationType + : compatibleOperations[0]; + } else if ('field' in choice) { + operation = choice.operationType; + } + column = buildColumn({ + columns: props.state.layers[props.layerId].columns, + field: fieldMap[choice.field], + indexPattern: currentIndexPattern, + layerId: props.layerId, + suggestedPriority: props.suggestedPriority, + op: operation as OperationType, + previousColumn: selectedColumn, + }); + } - setState( - changeColumn({ - state, - layerId, - columnId, - newColumn: column, - keepParams: false, - }) - ); - setInvalidOperationType(null); - }} - /> - + setState( + changeColumn({ + state, + layerId, + columnId, + newColumn: column, + keepParams: false, + }) + ); + setInvalidOperationType(null); + }} + /> + + ) : null} - {!incompatibleSelectedOperationType && ParamEditor && ( + {!incompatibleSelectedOperationType && selectedColumn && ParamEditor && ( <> { let state: IndexPatternPrivateState; let setState: jest.Mock; let defaultProps: IndexPatternDimensionEditorProps; let dragDropContext: DragContextState; + function getStateWithColumns(columns: Record) { + return { ...state, layers: { first: { ...state.layers.first, columns } } }; + } + beforeEach(() => { state = { indexPatternRefs: [], @@ -179,7 +207,7 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(filterOperations).toBeCalled(); }); - it('should show field select combo box on click', () => { + it('should show field select', () => { wrapper = mount(); expect( @@ -187,6 +215,29 @@ describe('IndexPatternDimensionEditorPanel', () => { ).toHaveLength(1); }); + it('should not show field select on fieldless operation', () => { + wrapper = mount( + + ); + + expect( + wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') + ).toHaveLength(0); + }); + it('should not show any choices if the filter returns false', () => { wrapper = mount( { wrapper = mount( ); @@ -292,26 +324,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount( ); @@ -324,30 +337,15 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(items.find(({ label }) => label === 'Date histogram')!['data-test-subj']).toContain( 'incompatible' ); + + // Fieldless operation is compatible with field + expect(items.find(({ label }) => label === 'Filters')!['data-test-subj']).toContain( + 'compatible' + ); }); it('should keep the operation when switching to another field compatible with this operation', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'max', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, - }, - }, - }, - }, - }; + const initialState: IndexPatternPrivateState = getStateWithColumns({ col1: bytesColumn }); wrapper = mount( @@ -415,27 +413,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount( ); @@ -505,27 +483,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount( ); @@ -553,28 +511,13 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount( ); @@ -640,6 +583,62 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); }); + it('should leave error state if the original operation is re-selected', () => { + wrapper = mount(); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); + + it('should leave error state when switching from incomplete state to fieldless operation', () => { + wrapper = mount(); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-filters incompatible"]') + .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); + + it('should leave error state when re-selecting the original fieldless function', () => { + wrapper = mount( + + ); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-filters"]') + .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); + it('should indicate fields compatible with selected operation', () => { wrapper = mount(); @@ -701,28 +700,18 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should select the Records field when count is selected', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'avg', - sourceField: 'bytes', - }, - }, - }, - }, - }; wrapper = mount( ); @@ -737,28 +726,18 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should indicate document and field compatibility with selected document operation', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'count', - sourceField: 'Records', - }, - }, - }, - }, - }; wrapper = mount( ); @@ -942,28 +921,18 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should indicate document compatibility when document operation is selected', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'count', - sourceField: 'Records', - }, - }, - }, - }, - }; wrapper = mount( ); @@ -1031,26 +1000,9 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should use helper function when changing the function', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'max', - sourceField: 'bytes', - }, - }, - }, - }, - }; + const initialState: IndexPatternPrivateState = getStateWithColumns({ + col1: bytesColumn, + }); wrapper = mount( ); @@ -1095,25 +1047,16 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('allows custom format', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of memory', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'memory', - }, - }, - }, + const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Average of memory', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'memory', }, - }; + }); wrapper = mount( @@ -1145,29 +1088,19 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('keeps decimal places while switching', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of memory', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'memory', - params: { - format: { id: 'bytes', params: { decimals: 0 } }, - }, - }, - }, + const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Average of memory', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'memory', + params: { + format: { id: 'bytes', params: { decimals: 0 } }, }, }, - }; - + }); wrapper = mount( ); @@ -1195,28 +1128,19 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('allows custom format with number of decimal places', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of memory', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'memory', - params: { - format: { id: 'bytes', params: { decimals: 2 } }, - }, - }, - }, + const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Average of memory', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'memory', + params: { + format: { id: 'bytes', params: { decimals: 2 } }, }, }, - }; + }); wrapper = mount( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 923f7145d1c64c..c4d8300722f83c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -46,8 +46,9 @@ export type IndexPatternDimensionEditorProps = DatasourceDimensionEditorProps< dateRange: DateRange; }; -export interface OperationFieldSupportMatrix { +export interface OperationSupportMatrix { operationByField: Partial>; + operationWithoutField: OperationType[]; fieldByOperation: Partial>; } @@ -58,7 +59,7 @@ type Props = Pick< // TODO: This code has historically been memoized, as a potentially performance // sensitive task. If we can add memoization without breaking the behavior, we should. -const getOperationFieldSupportMatrix = (props: Props): OperationFieldSupportMatrix => { +const getOperationSupportMatrix = (props: Props): OperationSupportMatrix => { const layerId = props.layerId; const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; @@ -67,37 +68,43 @@ const getOperationFieldSupportMatrix = (props: Props): OperationFieldSupportMatr ).filter((operation) => props.filterOperations(operation.operationMetaData)); const supportedOperationsByField: Partial> = {}; + const supportedOperationsWithoutField: OperationType[] = []; const supportedFieldsByOperation: Partial> = {}; filteredOperationsByMetadata.forEach(({ operations }) => { operations.forEach((operation) => { - if (supportedOperationsByField[operation.field]) { - supportedOperationsByField[operation.field]!.push(operation.operationType); - } else { - supportedOperationsByField[operation.field] = [operation.operationType]; - } - - if (supportedFieldsByOperation[operation.operationType]) { - supportedFieldsByOperation[operation.operationType]!.push(operation.field); - } else { - supportedFieldsByOperation[operation.operationType] = [operation.field]; + if (operation.type === 'field') { + if (supportedOperationsByField[operation.field]) { + supportedOperationsByField[operation.field]!.push(operation.operationType); + } else { + supportedOperationsByField[operation.field] = [operation.operationType]; + } + + if (supportedFieldsByOperation[operation.operationType]) { + supportedFieldsByOperation[operation.operationType]!.push(operation.field); + } else { + supportedFieldsByOperation[operation.operationType] = [operation.field]; + } + } else if (operation.type === 'none') { + supportedOperationsWithoutField.push(operation.operationType); } }); }); return { operationByField: _.mapValues(supportedOperationsByField, _.uniq), + operationWithoutField: _.uniq(supportedOperationsWithoutField), fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), }; }; export function canHandleDrop(props: DatasourceDimensionDropProps) { - const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + const operationSupportMatrix = getOperationSupportMatrix(props); const { dragging } = props.dragDropContext; const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; function hasOperationForField(field: IndexPatternField) { - return Boolean(operationFieldSupportMatrix.operationByField[field.name]); + return Boolean(operationSupportMatrix.operationByField[field.name]); } if (isDraggedField(dragging)) { @@ -119,11 +126,11 @@ export function canHandleDrop(props: DatasourceDimensionDropProps) { - const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + const operationSupportMatrix = getOperationSupportMatrix(props); const droppedItem = props.droppedItem; function hasOperationForField(field: IndexPatternField) { - return Boolean(operationFieldSupportMatrix.operationByField[field.name]); + return Boolean(operationSupportMatrix.operationByField[field.name]); } if (isDraggedOperation(droppedItem) && droppedItem.layerId === props.layerId) { @@ -167,8 +174,7 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps ); }; 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 e71a85868b8552..de472cb09cdfee 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 @@ -20,7 +20,7 @@ import { EuiHighlight } from '@elastic/eui'; import { OperationType } from '../indexpattern'; import { LensFieldIcon } from '../lens_field_icon'; import { DataType } from '../../types'; -import { OperationFieldSupportMatrix } from './dimension_panel'; +import { OperationSupportMatrix } from './dimension_panel'; import { IndexPattern, IndexPatternField, IndexPatternPrivateState } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { fieldExists } from '../pure_helpers'; @@ -37,7 +37,7 @@ export interface FieldSelectProps extends EuiComboBoxProps<{}> { incompatibleSelectedOperationType: OperationType | null; selectedColumnOperationType?: OperationType; selectedColumnSourceField?: string; - operationFieldSupportMatrix: OperationFieldSupportMatrix; + operationSupportMatrix: OperationSupportMatrix; onChoose: (choice: FieldChoice) => void; onDeleteColumn: () => void; existingFields: IndexPatternPrivateState['existingFields']; @@ -49,13 +49,13 @@ export function FieldSelect({ incompatibleSelectedOperationType, selectedColumnOperationType, selectedColumnSourceField, - operationFieldSupportMatrix, + operationSupportMatrix, onChoose, onDeleteColumn, existingFields, ...rest }: FieldSelectProps) { - const { operationByField } = operationFieldSupportMatrix; + const { operationByField } = operationSupportMatrix; const memoizedFieldOptions = useMemo(() => { const fields = Object.keys(operationByField).sort(); @@ -173,15 +173,13 @@ export function FieldSelect({ options={(memoizedFieldOptions as unknown) as EuiComboBoxOptionOption[]} isInvalid={Boolean(incompatibleSelectedOperationType)} selectedOptions={ - ((selectedColumnOperationType - ? selectedColumnSourceField - ? [ - { - label: fieldMap[selectedColumnSourceField].displayName, - value: { type: 'field', field: selectedColumnSourceField }, - }, - ] - : [memoizedFieldOptions[0]] + ((selectedColumnOperationType && selectedColumnSourceField + ? [ + { + label: fieldMap[selectedColumnSourceField].displayName, + value: { type: 'field', field: selectedColumnSourceField }, + }, + ] : []) as unknown) as EuiComboBoxOptionOption[] } singleSelection={{ asPlainText: true }} 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 f3aa9c4f51c82f..f5e64149c2c769 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -483,11 +483,15 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId const updatedLayer = { ...layer, columnOrder: [secondBucket, firstBucket, ...rest] }; const currentFields = state.indexPatterns[state.currentIndexPatternId].fields; const firstBucketLabel = - currentFields.find((field) => field.name === layer.columns[firstBucket].sourceField) - ?.displayName || ''; + currentFields.find((field) => { + const column = layer.columns[firstBucket]; + return hasField(column) && column.sourceField === field.name; + })?.displayName || ''; const secondBucketLabel = - currentFields.find((field) => field.name === layer.columns[secondBucket].sourceField) - ?.displayName || ''; + currentFields.find((field) => { + const column = layer.columns[secondBucket]; + return hasField(column) && column.sourceField === field.name; + })?.displayName || ''; return buildSuggestion({ state, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index b0777c7febd7d6..65119d3978ee6b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { OperationDefinition } from './index'; -import { FormattedIndexPatternColumn } from './column_types'; +import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; const supportedTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']); @@ -21,15 +21,18 @@ function ofName(name: string) { }); } -export interface CardinalityIndexPatternColumn extends FormattedIndexPatternColumn { +export interface CardinalityIndexPatternColumn + extends FormattedIndexPatternColumn, + FieldBasedIndexPatternColumn { operationType: 'cardinality'; } -export const cardinalityOperation: OperationDefinition = { +export const cardinalityOperation: OperationDefinition = { type: OPERATION_TYPE, displayName: i18n.translate('xpack.lens.indexPattern.cardinality', { defaultMessage: 'Unique count', }), + input: 'field', getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( supportedTypes.has(type) && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts index 3244eeb94d1e27..2e95e3fd4250fc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts @@ -14,7 +14,6 @@ import { Operation, DimensionPriority } from '../../../types'; export interface BaseIndexPatternColumn extends Operation { // Private operationType: string; - sourceField: string; suggestedPriority?: DimensionPriority; customLabel?: boolean; } @@ -31,23 +30,6 @@ export interface FormattedIndexPatternColumn extends BaseIndexPatternColumn { }; } -/** - * Base type for a column that doesn't have additional parameter. - * - * * `TOperationType` should be a string type containing just the type - * of the operation (e.g. `"sum"`). - * * `TBase` is the base column interface the operation type is set for - - * by default this is `FieldBasedIndexPatternColumn`, so - * `ParameterlessIndexPatternColumn<'foo'>` will give you a column type - * for an operation named foo that operates on a field. - * By passing in another `TBase` (e.g. just `BaseIndexPatternColumn`), - * you can also create other column types. - */ -export type ParameterlessIndexPatternColumn< - TOperationType extends string, - TBase extends BaseIndexPatternColumn = FieldBasedIndexPatternColumn -> = TBase & { operationType: TOperationType }; - export interface FieldBasedIndexPatternColumn extends BaseIndexPatternColumn { - suggestedPriority?: DimensionPriority; + sourceField: string; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index bb1aef856de780..cdf1a6b7604930 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -6,23 +6,25 @@ import { i18n } from '@kbn/i18n'; import { OperationDefinition } from './index'; -import { FormattedIndexPatternColumn } from './column_types'; +import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; import { IndexPatternField } from '../../types'; const countLabel = i18n.translate('xpack.lens.indexPattern.countOf', { defaultMessage: 'Count of records', }); -export type CountIndexPatternColumn = FormattedIndexPatternColumn & { - operationType: 'count'; -}; +export type CountIndexPatternColumn = FormattedIndexPatternColumn & + FieldBasedIndexPatternColumn & { + operationType: 'count'; + }; -export const countOperation: OperationDefinition = { +export const countOperation: OperationDefinition = { type: 'count', priority: 2, displayName: i18n.translate('xpack.lens.indexPattern.count', { defaultMessage: 'Count', }), + input: 'field', onFieldChange: (oldColumn, indexPattern, field) => { return { ...oldColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index 7784024b031324..185f44405bb4b1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -36,11 +36,15 @@ export interface DateHistogramIndexPatternColumn extends FieldBasedIndexPatternC }; } -export const dateHistogramOperation: OperationDefinition = { +export const dateHistogramOperation: OperationDefinition< + DateHistogramIndexPatternColumn, + 'field' +> = { type: 'date_histogram', displayName: i18n.translate('xpack.lens.indexPattern.dateHistogram', { defaultMessage: 'Date histogram', }), + input: 'field', priority: 5, // Highest priority level used getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( @@ -136,7 +140,7 @@ export const dateHistogramOperation: OperationDefinition { + paramEditor: ({ state, setState, currentColumn, layerId, dateRange, data }) => { const field = currentColumn && state.indexPatterns[state.layers[layerId].indexPatternId].fields.find( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx index 2d79c5faf74fe2..3ac01886537dca 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx @@ -59,7 +59,6 @@ describe('filters', () => { operationType: 'filters', scale: 'ordinal', isBucketed: true, - sourceField: 'Records', params: { filters: [ { @@ -112,34 +111,14 @@ describe('filters', () => { }); }); - describe('getPossibleOperationForField', () => { + describe('getPossibleOperation', () => { it('should return operation with the right type for document', () => { - expect( - filtersOperation.getPossibleOperationForField({ - aggregatable: true, - searchable: true, - name: 'test', - displayName: 'test', - type: 'document', - }) - ).toEqual({ + expect(filtersOperation.getPossibleOperation()).toEqual({ dataType: 'string', isBucketed: true, scale: 'ordinal', }); }); - - it('should not return operation if field type is not document', () => { - expect( - filtersOperation.getPossibleOperationForField({ - aggregatable: false, - searchable: true, - name: 'test', - displayName: 'test', - type: 'string', - }) - ).toEqual(undefined); - }); }); describe('popover param editor', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index 9985ad7229ecc8..ad0b9f2dbb0ab0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiLink, htmlIdGenerator } from '@elastic/eui'; import { updateColumnParam } from '../../../state_helpers'; import { OperationDefinition } from '../index'; -import { FieldBasedIndexPatternColumn } from '../column_types'; +import { BaseIndexPatternColumn } from '../column_types'; import { FilterPopover } from './filter_popover'; import { IndexPattern } from '../../../types'; import { Query, esKuery, esQuery } from '../../../../../../../../src/plugins/data/public'; @@ -61,31 +61,22 @@ export const isQueryValid = (input: Query, indexPattern: IndexPattern) => { } }; -export interface FiltersIndexPatternColumn extends FieldBasedIndexPatternColumn { +export interface FiltersIndexPatternColumn extends BaseIndexPatternColumn { operationType: 'filters'; params: { filters: Filter[]; }; } -export const filtersOperation: OperationDefinition = { +export const filtersOperation: OperationDefinition = { type: 'filters', displayName: filtersLabel, priority: 3, // Higher than any metric - getPossibleOperationForField: ({ type }) => { - if (type === 'document') { - return { - dataType: 'string', - isBucketed: true, - scale: 'ordinal', - }; - } - }, - isTransferable: () => false, - onFieldChange: (oldColumn, indexPattern, field) => oldColumn, + input: 'none', + isTransferable: () => true, - buildColumn({ suggestedPriority, field, previousColumn }) { + buildColumn({ suggestedPriority, previousColumn }) { let params = { filters: [defaultFilter] }; if (previousColumn?.operationType === 'terms') { params = { @@ -108,11 +99,18 @@ export const filtersOperation: OperationDefinition = scale: 'ordinal', suggestedPriority, isBucketed: true, - sourceField: field.name, params, }; }, + getPossibleOperation() { + return { + dataType: 'string', + isBucketed: true, + scale: 'ordinal', + }; + }, + toEsAggsConfig: (column, columnId, indexPattern) => { const validFilters = column.params.filters?.filter((f: Filter) => isQueryValid(f.input, indexPattern) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 19523b550af5a6..38aec866ca5cb1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -28,22 +28,6 @@ import { DateRange } from '../../../../common'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { RangeIndexPatternColumn, rangeOperation } from './ranges'; -// List of all operation definitions registered to this data source. -// If you want to implement a new operation, add the definition to this array and -// the column type to the `IndexPatternColumn` union type below. -const internalOperationDefinitions = [ - filtersOperation, - termsOperation, - dateHistogramOperation, - minOperation, - maxOperation, - averageOperation, - cardinalityOperation, - sumOperation, - countOperation, - rangeOperation, -]; - /** * A union type of all available column types. If a column is of an unknown type somewhere * withing the indexpattern data source it should be typed as `IndexPatternColumn` to make @@ -61,6 +45,24 @@ export type IndexPatternColumn = | SumIndexPatternColumn | CountIndexPatternColumn; +export type FieldBasedIndexPatternColumn = Extract; + +// List of all operation definitions registered to this data source. +// If you want to implement a new operation, add the definition to this array and +// the column type to the `IndexPatternColumn` union type below. +const internalOperationDefinitions = [ + filtersOperation, + termsOperation, + dateHistogramOperation, + minOperation, + maxOperation, + averageOperation, + cardinalityOperation, + sumOperation, + countOperation, + rangeOperation, +]; + export { termsOperation } from './terms'; export { rangeOperation } from './ranges'; export { filtersOperation } from './filters'; @@ -71,7 +73,7 @@ export { countOperation } from './count'; /** * Properties passed to the operation-specific part of the popover editor */ -export interface ParamEditorProps { +export interface ParamEditorProps { currentColumn: C; state: IndexPatternPrivateState; setState: StateSetter; @@ -138,13 +140,25 @@ interface BaseBuildColumnArgs { indexPattern: IndexPattern; } -/** - * Shape of an operation definition. If the type parameter of the definition - * indicates a field based column, `getPossibleOperationForField` has to be - * specified, otherwise `getPossibleOperationForDocument` has to be defined. - */ -export interface OperationDefinition - extends BaseOperationDefinitionProps { +interface FieldlessOperationDefinition { + input: 'none'; + /** + * Builds the column object for the given parameters. Should include default p + */ + buildColumn: ( + arg: BaseBuildColumnArgs & { + previousColumn?: IndexPatternColumn; + } + ) => C; + /** + * Returns the meta data of the operation if applied. Undefined + * if the field is not applicable. + */ + getPossibleOperation: () => OperationMetadata | undefined; +} + +interface FieldBasedOperationDefinition { + input: 'field'; /** * Returns the meta data of the operation if applied to the given field. Undefined * if the field is not applicable to the operation. @@ -156,7 +170,8 @@ export interface OperationDefinition buildColumn: ( arg: BaseBuildColumnArgs & { field: IndexPatternField; - previousColumn?: IndexPatternColumn; + // previousColumn?: IndexPatternColumn; + previousColumn?: C; } ) => C; /** @@ -175,9 +190,29 @@ export interface OperationDefinition * @param indexPattern The index pattern that field is on. * @param field The field that the user changed to. */ - onFieldChange: (oldColumn: C, indexPattern: IndexPattern, field: IndexPatternField) => C; + onFieldChange: ( + // oldColumn: FieldBasedIndexPatternColumn, + oldColumn: C, + indexPattern: IndexPattern, + field: IndexPatternField + ) => C; } +interface OperationDefinitionMap { + field: FieldBasedOperationDefinition; + none: FieldlessOperationDefinition; +} + +/** + * Shape of an operation definition. If the type parameter of the definition + * indicates a field based column, `getPossibleOperationForField` has to be + * specified, otherwise `getPossibleOperation` has to be defined. + */ +export type OperationDefinition< + C extends BaseIndexPatternColumn, + Input extends keyof OperationDefinitionMap +> = BaseOperationDefinitionProps & OperationDefinitionMap[Input]; + /** * A union type of all available operation types. The operation type is a unique id of an operation. * Each column is assigned to exactly one operation type. @@ -188,7 +223,9 @@ export type OperationType = typeof internalOperationDefinitions[number]['type']; * This is an operation definition of an unspecified column out of all possible * column types. */ -export type GenericOperationDefinition = OperationDefinition; +export type GenericOperationDefinition = + | OperationDefinition + | OperationDefinition; /** * List of all available operation definitions @@ -206,7 +243,10 @@ export const operationDefinitions = internalOperationDefinitions as GenericOpera * (e.g. `import { termsOperation } from './operations/definitions'`). This map is * intended to be used in situations where the operation type is not known during compile time. */ -export const operationDefinitionMap = internalOperationDefinitions.reduce( +export const operationDefinitionMap: Record< + string, + GenericOperationDefinition +> = internalOperationDefinitions.reduce( (definitionMap, definition) => ({ ...definitionMap, [definition.type]: definition }), {} -) as Record; +); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 4c37d95f6b0509..c02f7bcb7d2cda 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -6,11 +6,12 @@ import { i18n } from '@kbn/i18n'; import { OperationDefinition } from './index'; -import { FormattedIndexPatternColumn } from './column_types'; +import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; -type MetricColumn = FormattedIndexPatternColumn & { - operationType: T; -}; +type MetricColumn = FormattedIndexPatternColumn & + FieldBasedIndexPatternColumn & { + operationType: T; + }; function buildMetricOperation>({ type, @@ -27,6 +28,7 @@ function buildMetricOperation>({ type, priority, displayName, + input: 'field', getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type: fieldType }) => { if ( fieldType === 'number' && @@ -78,7 +80,7 @@ function buildMetricOperation>({ missing: 0, }, }), - } as OperationDefinition; + } as OperationDefinition; } export type SumIndexPatternColumn = MetricColumn<'sum'>; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index 530c2e962759b8..1971fb2875bed5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -76,12 +76,13 @@ function getEsAggsParams({ sourceField, params }: RangeIndexPatternColumn) { }; } -export const rangeOperation: OperationDefinition = { +export const rangeOperation: OperationDefinition = { type: 'range', displayName: i18n.translate('xpack.lens.indexPattern.ranges', { defaultMessage: 'Ranges', }), priority: 4, // Higher than terms, so numbers get histogram + input: 'field', getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( type === 'number' && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx index c1a87a2013747e..c147029bbd3c77 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx @@ -48,12 +48,13 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { }; } -export const termsOperation: OperationDefinition = { +export const termsOperation: OperationDefinition = { type: 'terms', displayName: i18n.translate('xpack.lens.indexPattern.terms', { defaultMessage: 'Top values', }), priority: 3, // Higher than any metric + input: 'field', getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( supportedTypes.has(type) && @@ -95,23 +96,25 @@ export const termsOperation: OperationDefinition = { }, }; }, - toEsAggsConfig: (column, columnId, _indexPattern) => ({ - id: columnId, - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: column.sourceField, - orderBy: - column.params.orderBy.type === 'alphabetical' ? '_key' : column.params.orderBy.columnId, - order: column.params.orderDirection, - size: column.params.size, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }), + toEsAggsConfig: (column, columnId, _indexPattern) => { + return { + id: columnId, + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: column.sourceField, + orderBy: + column.params.orderBy.type === 'alphabetical' ? '_key' : column.params.orderBy.columnId, + order: column.params.orderDirection, + size: column.params.size, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }; + }, onFieldChange: (oldColumn, indexPattern, field) => { return { ...oldColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts index 1e2bc5dcb6b623..31a36c59274da7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts @@ -5,4 +5,4 @@ */ export * from './operations'; -export { OperationType, IndexPatternColumn } from './definitions'; +export { OperationType, IndexPatternColumn, FieldBasedIndexPatternColumn } from './definitions'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 703431f724c5dd..c1bd4b84099b74 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -182,7 +182,7 @@ describe('getOperationTypesForField', () => { }, }; - it('should build a column for the given operation type if it is passed in', () => { + it('should build a column for the given field-based operation type if it is passed in', () => { const column = buildColumn({ layerId: 'first', indexPattern: expectedIndexPatterns[1], @@ -194,6 +194,17 @@ describe('getOperationTypesForField', () => { expect(column.operationType).toEqual('count'); }); + it('should build a column for the given no-input operation type if it is passed in', () => { + const column = buildColumn({ + layerId: 'first', + indexPattern: expectedIndexPatterns[1], + columns: state.layers.first.columns, + suggestedPriority: 0, + op: 'filters', + }); + expect(column.operationType).toEqual('filters'); + }); + it('should build a column for the given operation type and field if it is passed in', () => { const field = expectedIndexPatterns[1].fields[1]; const column = buildColumn({ @@ -222,7 +233,7 @@ describe('getOperationTypesForField', () => { ); }); - it('should list out all field-operation tuples for different operation meta data', () => { + it('should list out all operation tuples', () => { expect(getAvailableOperationsByMetadata(expectedIndexPatterns[1])).toMatchInlineSnapshot(` Array [ Object { @@ -255,13 +266,17 @@ describe('getOperationTypesForField', () => { }, Object { "operationMetaData": Object { - "dataType": "number", + "dataType": "string", "isBucketed": true, "scale": "ordinal", }, "operations": Array [ Object { - "field": "bytes", + "operationType": "filters", + "type": "none", + }, + Object { + "field": "source", "operationType": "terms", "type": "field", }, @@ -269,13 +284,13 @@ describe('getOperationTypesForField', () => { }, Object { "operationMetaData": Object { - "dataType": "string", + "dataType": "number", "isBucketed": true, "scale": "ordinal", }, "operations": Array [ Object { - "field": "source", + "field": "bytes", "operationType": "terms", "type": "field", }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index 9e5a0f496357d4..46dd73ba849a26 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -63,7 +63,7 @@ export function getOperationTypesForField(field: IndexPatternField): OperationTy return operationDefinitions .filter( (operationDefinition) => - 'getPossibleOperationForField' in operationDefinition && + operationDefinition.input === 'field' && operationDefinition.getPossibleOperationForField(field) ) .sort(getSortScoreByPriority) @@ -80,11 +80,16 @@ export function isDocumentOperation(type: string) { return documentOperations.has(type); } -interface OperationFieldTuple { - type: 'field'; - operationType: OperationType; - field: string; -} +type OperationFieldTuple = + | { + type: 'field'; + operationType: OperationType; + field: string; + } + | { + type: 'none'; + operationType: OperationType; + }; /** * Returns all possible operations (matches between operations and fields of the index @@ -100,11 +105,18 @@ interface OperationFieldTuple { * [ * { * operationMetaData: { dataType: 'string', isBucketed: true }, - * operations: ['terms'] + * operations: [{ + * type: 'field', + * operationType: ['terms'], + * field: 'keyword' + * }] * }, * { - * operationMetaData: { dataType: 'number', isBucketed: false }, - * operations: ['avg', 'min', 'max'] + * operationMetaData: { dataType: 'string', isBucketed: true }, + * operations: [{ + * type: 'none', + * operationType: ['filters'], + * }] * }, * ] * ``` @@ -133,30 +145,31 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { }; operationDefinitions.sort(getSortScoreByPriority).forEach((operationDefinition) => { - indexPattern.fields.forEach((field) => { + if (operationDefinition.input === 'field') { + indexPattern.fields.forEach((field) => { + addToMap( + { + type: 'field', + operationType: operationDefinition.type, + field: field.name, + }, + operationDefinition.getPossibleOperationForField(field) + ); + }); + } else if (operationDefinition.input === 'none') { addToMap( { - type: 'field', + type: 'none', operationType: operationDefinition.type, - field: field.name, }, - getPossibleOperationForField(operationDefinition, field) + operationDefinition.getPossibleOperation() ); - }); + } }); return Object.values(operationByMetadata); } -function getPossibleOperationForField( - operationDefinition: GenericOperationDefinition, - field: IndexPatternField -): OperationMetadata | undefined { - return 'getPossibleOperationForField' in operationDefinition - ? operationDefinition.getPossibleOperationForField(field) - : undefined; -} - /** * Changes the field of the passed in colum. To do so, this method uses the `onFieldChange` function of * the operation definition of the column. Returns a new column object with the field changed. @@ -171,13 +184,13 @@ export function changeField( ) { const operationDefinition = operationDefinitionMap[column.operationType]; - if (!('onFieldChange' in operationDefinition)) { + if (operationDefinition.input === 'field' && 'sourceField' in column) { + return operationDefinition.onFieldChange(column, indexPattern, newField); + } else { throw new Error( "Invariant error: Cannot change field if operation isn't a field based operaiton" ); } - - return operationDefinition.onFieldChange(column, indexPattern, newField); } /** @@ -203,7 +216,7 @@ export function buildColumn({ suggestedPriority: DimensionPriority | undefined; layerId: string; indexPattern: IndexPattern; - field: IndexPatternField; + field?: IndexPatternField; previousColumn?: IndexPatternColumn; }): IndexPatternColumn { const operationDefinition = operationDefinitionMap[op]; @@ -220,16 +233,18 @@ export function buildColumn({ previousColumn, }; + if (operationDefinition.input === 'none') { + return operationDefinition.buildColumn(baseOptions); + } + if (!field) { throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`); } - const newColumn = operationDefinition.buildColumn({ + return operationDefinition.buildColumn({ ...baseOptions, field, }); - - return newColumn; } export { operationDefinitionMap } from './definitions'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts index 51691ae18a99a8..c977a7e0fa3703 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts @@ -5,8 +5,7 @@ */ import _ from 'lodash'; -import { isColumnTransferable } from './operations'; -import { operationDefinitionMap, IndexPatternColumn } from './operations'; +import { isColumnTransferable, operationDefinitionMap, IndexPatternColumn } from './operations'; import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; export function updateColumnParam({ diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 05047fab2517d6..1ed3a0864c2442 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -67,11 +67,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // legend item(s), so we're using a class selector here. expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3); }); + it('should create an xy visualization with filters aggregation', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('lnsXYvis'); await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); await PageObjects.lens.goToTimeRange(); + // Change the IP field to filters await PageObjects.lens.configureDimension({ dimension: 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', operation: 'filters', @@ -79,6 +81,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await PageObjects.lens.addFilterToAgg(`geo.src : CN`); + // Verify that the field was persisted from the transition expect(await PageObjects.lens.getFiltersAggLabels()).to.eql([`ip : *`, `geo.src : CN`]); expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); }); From 97fd0dfbef543d74f972cfd494a56dfbad823238 Mon Sep 17 00:00:00 2001 From: ncheckin <68351161+ncheckin@users.noreply.github.com> Date: Mon, 28 Sep 2020 11:27:27 -0400 Subject: [PATCH 4/8] Update tutorial-define-index.asciidoc (#76975) * Update tutorial-define-index.asciidoc Forgot to update alt text in previous pr. Additionally, it is unclear in the image where the "time field" dropdown is located. * Update docs/getting-started/tutorial-define-index.asciidoc Co-authored-by: Kaarina Tungseth --- docs/getting-started/tutorial-define-index.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/tutorial-define-index.asciidoc b/docs/getting-started/tutorial-define-index.asciidoc index 5a05d565ab3f81..215952c2d35953 100644 --- a/docs/getting-started/tutorial-define-index.asciidoc +++ b/docs/getting-started/tutorial-define-index.asciidoc @@ -45,7 +45,7 @@ contains the time series data. . From the *Time field* dropdown, select *@timestamp, then click *Create index pattern*. + [role="screenshot"] -image::images/tutorial_index_patterns.png[All tutorial index patterns] +image::images/tutorial_index_patterns.png[Image showing how to create an index pattern] NOTE: When you define an index pattern, the indices that match that pattern must exist in Elasticsearch and they must contain data. To check if the indices are From 689e1e32f17a53fa004c6c7fc147b73e17e6b2c2 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Mon, 28 Sep 2020 18:33:48 +0300 Subject: [PATCH 5/8] [Search] Error notification alignment (#77788) * OSS error alignemnt * Adjust error messages in xpack * Add getErrorMessage * Use showError in vizualize Add original error to expression exception * Cleanup * ts, doc and i18n fixes * Fix jest tests * Fix functional test * functional test * ts * Update functional tests * Add unit tests to interceptor and timeout error * expose toasts test function * doc * typos * review 1 * Code review * doc * doc fix * visualization type fix * fix jest * Fix xpack functional test * fix xpack test * code review * delete debubg flag * Update texts by @gchaps * docs and ts Co-authored-by: Elastic Machine --- ...plugin-plugins-data-public.isearchstart.md | 1 + ...gins-data-public.isearchstart.showerror.md | 11 ++ .../kibana-plugin-plugins-data-public.md | 4 +- ...data-public.painlesserror._constructor_.md | 21 ++ ...ta-public.painlesserror.geterrormessage.md | 22 +++ ...lugin-plugins-data-public.painlesserror.md | 30 +++ ...data-public.painlesserror.painlessstack.md | 11 ++ ...ublic.requesttimeouterror._constructor_.md | 20 -- ...plugins-data-public.requesttimeouterror.md | 20 -- ...public.searchinterceptor.gettimeoutmode.md | 15 ++ ...lic.searchinterceptor.handlesearcherror.md | 25 +++ ...n-plugins-data-public.searchinterceptor.md | 4 +- ...ns-data-public.searchinterceptor.search.md | 6 +- ...ata-public.searchinterceptor.showerror.md} | 17 +- ...public.searchtimeouterror._constructor_.md | 21 ++ ...blic.searchtimeouterror.geterrormessage.md | 22 +++ ...-plugins-data-public.searchtimeouterror.md | 32 +++ ...ins-data-public.searchtimeouterror.mode.md | 11 ++ ...in-plugins-data-public.timeouterrormode.md | 20 ++ .../search_examples/server/my_strategy.ts | 2 +- src/plugins/data/public/index.ts | 7 +- src/plugins/data/public/public.api.md | 92 ++++++--- .../public/search/errors}/index.ts | 3 +- .../public/search/errors/painless_error.tsx | 89 +++++++++ .../search/errors/timeout_error.test.tsx | 62 ++++++ .../public/search/errors/timeout_error.tsx | 111 +++++++++++ .../public/search/errors/types.ts} | 47 +---- src/plugins/data/public/search/index.ts | 2 +- src/plugins/data/public/search/mocks.ts | 1 + .../public/search/request_timeout_error.ts | 30 --- .../public/search/search_interceptor.test.ts | 114 ++++++----- .../data/public/search/search_interceptor.ts | 187 +++++++++++------- .../data/public/search/search_service.ts | 3 + src/plugins/data/public/search/types.ts | 2 + .../public/application/angular/discover.js | 28 +-- .../components/discover_legacy.tsx | 6 +- .../components/fetch_error/fetch_error.scss | 3 - .../components/fetch_error/fetch_error.tsx | 96 --------- .../common/expression_types/specs/error.ts | 1 + .../expressions/common/util/create_error.ts | 3 +- .../utils/get_visualization_instance.test.ts | 4 +- .../utils/get_visualization_instance.ts | 19 +- test/functional/apps/discover/_errors.ts | 7 +- test/functional/services/toasts.ts | 2 +- .../public/search/search_interceptor.test.ts | 5 +- .../public/search/search_interceptor.ts | 54 ++--- .../components/alerts_table/actions.test.tsx | 1 + .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - .../apps/discover/error_handling.ts | 8 +- 50 files changed, 849 insertions(+), 465 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.showerror.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.painlessstack.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md => kibana-plugin-plugins-data-public.searchinterceptor.showerror.md} (53%) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.mode.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timeouterrormode.md rename src/plugins/{discover/public/application/components/fetch_error => data/public/search/errors}/index.ts (92%) create mode 100644 src/plugins/data/public/search/errors/painless_error.tsx create mode 100644 src/plugins/data/public/search/errors/timeout_error.test.tsx create mode 100644 src/plugins/data/public/search/errors/timeout_error.tsx rename src/plugins/{discover/public/application/angular/get_painless_error.ts => data/public/search/errors/types.ts} (61%) delete mode 100644 src/plugins/data/public/search/request_timeout_error.ts delete mode 100644 src/plugins/discover/public/application/components/fetch_error/fetch_error.scss delete mode 100644 src/plugins/discover/public/application/components/fetch_error/fetch_error.tsx diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md index cee213fc6e7e3c..5defe4a6476142 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -19,4 +19,5 @@ export interface ISearchStart | [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | AggsStart | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | | [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | +| [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.showerror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.showerror.md new file mode 100644 index 00000000000000..fb14057d83d5c2 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.showerror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) > [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) + +## ISearchStart.showError property + +Signature: + +```typescript +showError: (e: Error) => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 0f45b5a727676e..e5f56a1ec387fb 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -19,10 +19,11 @@ | [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) | | | [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-public.optionedparamtype.md) | | +| [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) | | | [Plugin](./kibana-plugin-plugins-data-public.plugin.md) | | -| [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) | Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors. | | [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) | | | [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) | \* | +| [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) | Request Failure - When an entire multi request fails | | [TimeHistory](./kibana-plugin-plugins-data-public.timehistory.md) | | ## Enumerations @@ -35,6 +36,7 @@ | [METRIC\_TYPES](./kibana-plugin-plugins-data-public.metric_types.md) | | | [QuerySuggestionTypes](./kibana-plugin-plugins-data-public.querysuggestiontypes.md) | | | [SortDirection](./kibana-plugin-plugins-data-public.sortdirection.md) | | +| [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md) | | ## Functions diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md new file mode 100644 index 00000000000000..f8966572afbb66 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) > [(constructor)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md) + +## PainlessError.(constructor) + +Constructs a new instance of the `PainlessError` class + +Signature: + +```typescript +constructor(err: EsError, request: IKibanaSearchRequest); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| err | EsError | | +| request | IKibanaSearchRequest | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md new file mode 100644 index 00000000000000..a3b4c51c6c3313 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) > [getErrorMessage](./kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md) + +## PainlessError.getErrorMessage() method + +Signature: + +```typescript +getErrorMessage(application: ApplicationStart): JSX.Element; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| application | ApplicationStart | | + +Returns: + +`JSX.Element` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md new file mode 100644 index 00000000000000..306211cd602599 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) + +## PainlessError class + +Signature: + +```typescript +export declare class PainlessError extends KbnError +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(err, request)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md) | | Constructs a new instance of the PainlessError class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [painlessStack](./kibana-plugin-plugins-data-public.painlesserror.painlessstack.md) | | string | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [getErrorMessage(application)](./kibana-plugin-plugins-data-public.painlesserror.geterrormessage.md) | | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.painlessstack.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.painlessstack.md new file mode 100644 index 00000000000000..a7e6920b2ae219 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.painlessstack.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) > [painlessStack](./kibana-plugin-plugins-data-public.painlesserror.painlessstack.md) + +## PainlessError.painlessStack property + +Signature: + +```typescript +painlessStack?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md deleted file mode 100644 index 25e472817b46d2..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) > [(constructor)](./kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md) - -## RequestTimeoutError.(constructor) - -Constructs a new instance of the `RequestTimeoutError` class - -Signature: - -```typescript -constructor(message?: string); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| message | string | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror.md deleted file mode 100644 index 84b2fc3fe0b177..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.requesttimeouterror.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) - -## RequestTimeoutError class - -Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors. - -Signature: - -```typescript -export declare class RequestTimeoutError extends Error -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(message)](./kibana-plugin-plugins-data-public.requesttimeouterror._constructor_.md) | | Constructs a new instance of the RequestTimeoutError class | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md new file mode 100644 index 00000000000000..8ecd8b8c5ac226 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [getTimeoutMode](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md) + +## SearchInterceptor.getTimeoutMode() method + +Signature: + +```typescript +protected getTimeoutMode(): TimeoutErrorMode; +``` +Returns: + +`TimeoutErrorMode` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md new file mode 100644 index 00000000000000..02db74b1a9e914 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [handleSearchError](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) + +## SearchInterceptor.handleSearchError() method + +Signature: + +```typescript +protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| e | any | | +| request | IKibanaSearchRequest | | +| timeoutSignal | AbortSignal | | +| appAbortSignal | AbortSignal | | + +Returns: + +`Error` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md index 5cee345db6cd2a..a02a6116d7ae00 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md @@ -21,11 +21,13 @@ export declare class SearchInterceptor | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [deps](./kibana-plugin-plugins-data-public.searchinterceptor.deps.md) | | SearchInterceptorDeps | | -| [showTimeoutError](./kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md) | | ((e: Error) => void) & import("lodash").Cancelable | | ## Methods | Method | Modifiers | Description | | --- | --- | --- | +| [getTimeoutMode()](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md) | | | +| [handleSearchError(e, request, timeoutSignal, appAbortSignal)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | | | [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates pendingCount$ when the request is started/finalized. | +| [showError(e)](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md index 1a71b5808f485b..672ff5065c456c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md @@ -9,17 +9,19 @@ Searches using the given `search` method. Overrides the `AbortSignal` with one t Signature: ```typescript -search(request: IEsSearchRequest, options?: ISearchOptions): Observable; +search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| request | IEsSearchRequest | | +| request | IKibanaSearchRequest | | | options | ISearchOptions | | Returns: `Observable` +`Observalbe` emitting the search response or an error. + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showerror.md similarity index 53% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showerror.md index 91ecb2821acbfb..92e851c783dd06 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showerror.md @@ -1,11 +1,22 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [showTimeoutError](./kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [showError](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md) -## SearchInterceptor.showTimeoutError property +## SearchInterceptor.showError() method Signature: ```typescript -protected showTimeoutError: ((e: Error) => void) & import("lodash").Cancelable; +showError(e: Error): void; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| e | Error | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md new file mode 100644 index 00000000000000..1c6370c7d03561 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) > [(constructor)](./kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md) + +## SearchTimeoutError.(constructor) + +Constructs a new instance of the `SearchTimeoutError` class + +Signature: + +```typescript +constructor(err: Error, mode: TimeoutErrorMode); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| err | Error | | +| mode | TimeoutErrorMode | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md new file mode 100644 index 00000000000000..58ef953c9d7dbd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) > [getErrorMessage](./kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md) + +## SearchTimeoutError.getErrorMessage() method + +Signature: + +```typescript +getErrorMessage(application: ApplicationStart): JSX.Element; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| application | ApplicationStart | | + +Returns: + +`JSX.Element` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.md new file mode 100644 index 00000000000000..5c0bec04dcfbc1 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.md @@ -0,0 +1,32 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) + +## SearchTimeoutError class + +Request Failure - When an entire multi request fails + +Signature: + +```typescript +export declare class SearchTimeoutError extends KbnError +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(err, mode)](./kibana-plugin-plugins-data-public.searchtimeouterror._constructor_.md) | | Constructs a new instance of the SearchTimeoutError class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [mode](./kibana-plugin-plugins-data-public.searchtimeouterror.mode.md) | | TimeoutErrorMode | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [getErrorMessage(application)](./kibana-plugin-plugins-data-public.searchtimeouterror.geterrormessage.md) | | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.mode.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.mode.md new file mode 100644 index 00000000000000..d534a73eca2ec2 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchtimeouterror.mode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchTimeoutError](./kibana-plugin-plugins-data-public.searchtimeouterror.md) > [mode](./kibana-plugin-plugins-data-public.searchtimeouterror.mode.md) + +## SearchTimeoutError.mode property + +Signature: + +```typescript +mode: TimeoutErrorMode; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timeouterrormode.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timeouterrormode.md new file mode 100644 index 00000000000000..8ad63e2c1e9b4f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timeouterrormode.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md) + +## TimeoutErrorMode enum + +Signature: + +```typescript +export declare enum TimeoutErrorMode +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| CHANGE | 2 | | +| CONTACT | 1 | | +| UPGRADE | 0 | | + diff --git a/examples/search_examples/server/my_strategy.ts b/examples/search_examples/server/my_strategy.ts index 1f59d0a5d8f3a6..169982544e6e81 100644 --- a/examples/search_examples/server/my_strategy.ts +++ b/examples/search_examples/server/my_strategy.ts @@ -25,7 +25,7 @@ export const mySearchStrategyProvider = ( ): ISearchStrategy => { const es = data.search.getSearchStrategy('es'); return { - search: async (context, request, options) => { + search: async (context, request, options): Promise => { const esSearchRes = await es.search(context, request, options); return { ...esSearchRes, diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index f7dceffa9fdbc3..0e21f6f6955514 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -365,8 +365,6 @@ export { ISearchGeneric, ISearchSource, parseSearchSourceJSON, - RequestTimeoutError, - SearchError, SearchInterceptor, SearchInterceptorDeps, SearchRequest, @@ -375,6 +373,11 @@ export { // expression functions and types EsdslExpressionFunctionDefinition, EsRawResponseExpressionTypeDefinition, + // errors + SearchError, + SearchTimeoutError, + TimeoutErrorMode, + PainlessError, } from './search'; export type { SearchSource } from './search'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 9c059c0f358927..1ee453a0f14115 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -8,6 +8,7 @@ import { $Values } from '@kbn/utility-types'; import _ from 'lodash'; import { Action } from 'history'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; +import { ApplicationStart } from 'kibana/public'; import { Assign } from '@kbn/utility-types'; import { BehaviorSubject } from 'rxjs'; import Boom from 'boom'; @@ -69,7 +70,6 @@ import { SavedObjectsClientContract } from 'src/core/public'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; -import { Subscription } from 'rxjs'; import { ToastInputFields } from 'src/core/public/notifications'; import { ToastsSetup } from 'kibana/public'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; @@ -1462,6 +1462,8 @@ export interface ISearchStart { aggs: AggsStart; search: ISearchGeneric; searchSource: ISearchStartSearchSource; + // (undocumented) + showError: (e: Error) => void; } // @public @@ -1628,6 +1630,19 @@ export interface OptionedValueProp { value: string; } +// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "PainlessError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class PainlessError extends KbnError { + // Warning: (ae-forgotten-export) The symbol "EsError" needs to be exported by the entry point index.d.ts + constructor(err: EsError, request: IKibanaSearchRequest); + // (undocumented) + getErrorMessage(application: ApplicationStart): JSX.Element; + // (undocumented) + painlessStack?: string; +} + // Warning: (ae-forgotten-export) The symbol "parseEsInterval" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ParsedInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1893,13 +1908,6 @@ export interface RefreshInterval { value: number; } -// Warning: (ae-missing-release-tag) "RequestTimeoutError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export class RequestTimeoutError extends Error { - constructor(message?: string); -} - // Warning: (ae-missing-release-tag) "SavedQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2023,24 +2031,27 @@ export class SearchInterceptor { protected application: CoreStart['application']; // (undocumented) protected readonly deps: SearchInterceptorDeps; + // (undocumented) + protected getTimeoutMode(): TimeoutErrorMode; + // (undocumented) + protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error; // @internal protected pendingCount$: BehaviorSubject; // @internal (undocumented) - protected runSearch(request: IEsSearchRequest, signal: AbortSignal, strategy?: string): Observable; - search(request: IEsSearchRequest, options?: ISearchOptions): Observable; + protected runSearch(request: IKibanaSearchRequest, signal: AbortSignal, strategy?: string): Observable; + search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable; // @internal (undocumented) protected setupAbortSignal({ abortSignal, timeout, }: { abortSignal?: AbortSignal; timeout?: number; }): { combinedSignal: AbortSignal; + timeoutSignal: AbortSignal; cleanup: () => void; }; // (undocumented) - protected showTimeoutError: ((e: Error) => void) & import("lodash").Cancelable; - // @internal - protected timeoutSubscriptions: Subscription; -} + showError(e: Error): void; + } // Warning: (ae-missing-release-tag) "SearchInterceptorDeps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2153,6 +2164,17 @@ export interface SearchSourceFields { version?: boolean; } +// Warning: (ae-missing-release-tag) "SearchTimeoutError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class SearchTimeoutError extends KbnError { + constructor(err: Error, mode: TimeoutErrorMode); + // (undocumented) + getErrorMessage(application: ApplicationStart): JSX.Element; + // (undocumented) + mode: TimeoutErrorMode; + } + // Warning: (ae-missing-release-tag) "SortDirection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2225,6 +2247,18 @@ export class TimeHistory { // @public (undocumented) export type TimeHistoryContract = PublicMethodsOf; +// Warning: (ae-missing-release-tag) "TimeoutErrorMode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export enum TimeoutErrorMode { + // (undocumented) + CHANGE = 2, + // (undocumented) + CONTACT = 1, + // (undocumented) + UPGRADE = 0 +} + // Warning: (ae-missing-release-tag) "TimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2314,21 +2348,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/discover/public/application/components/fetch_error/index.ts b/src/plugins/data/public/search/errors/index.ts similarity index 92% rename from src/plugins/discover/public/application/components/fetch_error/index.ts rename to src/plugins/data/public/search/errors/index.ts index 0206bc48257ac3..6082e758a8badb 100644 --- a/src/plugins/discover/public/application/components/fetch_error/index.ts +++ b/src/plugins/data/public/search/errors/index.ts @@ -17,4 +17,5 @@ * under the License. */ -import './fetch_error'; +export * from './painless_error'; +export * from './timeout_error'; diff --git a/src/plugins/data/public/search/errors/painless_error.tsx b/src/plugins/data/public/search/errors/painless_error.tsx new file mode 100644 index 00000000000000..244f205469a2f4 --- /dev/null +++ b/src/plugins/data/public/search/errors/painless_error.tsx @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiSpacer, EuiText, EuiCodeBlock } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ApplicationStart } from 'kibana/public'; +import { KbnError } from '../../../../kibana_utils/common'; +import { EsError, isEsError } from './types'; +import { IKibanaSearchRequest } from '..'; + +export class PainlessError extends KbnError { + painlessStack?: string; + constructor(err: EsError, request: IKibanaSearchRequest) { + const rootCause = getRootCause(err as EsError); + + super( + i18n.translate('data.painlessError.painlessScriptedFieldErrorMessage', { + defaultMessage: "Error executing Painless script: '{script}'.", + values: { script: rootCause?.script }, + }) + ); + this.painlessStack = rootCause?.script_stack ? rootCause?.script_stack.join('\n') : undefined; + } + + public getErrorMessage(application: ApplicationStart) { + function onClick() { + application.navigateToApp('management', { + path: `/kibana/indexPatterns`, + }); + } + + return ( + <> + {this.message} + + + {this.painlessStack ? ( + + {this.painlessStack} + + ) : null} + + + + + + + ); + } +} + +function getFailedShards(err: EsError) { + const failedShards = + err.body?.attributes?.error?.failed_shards || + err.body?.attributes?.error?.caused_by?.failed_shards; + return failedShards ? failedShards[0] : undefined; +} + +function getRootCause(err: EsError) { + return getFailedShards(err)?.reason; +} + +export function isPainlessError(err: Error | EsError) { + if (!isEsError(err)) return false; + + const rootCause = getRootCause(err as EsError); + if (!rootCause) return false; + + const { lang } = rootCause; + return lang === 'painless'; +} diff --git a/src/plugins/data/public/search/errors/timeout_error.test.tsx b/src/plugins/data/public/search/errors/timeout_error.test.tsx new file mode 100644 index 00000000000000..87b491b976ebc1 --- /dev/null +++ b/src/plugins/data/public/search/errors/timeout_error.test.tsx @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SearchTimeoutError, TimeoutErrorMode } from './timeout_error'; + +import { coreMock } from '../../../../../core/public/mocks'; +const startMock = coreMock.createStart(); + +import { mount } from 'enzyme'; +import { AbortError } from 'src/plugins/data/common'; + +describe('SearchTimeoutError', () => { + beforeEach(() => { + jest.clearAllMocks(); + startMock.application.navigateToApp.mockImplementation(jest.fn()); + }); + + it('Should navigate to upgrade', () => { + const e = new SearchTimeoutError(new AbortError(), TimeoutErrorMode.UPGRADE); + const component = mount(e.getErrorMessage(startMock.application)); + + expect(component.find('EuiButton').length).toBe(1); + component.find('EuiButton').simulate('click'); + expect(startMock.application.navigateToApp).toHaveBeenCalledWith('management', { + path: '/kibana/indexPatterns', + }); + }); + + it('Should create contact admin message', () => { + const e = new SearchTimeoutError(new AbortError(), TimeoutErrorMode.CONTACT); + const component = mount(e.getErrorMessage(startMock.application)); + + expect(component.find('EuiButton').length).toBe(0); + }); + + it('Should navigate to settings', () => { + const e = new SearchTimeoutError(new AbortError(), TimeoutErrorMode.CHANGE); + const component = mount(e.getErrorMessage(startMock.application)); + + expect(component.find('EuiButton').length).toBe(1); + component.find('EuiButton').simulate('click'); + expect(startMock.application.navigateToApp).toHaveBeenCalledWith('management', { + path: '/kibana/settings', + }); + }); +}); diff --git a/src/plugins/data/public/search/errors/timeout_error.tsx b/src/plugins/data/public/search/errors/timeout_error.tsx new file mode 100644 index 00000000000000..56aecb42f5326e --- /dev/null +++ b/src/plugins/data/public/search/errors/timeout_error.tsx @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; +import { ApplicationStart } from 'kibana/public'; +import { KbnError } from '../../../../kibana_utils/common'; + +export enum TimeoutErrorMode { + UPGRADE, + CONTACT, + CHANGE, +} + +/** + * Request Failure - When an entire multi request fails + * @param {Error} err - the Error that came back + */ +export class SearchTimeoutError extends KbnError { + public mode: TimeoutErrorMode; + constructor(err: Error, mode: TimeoutErrorMode) { + super(`Request timeout: ${JSON.stringify(err?.message)}`); + this.mode = mode; + } + + private getMessage() { + switch (this.mode) { + case TimeoutErrorMode.UPGRADE: + return i18n.translate('data.search.upgradeLicense', { + defaultMessage: + 'Your query has timed out. With our free Basic tier, your queries never time out.', + }); + case TimeoutErrorMode.CONTACT: + return i18n.translate('data.search.timeoutContactAdmin', { + defaultMessage: + 'Your query has timed out. Contact your system administrator to increase the run time.', + }); + case TimeoutErrorMode.CHANGE: + return i18n.translate('data.search.timeoutIncreaseSetting', { + defaultMessage: + 'Your query has timed out. Increase run time with the search timeout advanced setting.', + }); + } + } + + private getActionText() { + switch (this.mode) { + case TimeoutErrorMode.UPGRADE: + return i18n.translate('data.search.upgradeLicenseActionText', { + defaultMessage: 'Upgrade now', + }); + break; + case TimeoutErrorMode.CHANGE: + return i18n.translate('data.search.timeoutIncreaseSettingActionText', { + defaultMessage: 'Edit setting', + }); + break; + } + } + + private onClick(application: ApplicationStart) { + switch (this.mode) { + case TimeoutErrorMode.UPGRADE: + application.navigateToApp('management', { + path: `/kibana/indexPatterns`, + }); + break; + case TimeoutErrorMode.CHANGE: + application.navigateToApp('management', { + path: `/kibana/settings`, + }); + break; + } + } + + public getErrorMessage(application: ApplicationStart) { + const actionText = this.getActionText(); + return ( + <> + {this.getMessage()} + {actionText && ( + <> + + + this.onClick(application)} size="s"> + {actionText} + + + + )} + + ); + } +} diff --git a/src/plugins/discover/public/application/angular/get_painless_error.ts b/src/plugins/data/public/search/errors/types.ts similarity index 61% rename from src/plugins/discover/public/application/angular/get_painless_error.ts rename to src/plugins/data/public/search/errors/types.ts index 162dacd3ac3b7c..4182209eb68a5a 100644 --- a/src/plugins/discover/public/application/angular/get_painless_error.ts +++ b/src/plugins/data/public/search/errors/types.ts @@ -17,9 +17,7 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; - -interface FailedShards { +interface FailedShard { shard: number; index: string; node: string; @@ -41,7 +39,7 @@ interface FailedShards { }; } -interface EsError { +export interface EsError { body: { statusCode: number; error: string; @@ -56,51 +54,20 @@ interface EsError { ]; type: string; reason: string; + failed_shards: FailedShard[]; caused_by: { type: string; reason: string; phase: string; grouped: boolean; - failed_shards: FailedShards[]; + failed_shards: FailedShard[]; + script_stack: string[]; }; }; }; }; } -export function getCause(error: EsError) { - const cause = error.body?.attributes?.error?.root_cause; - if (cause) { - return cause[0]; - } - - const failedShards = error.body?.attributes?.error?.caused_by?.failed_shards; - - if (failedShards && failedShards[0] && failedShards[0].reason) { - return error.body?.attributes?.error?.caused_by?.failed_shards[0].reason; - } -} - -export function getPainlessError(error: EsError) { - const cause = getCause(error); - - if (!cause) { - return; - } - - const { lang, script } = cause; - - if (lang !== 'painless') { - return; - } - - return { - lang, - script, - message: i18n.translate('discover.painlessError.painlessScriptedFieldErrorMessage', { - defaultMessage: "Error with Painless scripted field '{script}'.", - values: { script }, - }), - error: error.body?.message, - }; +export function isEsError(e: any): e is EsError { + return !!e.body?.attributes; } diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index fc3d71936a8594..86804a819cb0e5 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -46,4 +46,4 @@ export { export { getEsPreference } from './es_search'; export { SearchInterceptor, SearchInterceptorDeps } from './search_interceptor'; -export { RequestTimeoutError } from './request_timeout_error'; +export * from './errors'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index fdd6a900134133..e931b39eae2a58 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -34,6 +34,7 @@ function createStartContract(): jest.Mocked { return { aggs: searchAggsStartMock(), search: jest.fn(), + showError: jest.fn(), searchSource: searchSourceMock, }; } diff --git a/src/plugins/data/public/search/request_timeout_error.ts b/src/plugins/data/public/search/request_timeout_error.ts deleted file mode 100644 index 92894deb4f0ffa..00000000000000 --- a/src/plugins/data/public/search/request_timeout_error.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Class used to signify that a request timed out. Useful for applications to conditionally handle - * this type of error differently than other errors. - */ -export class RequestTimeoutError extends Error { - constructor(message = 'Request timed out') { - super(message); - this.message = message; - this.name = 'RequestTimeoutError'; - } -} diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index 7bfa6f0ab1bc57..ade15adc1c3a3f 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -22,6 +22,7 @@ import { coreMock } from '../../../../core/public/mocks'; import { IEsSearchRequest } from '../../common/search'; import { SearchInterceptor } from './search_interceptor'; import { AbortError } from '../../common'; +import { SearchTimeoutError, PainlessError } from './errors'; let searchInterceptor: SearchInterceptor; let mockCoreSetup: MockedKeys; @@ -53,8 +54,8 @@ describe('SearchInterceptor', () => { expect(result).toBe(mockResponse); }); - test('Observable should fail if fetch has an error', async () => { - const mockResponse: any = { result: 500 }; + test('Observable should fail if fetch has an internal error', async () => { + const mockResponse: any = { result: 500, message: 'Internal Error' }; mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, @@ -68,64 +69,83 @@ describe('SearchInterceptor', () => { } }); - test('Observable should fail if fetch times out (test merged signal)', async () => { - mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { - return new Promise((resolve, reject) => { - options.signal.addEventListener('abort', () => { - reject(new AbortError()); - }); - - setTimeout(resolve, 5000); - }); - }); + test('Should throw SearchTimeoutError on server timeout AND show toast', async (done) => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, }; const response = searchInterceptor.search(mockRequest); - const next = jest.fn(); - const error = (e: any) => { - expect(next).not.toBeCalled(); - expect(e).toBeInstanceOf(AbortError); - }; - response.subscribe({ next, error }); - - jest.advanceTimersByTime(5000); - - await flushPromises(); + try { + await response.toPromise(); + } catch (e) { + expect(e).toBeInstanceOf(SearchTimeoutError); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); + done(); + } }); - test('Should not timeout if requestTimeout is undefined', async () => { - searchInterceptor = new SearchInterceptor({ - startServices: mockCoreSetup.getStartServices(), - uiSettings: mockCoreSetup.uiSettings, - http: mockCoreSetup.http, - toasts: mockCoreSetup.notifications.toasts, - }); - mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { - return new Promise((resolve, reject) => { - options.signal.addEventListener('abort', () => { - reject(new AbortError()); - }); - - setTimeout(resolve, 5000); - }); - }); + test('Search error should be debounced', async (done) => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, }; - const response = searchInterceptor.search(mockRequest); + try { + await searchInterceptor.search(mockRequest).toPromise(); + } catch (e) { + expect(e).toBeInstanceOf(SearchTimeoutError); + try { + await searchInterceptor.search(mockRequest).toPromise(); + } catch (e2) { + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); + done(); + } + } + }); - expect.assertions(1); - const next = jest.fn(); - const complete = () => { - expect(next).toBeCalled(); + test('Should throw Painless error on server error with OSS format', async (done) => { + const mockResponse: any = { + result: 500, + body: { + attributes: { + error: { + failed_shards: [ + { + reason: { + lang: 'painless', + script_stack: ['a', 'b'], + reason: 'banana', + }, + }, + ], + }, + }, + }, }; - response.subscribe({ next, complete }); - - jest.advanceTimersByTime(5000); + mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); - await flushPromises(); + try { + await response.toPromise(); + } catch (e) { + expect(e).toBeInstanceOf(PainlessError); + done(); + } }); test('Observable should fail if user aborts (test merged signal)', async () => { diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 802ee6db9433e9..2e42635a7f811c 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -17,29 +17,21 @@ * under the License. */ -import { trimEnd, debounce } from 'lodash'; -import { - BehaviorSubject, - throwError, - timer, - Subscription, - defer, - from, - Observable, - NEVER, -} from 'rxjs'; +import { get, trimEnd, debounce } from 'lodash'; +import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; -import { i18n } from '@kbn/i18n'; import { getCombinedSignal, AbortError, - IEsSearchRequest, + IKibanaSearchRequest, IKibanaSearchResponse, ISearchOptions, ES_SEARCH_STRATEGY, } from '../../common'; import { SearchUsageCollector } from './collectors'; +import { SearchTimeoutError, PainlessError, isPainlessError, TimeoutErrorMode } from './errors'; +import { toMountPoint } from '../../../kibana_react/public'; export interface SearchInterceptorDeps { http: CoreSetup['http']; @@ -62,12 +54,6 @@ export class SearchInterceptor { */ protected pendingCount$ = new BehaviorSubject(0); - /** - * The subscriptions from scheduling the automatic timeout for each request. - * @internal - */ - protected timeoutSubscriptions: Subscription = new Subscription(); - /** * @internal */ @@ -84,11 +70,46 @@ export class SearchInterceptor { }); } + /* + * @returns `TimeoutErrorMode` indicating what action should be taken in case of a request timeout based on license and permissions. + * @internal + */ + protected getTimeoutMode() { + return TimeoutErrorMode.UPGRADE; + } + + /* + * @returns `Error` a search service specific error or the original error, if a specific error can't be recognized. + * @internal + */ + protected handleSearchError( + e: any, + request: IKibanaSearchRequest, + timeoutSignal: AbortSignal, + appAbortSignal?: AbortSignal + ): Error { + if (timeoutSignal.aborted || get(e, 'body.message') === 'Request timed out') { + // Handle a client or a server side timeout + const err = new SearchTimeoutError(e, this.getTimeoutMode()); + + // Show the timeout error here, so that it's shown regardless of how an application chooses to handle errors. + this.showTimeoutError(err); + return err; + } else if (appAbortSignal?.aborted) { + // In the case an application initiated abort, throw the existing AbortError. + return e; + } else if (isPainlessError(e)) { + return new PainlessError(e, request); + } else { + return e; + } + } + /** * @internal */ protected runSearch( - request: IEsSearchRequest, + request: IKibanaSearchRequest, signal: AbortSignal, strategy?: string ): Observable { @@ -105,41 +126,6 @@ export class SearchInterceptor { ); } - /** - * Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort - * either when `cancelPending` is called, when the request times out, or when the original - * `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized. - */ - public search( - request: IEsSearchRequest, - options?: ISearchOptions - ): Observable { - // Defer the following logic until `subscribe` is actually called - return defer(() => { - if (options?.abortSignal?.aborted) { - return throwError(new AbortError()); - } - - const { combinedSignal, cleanup } = this.setupAbortSignal({ - abortSignal: options?.abortSignal, - }); - this.pendingCount$.next(this.pendingCount$.getValue() + 1); - - return this.runSearch(request, combinedSignal, options?.strategy).pipe( - catchError((e: any) => { - if (e.body?.attributes?.error === 'Request timed out') { - this.showTimeoutError(e); - } - return throwError(e); - }), - finalize(() => { - this.pendingCount$.next(this.pendingCount$.getValue() - 1); - cleanup(); - }) - ); - }); - } - /** * @internal */ @@ -156,9 +142,7 @@ export class SearchInterceptor { const timeout$ = timeout ? timer(timeout) : NEVER; const subscription = timeout$.subscribe(() => { timeoutController.abort(); - this.showTimeoutError(new AbortError()); }); - this.timeoutSubscriptions.add(subscription); // Get a combined `AbortSignal` that will be aborted whenever the first of the following occurs: // 1. The user manually aborts (via `cancelPending`) @@ -172,34 +156,95 @@ export class SearchInterceptor { const combinedSignal = getCombinedSignal(signals); const cleanup = () => { - this.timeoutSubscriptions.remove(subscription); + subscription.unsubscribe(); }; combinedSignal.addEventListener('abort', cleanup); return { combinedSignal, + timeoutSignal, cleanup, }; } - // Right now we are debouncing but we will hook this up with background sessions to show only one - // error notification per session. - protected showTimeoutError = debounce( - (e: Error) => { - this.deps.toasts.addError(e, { + /** + * Right now we are throttling but we will hook this up with background sessions to show only one + * error notification per session. + * @internal + */ + private showTimeoutError = debounce( + (e: SearchTimeoutError) => { + this.deps.toasts.addDanger({ title: 'Timed out', - toastMessage: i18n.translate('data.search.upgradeLicense', { - defaultMessage: - 'One or more queries timed out. With our free Basic tier, your queries never time out.', - }), + text: toMountPoint(e.getErrorMessage(this.application)), }); }, - 60000, - { - leading: true, - } + 30000, + { leading: true, trailing: false } ); + + /** + * Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort + * either when `cancelPending` is called, when the request times out, or when the original + * `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized. + * + * @param request + * @options + * @returns `Observalbe` emitting the search response or an error. + */ + public search( + request: IKibanaSearchRequest, + options?: ISearchOptions + ): Observable { + // Defer the following logic until `subscribe` is actually called + return defer(() => { + if (options?.abortSignal?.aborted) { + return throwError(new AbortError()); + } + + const { timeoutSignal, combinedSignal, cleanup } = this.setupAbortSignal({ + abortSignal: options?.abortSignal, + }); + this.pendingCount$.next(this.pendingCount$.getValue() + 1); + + return this.runSearch(request, combinedSignal, options?.strategy).pipe( + catchError((e: any) => { + return throwError( + this.handleSearchError(e, request, timeoutSignal, options?.abortSignal) + ); + }), + finalize(() => { + this.pendingCount$.next(this.pendingCount$.getValue() - 1); + cleanup(); + }) + ); + }); + } + + /* + * + */ + public showError(e: Error) { + if (e instanceof AbortError) return; + + if (e instanceof SearchTimeoutError) { + // The SearchTimeoutError is shown by the interceptor in getSearchError (regardless of how the app chooses to handle errors) + return; + } + + if (e instanceof PainlessError) { + this.deps.toasts.addDanger({ + title: 'Search Error', + text: toMountPoint(e.getErrorMessage(this.application)), + }); + return; + } + + this.deps.toasts.addError(e, { + title: 'Search Error', + }); + } } export type ISearchInterceptor = PublicMethodsOf; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index d8937ed30e401b..173baba5cab6fb 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -111,6 +111,9 @@ export class SearchService implements Plugin { return { aggs: this.aggsService.start({ fieldFormats, uiSettings }), search, + showError: (e: Error) => { + this.searchInterceptor.showError(e); + }, searchSource: { /** * creates searchsource based on serialized search source fields diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 6ae5d83499aa6b..a133a8cd4be93b 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -73,6 +73,8 @@ export interface ISearchStart { * {@link ISearchGeneric} */ search: ISearchGeneric; + + showError: (e: Error) => void; /** * high level search * {@link ISearchStartSearchSource} diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 7871cc4b16464e..a396033e5dedb3 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -27,6 +27,12 @@ import { i18n } from '@kbn/i18n'; import { getState, splitState } from './discover_state'; import { RequestAdapter } from '../../../../inspector/public'; +import { + esFilters, + indexPatterns as indexPatternsUtils, + connectToQueryState, + syncQueryStateWithUrl, +} from '../../../../data/public'; import { SavedObjectSaveModal, showSaveModal } from '../../../../saved_objects/public'; import { getSortArray, getSortForSearchSource } from './doc_table'; import { createFixedScroll } from './directives/fixed_scroll'; @@ -34,7 +40,6 @@ import * as columnActions from './doc_table/actions/columns'; import indexTemplateLegacy from './discover_legacy.html'; import { showOpenSearchPanel } from '../components/top_nav/show_open_search_panel'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; -import { getPainlessError } from './get_painless_error'; import { discoverResponseHandler } from './response_handler'; import { getRequestInspectorStats, @@ -65,12 +70,7 @@ const { import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; import { validateTimeRange } from '../helpers/validate_time_range'; -import { - esFilters, - indexPatterns as indexPatternsUtils, - connectToQueryState, - syncQueryStateWithUrl, -} from '../../../../data/public'; + import { getIndexPatternId } from '../helpers/get_index_pattern_id'; import { addFatalError } from '../../../../kibana_legacy/public'; import { @@ -786,18 +786,10 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise // If the request was aborted then no need to surface this error in the UI if (error instanceof Error && error.name === 'AbortError') return; - const fetchError = getPainlessError(error); + $scope.fetchStatus = fetchStatuses.NO_RESULTS; + $scope.rows = []; - if (fetchError) { - $scope.fetchError = fetchError; - } else { - toastNotifications.addError(error, { - title: i18n.translate('discover.errorLoadingData', { - defaultMessage: 'Error loading data', - }), - toastMessage: error.shortMessage || error.body?.message, - }); - } + data.search.showError(error); }); }; diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index 9c3d833d73b237..de1faaf9fc19d4 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -31,7 +31,6 @@ import { DiscoverNoResults } from '../angular/directives/no_results'; import { DiscoverUninitialized } from '../angular/directives/uninitialized'; import { DiscoverHistogram } from '../angular/directives/histogram'; import { LoadingSpinner } from './loading_spinner/loading_spinner'; -import { DiscoverFetchError, FetchError } from './fetch_error/fetch_error'; import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; import { SkipBottomButton } from './skip_bottom_button'; import { @@ -54,7 +53,6 @@ export interface DiscoverLegacyProps { addColumn: (column: string) => void; fetch: () => void; fetchCounter: number; - fetchError: FetchError; fieldCounts: Record; histogramData: Chart; hits: number; @@ -95,7 +93,6 @@ export function DiscoverLegacy({ addColumn, fetch, fetchCounter, - fetchError, fieldCounts, histogramData, hits, @@ -216,8 +213,7 @@ export function DiscoverLegacy({ {resultState === 'uninitialized' && } {/* @TODO: Solved in the Angular way to satisfy functional test - should be improved*/} - {fetchError && } -
+
diff --git a/src/plugins/discover/public/application/components/fetch_error/fetch_error.scss b/src/plugins/discover/public/application/components/fetch_error/fetch_error.scss deleted file mode 100644 index a587b2897e3a02..00000000000000 --- a/src/plugins/discover/public/application/components/fetch_error/fetch_error.scss +++ /dev/null @@ -1,3 +0,0 @@ -.discoverFetchError { - max-width: 1000px; -} diff --git a/src/plugins/discover/public/application/components/fetch_error/fetch_error.tsx b/src/plugins/discover/public/application/components/fetch_error/fetch_error.tsx deleted file mode 100644 index dc8f1238eac6fd..00000000000000 --- a/src/plugins/discover/public/application/components/fetch_error/fetch_error.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import './fetch_error.scss'; -import React, { Fragment } from 'react'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { EuiFlexGroup, EuiFlexItem, EuiCallOut, EuiCodeBlock, EuiSpacer } from '@elastic/eui'; -import { getServices } from '../../../kibana_services'; - -export interface FetchError { - lang: string; - script: string; - message: string; - error: string; -} - -interface Props { - fetchError: FetchError; -} - -export const DiscoverFetchError = ({ fetchError }: Props) => { - if (!fetchError) { - return null; - } - - let body; - - if (fetchError.lang === 'painless') { - const { chrome } = getServices(); - const mangagementUrlObj = chrome.navLinks.get('kibana:stack_management'); - const managementUrl = mangagementUrlObj ? mangagementUrlObj.url : ''; - const url = `${managementUrl}/kibana/indexPatterns`; - - body = ( -

- - ), - managementLink: ( - - - - ), - }} - /> -

- ); - } - - return ( - - - - - - - - {body} - - {fetchError.error} - - - - - - - - ); -}; diff --git a/src/plugins/expressions/common/expression_types/specs/error.ts b/src/plugins/expressions/common/expression_types/specs/error.ts index 35554954d08282..c95a019f4e8d22 100644 --- a/src/plugins/expressions/common/expression_types/specs/error.ts +++ b/src/plugins/expressions/common/expression_types/specs/error.ts @@ -30,6 +30,7 @@ export type ExpressionValueError = ExpressionValueBoxed< message: string; name?: string; stack?: string; + original?: Error; }; info?: unknown; } diff --git a/src/plugins/expressions/common/util/create_error.ts b/src/plugins/expressions/common/util/create_error.ts index 876e7dfec799c9..9bdab74efd6f96 100644 --- a/src/plugins/expressions/common/util/create_error.ts +++ b/src/plugins/expressions/common/util/create_error.ts @@ -21,7 +21,7 @@ import { ExpressionValueError } from '../../common'; type ErrorLike = Partial>; -export const createError = (err: string | ErrorLike): ExpressionValueError => ({ +export const createError = (err: string | Error | ErrorLike): ExpressionValueError => ({ type: 'error', error: { stack: @@ -32,5 +32,6 @@ export const createError = (err: string | ErrorLike): ExpressionValueError => ({ : undefined, message: typeof err === 'string' ? err : String(err.message), name: typeof err === 'object' ? err.name || 'Error' : 'Error', + original: err instanceof Error ? err : undefined, }, }); diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts index 31f0fc5f94479a..bb4fabb189a274 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts @@ -50,6 +50,8 @@ describe('getVisualizationInstance', () => { }; savedVisMock = {}; // @ts-expect-error + mockServices.data.search.showError.mockImplementation(() => {}); + // @ts-expect-error mockServices.savedVisualizations.get.mockImplementation(() => savedVisMock); // @ts-expect-error mockServices.visualizations.convertToSerializedVis.mockImplementation(() => serializedVisMock); @@ -119,6 +121,6 @@ describe('getVisualizationInstance', () => { error: 'error', }); - expect(mockServices.toastNotifications.addError).toHaveBeenCalled(); + expect(mockServices.data.search.showError).toHaveBeenCalled(); }); }); diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index 3ffca578f8052f..c5cfa5a4c639b3 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -17,7 +17,6 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; import { SerializedVis, Vis, @@ -28,6 +27,7 @@ import { import { SearchSourceFields } from 'src/plugins/data/public'; import { SavedObject } from 'src/plugins/saved_objects/public'; import { cloneDeep } from 'lodash'; +import { ExpressionValueError } from 'src/plugins/expressions/public'; import { createSavedSearchesLoader } from '../../../../discover/public'; import { VisualizeServices } from '../types'; @@ -35,14 +35,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( vis: Vis, visualizeServices: VisualizeServices ) => { - const { - chrome, - data, - overlays, - createVisEmbeddableFromObject, - savedObjects, - toastNotifications, - } = visualizeServices; + const { chrome, data, overlays, createVisEmbeddableFromObject, savedObjects } = visualizeServices; const embeddableHandler = (await createVisEmbeddableFromObject(vis, { timeRange: data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), @@ -51,11 +44,9 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( embeddableHandler.getOutput$().subscribe((output) => { if (output.error) { - toastNotifications.addError(output.error, { - title: i18n.translate('visualize.error.title', { - defaultMessage: 'Visualization error', - }), - }); + data.search.showError( + ((output.error as unknown) as ExpressionValueError['error']).original || output.error + ); } }); diff --git a/test/functional/apps/discover/_errors.ts b/test/functional/apps/discover/_errors.ts index 9520d652a65d56..7f1552b90668b7 100644 --- a/test/functional/apps/discover/_errors.ts +++ b/test/functional/apps/discover/_errors.ts @@ -22,7 +22,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const testSubjects = getService('testSubjects'); + const toasts = getService('toasts'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); describe('errors', function describeIndexTests() { @@ -39,8 +39,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('invalid scripted field error', () => { it('is rendered', async () => { - const isFetchErrorVisible = await testSubjects.exists('discoverFetchError'); - expect(isFetchErrorVisible).to.be(true); + const toast = await toasts.getToastElement(1); + const painlessStackTrace = await toast.findByTestSubject('painlessStackTrace'); + expect(painlessStackTrace).not.to.be(undefined); }); }); }); diff --git a/test/functional/services/toasts.ts b/test/functional/services/toasts.ts index a70e4ba464ae85..f5416a44e3b5a9 100644 --- a/test/functional/services/toasts.ts +++ b/test/functional/services/toasts.ts @@ -63,7 +63,7 @@ export function ToastsProvider({ getService }: FtrProviderContext) { } } - private async getToastElement(index: number) { + public async getToastElement(index: number) { const list = await this.getGlobalToastList(); return await list.findByCssSelector(`.euiToast:nth-child(${index})`); } diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index af2fc85602541a..6e34e4c1964c59 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -8,6 +8,7 @@ import { coreMock } from '../../../../../src/core/public/mocks'; import { EnhancedSearchInterceptor } from './search_interceptor'; import { CoreSetup, CoreStart } from 'kibana/public'; import { AbortError, UI_SETTINGS } from '../../../../../src/plugins/data/common'; +import { SearchTimeoutError } from 'src/plugins/data/public'; const timeTravel = (msToRun = 0) => { jest.advanceTimersByTime(msToRun); @@ -265,7 +266,7 @@ describe('EnhancedSearchInterceptor', () => { await timeTravel(1000); expect(error).toHaveBeenCalled(); - expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); + expect(error.mock.calls[0][0]).toBeInstanceOf(SearchTimeoutError); expect(mockCoreSetup.http.fetch).toHaveBeenCalled(); expect(mockCoreSetup.http.delete).not.toHaveBeenCalled(); }); @@ -305,7 +306,7 @@ describe('EnhancedSearchInterceptor', () => { await timeTravel(1000); expect(error).toHaveBeenCalled(); - expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); + expect(error.mock.calls[0][0]).toBeInstanceOf(SearchTimeoutError); expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2); expect(mockCoreSetup.http.delete).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index c8fe72e6f2c1e9..cca87c85e326c4 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -5,9 +5,7 @@ */ import { throwError, EMPTY, timer, from, Subscription } from 'rxjs'; -import { mergeMap, expand, takeUntil, finalize, tap } from 'rxjs/operators'; -import { debounce } from 'lodash'; -import { i18n } from '@kbn/i18n'; +import { mergeMap, expand, takeUntil, finalize, catchError } from 'rxjs/operators'; import { SearchInterceptor, SearchInterceptorDeps, @@ -15,6 +13,7 @@ import { } from '../../../../../src/plugins/data/public'; import { isErrorResponse, isCompleteResponse } from '../../../../../src/plugins/data/public'; import { AbortError, toPromise } from '../../../../../src/plugins/data/common'; +import { TimeoutErrorMode } from '../../../../../src/plugins/data/public'; import { IAsyncSearchOptions } from '.'; import { IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY } from '../../common'; @@ -40,6 +39,12 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { this.uiSettingsSub.unsubscribe(); } + protected getTimeoutMode() { + return this.application.capabilities.advancedSettings?.save + ? TimeoutErrorMode.CHANGE + : TimeoutErrorMode.CONTACT; + } + /** * Abort our `AbortController`, which in turn aborts any intercepted searches. */ @@ -55,7 +60,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { ) { let { id } = request; - const { combinedSignal, cleanup } = this.setupAbortSignal({ + const { combinedSignal, timeoutSignal, cleanup } = this.setupAbortSignal({ abortSignal: options.abortSignal, timeout: this.searchTimeout, }); @@ -86,15 +91,14 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { ); }), takeUntil(aborted$), - tap({ - error: () => { - // If we haven't received the response to the initial request, including the ID, then - // we don't need to send a follow-up request to delete this search. Otherwise, we - // send the follow-up request to delete this search, then throw an abort error. - if (id !== undefined) { - this.deps.http.delete(`/internal/search/${strategy}/${id}`); - } - }, + catchError((e: any) => { + // If we haven't received the response to the initial request, including the ID, then + // we don't need to send a follow-up request to delete this search. Otherwise, we + // send the follow-up request to delete this search, then throw an abort error. + if (id !== undefined) { + this.deps.http.delete(`/internal/search/${strategy}/${id}`); + } + return throwError(this.handleSearchError(e, request, timeoutSignal, options?.abortSignal)); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); @@ -102,28 +106,4 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { }) ); } - - // Right now we are debouncing but we will hook this up with background sessions to show only one - // error notification per session. - protected showTimeoutError = debounce( - (e: Error) => { - const message = this.application.capabilities.advancedSettings?.save - ? i18n.translate('xpack.data.search.timeoutIncreaseSetting', { - defaultMessage: - 'One or more queries timed out. Increase run time with the search.timeout advanced setting.', - }) - : i18n.translate('xpack.data.search.timeoutContactAdmin', { - defaultMessage: - 'One or more queries timed out. Contact your system administrator to increase the run time.', - }); - this.deps.toasts.addError(e, { - title: 'Timed out', - toastMessage: message, - }); - }, - 60000, - { - leading: true, - } - ); } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index f326d5ad54ef29..47da1e93cf004b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -44,6 +44,7 @@ describe('alert actions', () => { updateTimelineIsLoading = jest.fn() as jest.Mocked; searchStrategyClient = { aggs: {} as ISearchStart['aggs'], + showError: jest.fn(), search: jest.fn().mockResolvedValue({ data: mockTimelineDetails }), searchSource: {} as ISearchStart['searchSource'], }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fd743400133a7d..1c2c4c78504f0c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1367,10 +1367,6 @@ "discover.embeddable.inspectorRequestDataTitle": "データ", "discover.embeddable.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。", "discover.embeddable.search.displayName": "検索", - "discover.errorLoadingData": "データの読み込み中にエラーが発生", - "discover.fetchError.howToAddressErrorDescription": "このエラーは、{scriptedFields}タブにある {managementLink}の{fetchErrorScript}フィールドを編集することで解決できます。", - "discover.fetchError.managmentLinkText": "管理>インデックスパターン", - "discover.fetchError.scriptedFieldsText": "「スクリプトフィールド」", "discover.fieldChooser.detailViews.emptyStringText": "空の文字列", "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"", "discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "{field}を除外:\"{value}\"", @@ -1445,7 +1441,6 @@ "discover.notifications.invalidTimeRangeTitle": "無効な時間範囲", "discover.notifications.notSavedSearchTitle": "検索「{savedSearchTitle}」は保存されませんでした。", "discover.notifications.savedSearchTitle": "検索「{savedSearchTitle}」が保存されました。", - "discover.painlessError.painlessScriptedFieldErrorMessage": "Painlessスクリプトのフィールド「{script}」のエラー.", "discover.reloadSavedSearchButton": "検索をリセット", "discover.rootBreadcrumb": "発見", "discover.savedSearch.savedObjectName": "保存検索", @@ -4380,7 +4375,6 @@ "visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPatternまたはsavedSearchIdが必要です", "visualize.createVisualization.noVisTypeErrorMessage": "有効なビジュアライゼーションタイプを指定してください", "visualize.editor.createBreadcrumb": "作成", - "visualize.error.title": "ビジュアライゼーションエラー", "visualize.helpMenu.appName": "可視化", "visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました", "visualize.listing.betaTitle": "ベータ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 104fc70f5dd715..5738b440a9e925 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1368,10 +1368,6 @@ "discover.embeddable.inspectorRequestDataTitle": "数据", "discover.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。", "discover.embeddable.search.displayName": "搜索", - "discover.errorLoadingData": "加载数据时出错", - "discover.fetchError.howToAddressErrorDescription": "您可以通过编辑{managementLink}中{scriptedFields}选项卡下的“{fetchErrorScript}”字段来解决此错误。", - "discover.fetchError.managmentLinkText": "“管理”>“索引模式”", - "discover.fetchError.scriptedFieldsText": "“脚本字段”", "discover.fieldChooser.detailViews.emptyStringText": "空字符串", "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除 {field}:“{value}”", "discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "筛留 {field}:“{value}”", @@ -1446,7 +1442,6 @@ "discover.notifications.invalidTimeRangeTitle": "时间范围无效", "discover.notifications.notSavedSearchTitle": "搜索“{savedSearchTitle}”未保存。", "discover.notifications.savedSearchTitle": "搜索“{savedSearchTitle}”已保存", - "discover.painlessError.painlessScriptedFieldErrorMessage": "Painless 脚本字段“{script}”有错误。", "discover.reloadSavedSearchButton": "重置搜索", "discover.rootBreadcrumb": "Discover", "discover.savedSearch.savedObjectName": "已保存搜索", @@ -4381,7 +4376,6 @@ "visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId", "visualize.createVisualization.noVisTypeErrorMessage": "必须提供有效的可视化类型", "visualize.editor.createBreadcrumb": "创建", - "visualize.error.title": "可视化错误", "visualize.helpMenu.appName": "Visualize", "visualize.linkedToSearch.unlinkSuccessNotificationText": "已取消与已保存搜索“{searchTitle}”的链接", "visualize.listing.betaTitle": "公测版", diff --git a/x-pack/test/functional/apps/discover/error_handling.ts b/x-pack/test/functional/apps/discover/error_handling.ts index 515e5e293ae280..40aa8cd5c06069 100644 --- a/x-pack/test/functional/apps/discover/error_handling.ts +++ b/x-pack/test/functional/apps/discover/error_handling.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const testSubjects = getService('testSubjects'); + const toasts = getService('toasts'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); describe('errors', function describeIndexTests() { @@ -23,11 +23,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async function () { await esArchiver.unload('invalid_scripted_field'); }); + // this is the same test as in OSS but it catches different error message issue in different licences describe('invalid scripted field error', () => { it('is rendered', async () => { - const isFetchErrorVisible = await testSubjects.exists('discoverFetchError'); - expect(isFetchErrorVisible).to.be(true); + const toast = await toasts.getToastElement(1); + const painlessStackTrace = await toast.findByTestSubject('painlessStackTrace'); + expect(painlessStackTrace).not.to.be(undefined); }); }); }); From 8ce96e3ce4345c67142faf88280eba2bd709b003 Mon Sep 17 00:00:00 2001 From: ncheckin <68351161+ncheckin@users.noreply.github.com> Date: Mon, 28 Sep 2020 11:35:33 -0400 Subject: [PATCH 6/8] Update tutorial-discovering.asciidoc (#76976) * Update tutorial-discovering.asciidoc Adds alt text to images in doc. * Update docs/getting-started/tutorial-discovering.asciidoc * Update docs/getting-started/tutorial-discovering.asciidoc Co-authored-by: Kaarina Tungseth --- docs/getting-started/tutorial-discovering.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/tutorial-discovering.asciidoc b/docs/getting-started/tutorial-discovering.asciidoc index ec07a74b8ac0db..99a07acf987917 100644 --- a/docs/getting-started/tutorial-discovering.asciidoc +++ b/docs/getting-started/tutorial-discovering.asciidoc @@ -21,7 +21,7 @@ The search returns all account numbers between zero and 99 with balances in excess of 47,500. Results appear for account numbers 8, 32, 78, 85, and 97. + [role="screenshot"] -image::images/tutorial-discover-2.png[] +image::images/tutorial-discover-2.png[Image showing the search results for account numbers between zero and 99, with balances in excess of 47,500] + . Hover over the list of *Available fields*, then click *Add* next to each field you want include in the table. @@ -30,6 +30,6 @@ For example, when you add the `account_number` field, the display changes to a l account numbers. + [role="screenshot"] -image::images/tutorial-discover-3.png[] +image::images/tutorial-discover-3.png[Image showing a dropdown with five account numbers, which match the previous query for account balance] Now that you know what your documents contain, it's time to gain insight into your data with visualizations. From 68912875c4c4879c4723dae648d74b46ca9136fa Mon Sep 17 00:00:00 2001 From: ncheckin <68351161+ncheckin@users.noreply.github.com> Date: Mon, 28 Sep 2020 11:41:03 -0400 Subject: [PATCH 7/8] Update tutorial-visualizing.asciidoc (#76977) * Update tutorial-visualizing.asciidoc adds alt text to images * Update docs/getting-started/tutorial-visualizing.asciidoc * Update docs/getting-started/tutorial-visualizing.asciidoc * Update docs/getting-started/tutorial-visualizing.asciidoc * Update docs/getting-started/tutorial-visualizing.asciidoc * Update docs/getting-started/tutorial-visualizing.asciidoc * Update docs/getting-started/tutorial-visualizing.asciidoc Co-authored-by: Elastic Machine Co-authored-by: Kaarina Tungseth --- .../tutorial-visualizing.asciidoc | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/getting-started/tutorial-visualizing.asciidoc b/docs/getting-started/tutorial-visualizing.asciidoc index 33a70351602477..a53c8cb6bc23d7 100644 --- a/docs/getting-started/tutorial-visualizing.asciidoc +++ b/docs/getting-started/tutorial-visualizing.asciidoc @@ -16,7 +16,7 @@ To visualize the Shakespeare data and compare the number of speaking parts in th . Click *Create new*, then click *Lens* on the *New Visualization* window. + [role="screenshot"] -image::images/tutorial-visualize-wizard-step-1.png[Bar chart] +image::images/tutorial-visualize-wizard-step-1.png[Image showing different options for your new visualization] . Make sure the index pattern is *shakes*. @@ -39,7 +39,7 @@ image::images/tutorial-visualize-wizard-step-1.png[Bar chart] .. In the *Label* field, enter `Speaking Parts`. + [role="screenshot"] -image::images/tutorial-visualize-bar-1.5.png[Bar chart] +image::images/tutorial-visualize-bar-1.5.png[Bar chart showing the speaking parts data] . *Save* the chart with the name `Bar Example`. + @@ -85,7 +85,7 @@ Since the default search matches all documents, the pie contains a single slice. The pie chart displays the proportion of the 1,000 accounts that fall into each of the ranges. + [role="screenshot"] -image::images/tutorial-visualize-pie-2.png[Pie chart] +image::images/tutorial-visualize-pie-2.png[Pie chart displaying accounts that fall into each of the ranges, scaled to 1000 accounts] . Add another bucket aggregation that displays the ages of the account holders. @@ -99,7 +99,7 @@ The break down of the ages of the account holders are displayed in a ring around the balance ranges. + [role="screenshot"] -image::images/tutorial-visualize-pie-3.png[Final pie chart] +image::images/tutorial-visualize-pie-3.png[Final pie chart showing all of the changes] . Click *Save*, then enter `Pie Example` in the *Title* field. @@ -119,7 +119,7 @@ To visualize geographic information in the log file data, use <>. .. Set the *End date* to `May 20, 2015 @ 12:00:00.000`. + [role="screenshot"] -image::images/gs_maps_time_filter.png[Time filter for Maps tutorial] +image::images/gs_maps_time_filter.png[Image showing the time filter for Maps tutorial] .. Click *Update* @@ -140,7 +140,7 @@ image::images/gs_maps_time_filter.png[Time filter for Maps tutorial] .. From the *Border color* dropdown, select *#FFF*, then click *Save & close*. + [role="screenshot"] -image::images/tutorial-visualize-map-2.png[Map] +image::images/tutorial-visualize-map-2.png[Example of a map visualization] . Click *Save*, then enter `Map Example` in the *Title* field. @@ -170,12 +170,12 @@ The Markdown widget uses **markdown** syntax. The Markdown renders in the preview pane. + [role="screenshot"] -image::images/tutorial-visualize-md-2.png[] +image::images/tutorial-visualize-md-2.png[Image showing example markdown editing field] . Click *Save*, then enter `Markdown Example` in the *Title* field. [role="screenshot"] -image::images/tutorial-dashboard.png[] +image::images/tutorial-dashboard.png[Final visualization with bar chart, pie chart, map, and markdown text field] [float] === Next steps From 93248808b33d2ab8bafcff73a3c47c409d2a4700 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Mon, 28 Sep 2020 18:47:53 +0300 Subject: [PATCH 8/8] Fix types (#78619) --- src/plugins/visualizations/public/types.ts | 4 ++-- .../public/vis_types/base_vis_type.ts | 16 +++++++++------- .../public/vis_types/types_service.ts | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts index 897a8c1e323194..68ab3561d375c1 100644 --- a/src/plugins/visualizations/public/types.ts +++ b/src/plugins/visualizations/public/types.ts @@ -73,7 +73,7 @@ export interface VisToExpressionAstParams { abortSignal?: AbortSignal; } -export type VisToExpressionAst = ( - vis: Vis, +export type VisToExpressionAst = ( + vis: Vis, params: VisToExpressionAstParams ) => ExpressionAstExpression; diff --git a/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index 283286648ff16f..27a4f299c1e9ca 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { VisToExpressionAst, VisualizationControllerConstructor } from '../types'; +import { VisParams, VisToExpressionAst, VisualizationControllerConstructor } from '../types'; import { TriggerContextMapping } from '../../../ui_actions/public'; import { Adapters } from '../../../inspector/public'; @@ -43,8 +43,8 @@ interface CommonBaseVisTypeOptions { inspectorAdapters?: Adapters | (() => Adapters); } -interface ExpressionBaseVisTypeOptions extends CommonBaseVisTypeOptions { - toExpressionAst: VisToExpressionAst; +interface ExpressionBaseVisTypeOptions extends CommonBaseVisTypeOptions { + toExpressionAst: VisToExpressionAst; visualization?: undefined; } @@ -53,9 +53,11 @@ interface VisualizationBaseVisTypeOptions extends CommonBaseVisTypeOptions { visualization: VisualizationControllerConstructor | undefined; } -export type BaseVisTypeOptions = ExpressionBaseVisTypeOptions | VisualizationBaseVisTypeOptions; +export type BaseVisTypeOptions = + | ExpressionBaseVisTypeOptions + | VisualizationBaseVisTypeOptions; -export class BaseVisType { +export class BaseVisType { name: string; title: string; description: string; @@ -77,9 +79,9 @@ export class BaseVisType { setup?: unknown; useCustomNoDataScreen: boolean; inspectorAdapters?: Adapters | (() => Adapters); - toExpressionAst?: VisToExpressionAst; + toExpressionAst?: VisToExpressionAst; - constructor(opts: BaseVisTypeOptions) { + constructor(opts: BaseVisTypeOptions) { if (!opts.icon && !opts.image) { throw new Error('vis_type must define its icon or image'); } diff --git a/src/plugins/visualizations/public/vis_types/types_service.ts b/src/plugins/visualizations/public/vis_types/types_service.ts index 157dbd41ce8a21..1afbd6901a1959 100644 --- a/src/plugins/visualizations/public/vis_types/types_service.ts +++ b/src/plugins/visualizations/public/vis_types/types_service.ts @@ -71,7 +71,7 @@ export class TypesService { * registers a visualization type * @param config - visualization type definition */ - createBaseVisualization: (config: BaseVisTypeOptions): void => { + createBaseVisualization: (config: BaseVisTypeOptions): void => { const vis = new BaseVisType(config); registerVisualization(() => vis); },