diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index 8121405e5ae24..370643789c2cd 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -4,7 +4,6 @@ library 'kibana-pipeline-library' kibanaLibrary.load() def TASK_PARAM = params.TASK ?: params.CI_GROUP - // Looks like 'oss:ciGroup:1', 'oss:firefoxSmoke' def JOB_PARTS = TASK_PARAM.split(':') def IS_XPACK = JOB_PARTS[0] == 'xpack' @@ -111,6 +110,8 @@ def getWorkerFromParams(isXpack, job, ciGroup) { return kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh') } else if (job == 'apiIntegration') { return kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh') + } else if (job == 'pluginFunctional') { + return kibanaPipeline.functionalTestProcess('oss-pluginFunctional', './test/scripts/jenkins_plugin_functional.sh') } else { return kibanaPipeline.ossCiGroupProcess(ciGroup) } diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 87b64437deafc..f1095f8035b6c 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -13,12 +13,12 @@ pipeline { BASE_DIR = 'src/github.com/elastic/kibana' HOME = "${env.WORKSPACE}" E2E_DIR = 'x-pack/plugins/apm/e2e' - PIPELINE_LOG_LEVEL = 'DEBUG' + PIPELINE_LOG_LEVEL = 'INFO' KBN_OPTIMIZER_THEMES = 'v7light' } options { timeout(time: 1, unit: 'HOURS') - buildDiscarder(logRotator(numToKeepStr: '40', artifactNumToKeepStr: '20', daysToKeepStr: '30')) + buildDiscarder(logRotator(numToKeepStr: '30', artifactNumToKeepStr: '10', daysToKeepStr: '30')) timestamps() ansiColor('xterm') disableResume() diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index 272cd524c2c20..ac7cbba6e9933 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -71,7 +71,7 @@ Alias: `condition` [[alterColumn_fn]] === `alterColumn` -Converts between core types, including `string`, `number`, `null`, `boolean`, and `date`, and renames columns. See also <> and <>. +Converts between core types, including `string`, `number`, `null`, `boolean`, and `date`, and renames columns. See also <>, <>, and <>. *Expression syntax* [source,js] @@ -1717,11 +1717,16 @@ Adds a column calculated as the result of other columns. Changes are made only w |=== |Argument |Type |Description +|`id` + +|`string`, `null` +|An optional id of the resulting column. When no id is provided, the id will be looked up from the existing column by the provided name argument. If no column with this name exists yet, a new column with this name and an identical id will be added to the table. + |_Unnamed_ *** Aliases: `column`, `name` |`string` -|The name of the resulting column. +|The name of the resulting column. Names are not required to be unique. |`expression` *** @@ -1729,11 +1734,6 @@ Aliases: `exp`, `fn`, `function` |`boolean`, `number`, `string`, `null` |A Canvas expression that is passed to each row as a single row `datatable`. -|`id` - -|`string`, `null` -|An optional id of the resulting column. When not specified or `null` the name argument is used as id. - |`copyMetaFrom` |`string`, `null` @@ -1808,6 +1808,47 @@ Default: `"throw"` *Returns:* `number` | `boolean` | `null` +[float] +[[mathColumn_fn]] +=== `mathColumn` + +Adds a column by evaluating `TinyMath` on each row. This function is optimized for math, so it performs better than the <> with a <>. +*Accepts:* `datatable` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|id *** +|`string` +|id of the resulting column. Must be unique. + +|name *** +|`string` +|The name of the resulting column. Names are not required to be unique. + +|_Unnamed_ + +Alias: `expression` +|`string` +|A `TinyMath` expression evaluated on each row. See https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html. + +|`onError` + +|`string` +|In case the `TinyMath` evaluation fails or returns NaN, the return value is specified by onError. For example, `"null"`, `"zero"`, `"false"`, `"throw"`. When `"throw"`, it will throw an exception, terminating expression execution. + +Default: `"throw"` + +|`copyMetaFrom` + +|`string`, `null` +|If set, the meta object from the specified column id is copied over to the specified target column. Throws an exception if the column doesn't exist +|=== + +*Returns:* `datatable` + + [float] [[metric_fn]] === `metric` @@ -2581,7 +2622,7 @@ Default: `false` [[staticColumn_fn]] === `staticColumn` -Adds a column with the same static value in every row. See also <> and <>. +Adds a column with the same static value in every row. See also <>, <>, and <>. *Accepts:* `datatable` diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md index 1eaf00c7a678d..6229aeb9238e8 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md @@ -16,6 +16,7 @@ Note that when generating absolute urls, the origin (protocol, host and port) ar getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean; + deepLinkId?: string; }): string; ``` @@ -24,7 +25,7 @@ getUrlForApp(appId: string, options?: { | Parameter | Type | Description | | --- | --- | --- | | appId | string | | -| options | {
path?: string;
absolute?: boolean;
} | | +| options | {
path?: string;
absolute?: boolean;
deepLinkId?: string;
} | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin._constructor_.md similarity index 68% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin._constructor_.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin._constructor_.md index 64108a7c7be33..3eaf2176edf26 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin._constructor_.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Plugin](./kibana-plugin-plugins-data-public.plugin.md) > [(constructor)](./kibana-plugin-plugins-data-public.plugin._constructor_.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPlugin](./kibana-plugin-plugins-data-public.dataplugin.md) > [(constructor)](./kibana-plugin-plugins-data-public.dataplugin._constructor_.md) -## Plugin.(constructor) +## DataPlugin.(constructor) Constructs a new instance of the `DataPublicPlugin` class diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.md new file mode 100644 index 0000000000000..4b2cad7b42882 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPlugin](./kibana-plugin-plugins-data-public.dataplugin.md) + +## DataPlugin class + +Signature: + +```typescript +export declare class DataPublicPlugin implements Plugin +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(initializerContext)](./kibana-plugin-plugins-data-public.dataplugin._constructor_.md) | | Constructs a new instance of the DataPublicPlugin class | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [setup(core, { bfetch, expressions, uiActions, usageCollection, inspector })](./kibana-plugin-plugins-data-public.dataplugin.setup.md) | | | +| [start(core, { uiActions })](./kibana-plugin-plugins-data-public.dataplugin.start.md) | | | +| [stop()](./kibana-plugin-plugins-data-public.dataplugin.stop.md) | | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.setup.md similarity index 76% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.setup.md index 20181a5208b52..ab1f90c1ac104 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.setup.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Plugin](./kibana-plugin-plugins-data-public.plugin.md) > [setup](./kibana-plugin-plugins-data-public.plugin.setup.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPlugin](./kibana-plugin-plugins-data-public.dataplugin.md) > [setup](./kibana-plugin-plugins-data-public.dataplugin.setup.md) -## Plugin.setup() method +## DataPlugin.setup() method Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.start.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.start.md similarity index 70% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.start.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.start.md index 56934e8a29edd..4ea7ec8cd4f65 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.start.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.start.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Plugin](./kibana-plugin-plugins-data-public.plugin.md) > [start](./kibana-plugin-plugins-data-public.plugin.start.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPlugin](./kibana-plugin-plugins-data-public.dataplugin.md) > [start](./kibana-plugin-plugins-data-public.dataplugin.start.md) -## Plugin.start() method +## DataPlugin.start() method Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.stop.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.stop.md similarity index 52% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.stop.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.stop.md index 8b8b63db4e03a..b7067a01b4467 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.stop.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.dataplugin.stop.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Plugin](./kibana-plugin-plugins-data-public.plugin.md) > [stop](./kibana-plugin-plugins-data-public.plugin.stop.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPlugin](./kibana-plugin-plugins-data-public.dataplugin.md) > [stop](./kibana-plugin-plugins-data-public.dataplugin.stop.md) -## Plugin.stop() method +## DataPlugin.stop() method Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md index 5d92e348d6276..2cde2b7455585 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md @@ -10,6 +10,6 @@ esKuery: { nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("../../kibana_utils/common").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 7f5a042e0ab81..7c023e756ebd5 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 @@ -11,6 +11,7 @@ | [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) | | | [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) | | | [AggParamType](./kibana-plugin-plugins-data-public.aggparamtype.md) | | +| [DataPlugin](./kibana-plugin-plugins-data-public.dataplugin.md) | | | [DuplicateIndexPatternError](./kibana-plugin-plugins-data-public.duplicateindexpatternerror.md) | | | [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) | | | [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) | | @@ -19,7 +20,6 @@ | [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) | | | [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 | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md index 19cb742785e7b..4b96d8af756f3 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md @@ -10,6 +10,6 @@ esKuery: { nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("../../kibana_utils/common").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; } ``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction._constructor_.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction._constructor_.md index 388f0e064d866..e51c465e912e6 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction._constructor_.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `AddPanelAction` class Signature: ```typescript -constructor(getFactory: EmbeddableStart['getEmbeddableFactory'], getAllFactories: EmbeddableStart['getEmbeddableFactories'], overlays: OverlayStart, notifications: NotificationsStart, SavedObjectFinder: React.ComponentType); +constructor(getFactory: EmbeddableStart['getEmbeddableFactory'], getAllFactories: EmbeddableStart['getEmbeddableFactories'], overlays: OverlayStart, notifications: NotificationsStart, SavedObjectFinder: React.ComponentType, reportUiCounter?: ((appName: string, type: import("@kbn/analytics").UiCounterMetricType, eventNames: string | string[], count?: number | undefined) => void) | undefined); ``` ## Parameters @@ -21,4 +21,5 @@ constructor(getFactory: EmbeddableStart['getEmbeddableFactory'], getAllFactories | overlays | OverlayStart | | | notifications | NotificationsStart | | | SavedObjectFinder | React.ComponentType<any> | | +| reportUiCounter | ((appName: string, type: import("@kbn/analytics").UiCounterMetricType, eventNames: string | string[], count?: number | undefined) => void) | undefined | | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction.md index 74a6c2b2183a2..947e506f72b43 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.addpanelaction.md @@ -14,7 +14,7 @@ export declare class AddPanelAction implements Action | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(getFactory, getAllFactories, overlays, notifications, SavedObjectFinder)](./kibana-plugin-plugins-embeddable-public.addpanelaction._constructor_.md) | | Constructs a new instance of the AddPanelAction class | +| [(constructor)(getFactory, getAllFactories, overlays, notifications, SavedObjectFinder, reportUiCounter)](./kibana-plugin-plugins-embeddable-public.addpanelaction._constructor_.md) | | Constructs a new instance of the AddPanelAction class | ## Properties diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md index 90caaa3035b34..db45b691b446e 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md @@ -15,6 +15,7 @@ export declare function openAddPanelFlyout(options: { notifications: NotificationsStart; SavedObjectFinder: React.ComponentType; showCreateNewMenu?: boolean; + reportUiCounter?: UsageCollectionStart['reportUiCounter']; }): OverlayRef; ``` @@ -22,7 +23,7 @@ export declare function openAddPanelFlyout(options: { | Parameter | Type | Description | | --- | --- | --- | -| options | {
embeddable: IContainer;
getFactory: EmbeddableStart['getEmbeddableFactory'];
getAllFactories: EmbeddableStart['getEmbeddableFactories'];
overlays: OverlayStart;
notifications: NotificationsStart;
SavedObjectFinder: React.ComponentType<any>;
showCreateNewMenu?: boolean;
} | | +| options | {
embeddable: IContainer;
getFactory: EmbeddableStart['getEmbeddableFactory'];
getAllFactories: EmbeddableStart['getEmbeddableFactories'];
overlays: OverlayStart;
notifications: NotificationsStart;
SavedObjectFinder: React.ComponentType<any>;
showCreateNewMenu?: boolean;
reportUiCounter?: UsageCollectionStart['reportUiCounter'];
} | | Returns: diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md index c6e00842a31e6..2c03db82ba683 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md @@ -21,6 +21,7 @@ export interface ExpressionFunctionDefinitions | [derivative](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md) | ExpressionFunctionDerivative | | | [font](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.font.md) | ExpressionFunctionFont | | | [moving\_average](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.moving_average.md) | ExpressionFunctionMovingAverage | | +| [overall\_metric](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.overall_metric.md) | ExpressionFunctionOverallMetric | | | [theme](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.theme.md) | ExpressionFunctionTheme | | | [var\_set](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.var_set.md) | ExpressionFunctionVarSet | | | [var](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.var.md) | ExpressionFunctionVar | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.overall_metric.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.overall_metric.md new file mode 100644 index 0000000000000..8685788a2f351 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.overall_metric.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md) > [overall\_metric](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.overall_metric.md) + +## ExpressionFunctionDefinitions.overall\_metric property + +Signature: + +```typescript +overall_metric: ExpressionFunctionOverallMetric; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md index 219678244951b..f55fed99e1d3d 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md @@ -21,6 +21,7 @@ export interface ExpressionFunctionDefinitions | [derivative](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md) | ExpressionFunctionDerivative | | | [font](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.font.md) | ExpressionFunctionFont | | | [moving\_average](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.moving_average.md) | ExpressionFunctionMovingAverage | | +| [overall\_metric](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.overall_metric.md) | ExpressionFunctionOverallMetric | | | [theme](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.theme.md) | ExpressionFunctionTheme | | | [var\_set](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.var_set.md) | ExpressionFunctionVarSet | | | [var](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.var.md) | ExpressionFunctionVar | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.overall_metric.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.overall_metric.md new file mode 100644 index 0000000000000..b8564a696e6e4 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.overall_metric.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md) > [overall\_metric](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.overall_metric.md) + +## ExpressionFunctionDefinitions.overall\_metric property + +Signature: + +```typescript +overall_metric: ExpressionFunctionOverallMetric; +``` diff --git a/docs/discover/search-sessions.asciidoc b/docs/discover/search-sessions.asciidoc index fec1b8b26dd74..b503e8cfba3b4 100644 --- a/docs/discover/search-sessions.asciidoc +++ b/docs/discover/search-sessions.asciidoc @@ -68,3 +68,19 @@ behaves differently: * Relative dates are converted to absolute dates. * Panning and zooming is disabled for maps. * Changing a filter, query, or drilldown starts a new search session, which can be slow. + +[float] +==== Limitations + +Certain visualization features do not fully support background search sessions yet. If a dashboard using these features gets restored, +all panels using unsupported features won't load immediately, but instead send out additional data requests which can take a while to complete. +In this case a warning *Your search session is still running* will be shown. + +You can either wait for these additional requests to complete or come back to the dashboard later when all data requests have been finished. + +A panel on a dashboard can behave like this if one of the following features is used: +* *Lens* - A *top values* dimension with an enabled setting *Group other values as "Other"* (configurable in the *Advanced* section of the dimension) +* *Lens* - An *intervals* dimension is used +* *Aggregation based* visualizations - A *terms* aggregation is used with an enabled setting *Group other values in separate bucket* +* *Aggregation based* visualizations - A *histogram* aggregation is used +* *Maps* - Layers using joins, blended layers or tracks layers are used diff --git a/docs/user/alerting/domain-specific-rules.asciidoc b/docs/user/alerting/domain-specific-rules.asciidoc deleted file mode 100644 index f509f9e528823..0000000000000 --- a/docs/user/alerting/domain-specific-rules.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[role="xpack"] -[[domain-specific-rules]] -== Domain-specific rules - -For domain-specific rules, refer to the documentation for that app. -{kib} supports these rules: - -* {observability-guide}/create-alerts.html[Observability rules] -* {security-guide}/prebuilt-rules.html[Security rules] -* <> -* {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] beta:[] - -[NOTE] -============================================== -Some rule types are subscription features, while others are free features. -For a comparison of the Elastic subscription levels, -see {subscriptions}[the subscription page]. -============================================== - -include::map-rules/geo-rule-types.asciidoc[] diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 68cf3ee070b08..9ab6a2dc46ebf 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -3,6 +3,5 @@ include::alerting-setup.asciidoc[] include::create-and-manage-rules.asciidoc[] include::defining-rules.asciidoc[] include::rule-management.asciidoc[] -include::stack-rules.asciidoc[] -include::domain-specific-rules.asciidoc[] +include::rule-types.asciidoc[] include::alerting-troubleshooting.asciidoc[] diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc new file mode 100644 index 0000000000000..bb840014fe80f --- /dev/null +++ b/docs/user/alerting/rule-types.asciidoc @@ -0,0 +1,56 @@ +[role="xpack"] +[[rule-types]] +== Rule types + +A rule is a set of <>, <>, and <> that enable notifications. {kib} provides two types of rules: rules specific to the Elastic Stack and rules specific to a domain. + +[NOTE] +============================================== +Some rule types are subscription features, while others are free features. +For a comparison of the Elastic subscription levels, +see {subscriptions}[the subscription page]. +============================================== + +[float] +[[stack-rules]] +=== Stack rules + +<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information. + +[cols="2*<"] +|=== + +| <> +| Aggregate field values from documents using {es} queries, compare them to threshold values, and schedule actions to run when the thresholds are met. + +| <> +| Run a user-configured {es} query, compare the number of matches to a configured threshold, and schedule actions to run when the threshold condition is met. + +|=== + +[float] +[[domain-specific-rules]] +=== Domain rules + +Domain rules are registered by *Observability*, *Security*, <> and <>. + +[cols="2*<"] +|=== + +| {observability-guide}/create-alerts.html[Observability rules] +| Detect complex conditions in the *Logs*, *Metrics*, and *Uptime* apps. + +| {security-guide}/prebuilt-rules.html[Security rules] +| Detect suspicous source events with pre-built or custom rules and create alerts when a rule’s conditions are met. + +| <> +| Run an {es} query to determine if any documents are currently contained in any boundaries from a specified boundary index and generate alerts when a rule's conditions are met. + +| {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] beta:[] +| Run scheduled checks on an anomaly detection job to detect anomalies with certain conditions. If an anomaly meets the conditions, an alert is created and the associated action is triggered. + +|=== + +include::rule-types/index-threshold.asciidoc[] +include::rule-types/es-query.asciidoc[] +include::rule-types/geo-rule-types.asciidoc[] diff --git a/docs/user/alerting/stack-rules/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc similarity index 100% rename from docs/user/alerting/stack-rules/es-query.asciidoc rename to docs/user/alerting/rule-types/es-query.asciidoc diff --git a/docs/user/alerting/map-rules/geo-rule-types.asciidoc b/docs/user/alerting/rule-types/geo-rule-types.asciidoc similarity index 74% rename from docs/user/alerting/map-rules/geo-rule-types.asciidoc rename to docs/user/alerting/rule-types/geo-rule-types.asciidoc index eee7b59252205..244cf90c855a7 100644 --- a/docs/user/alerting/map-rules/geo-rule-types.asciidoc +++ b/docs/user/alerting/rule-types/geo-rule-types.asciidoc @@ -1,16 +1,14 @@ [role="xpack"] [[geo-alerting]] -=== Geo rule type +=== Tracking containment -Alerting now includes one additional stack rule: <>. - -As with other stack rules, you need `all` access to the *Stack Rules* feature -to be able to create and edit a geo rule. -See <> for more information on configuring roles that provide access to this feature. +<> offers the Tracking containment rule type which runs an {es} query over indices to determine whether any +documents are currently contained within any boundaries from the specified boundary index. +In the event that an entity is contained within a boundary, an alert may be generated. [float] -==== Geo alerting requirements -To create a *Tracking containment* rule, the following requirements must be present: +==== Requirements +To create a Tracking containment rule, the following requirements must be present: - *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field, and some form of entity identifier. An entity identifier is a `keyword` or `number` @@ -29,22 +27,12 @@ than the current time minus the amount of the interval. If data older than `now - ` is ingested, it won't trigger a rule. [float] -==== Creating a geo rule -Click the *Create* button in the <>. -Complete the <>. - -[role="screenshot"] -image::user/alerting/images/alert-types-tracking-select.png[Choosing a tracking rule type] +==== Create the rule -[float] -[[rule-type-tracking-containment]] -==== Tracking containment -The Tracking containment rule type runs an {es} query over indices, determining if any -documents are currently contained within any boundaries from the specified boundary index. -In the event that an entity is contained within a boundary, an alert may be generated. +Fill in the <>, then select Tracking containment. [float] -===== Defining the conditions +==== Define the conditions Tracking containment rules have 3 clauses that define the condition to detect, as well as 2 Kuery bars used to provide additional filtering context for each of the indices. @@ -61,6 +49,9 @@ Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_sha identifying boundaries, and an optional *Human-readable boundary name* for better alerting messages. +[float] +==== Add action + Conditions for how a rule is tracked can be specified uniquely for each individual action. A rule can be triggered either when a containment condition is met or when an entity is no longer contained. diff --git a/docs/user/alerting/stack-rules/index-threshold.asciidoc b/docs/user/alerting/rule-types/index-threshold.asciidoc similarity index 100% rename from docs/user/alerting/stack-rules/index-threshold.asciidoc rename to docs/user/alerting/rule-types/index-threshold.asciidoc diff --git a/docs/user/alerting/stack-rules.asciidoc b/docs/user/alerting/stack-rules.asciidoc deleted file mode 100644 index 483834c78806e..0000000000000 --- a/docs/user/alerting/stack-rules.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[role="xpack"] -[[stack-rules]] -== Stack rule types - -Kibana provides two types of rules: - -* Stack rules, which are built into {kib} -* <>, which are registered by {kib} apps. - -{kib} provides two stack rules: - -* <> -* <> - -Users require the `all` privilege to access the *Stack Rules* feature and create and edit rules. -See <> for more information. - -[NOTE] -============================================== -Some rule types are subscription features, while others are free features. -For a comparison of the Elastic subscription levels, -see {subscriptions}[the subscription page]. -============================================== - - -include::stack-rules/index-threshold.asciidoc[] -include::stack-rules/es-query.asciidoc[] diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc index 39e596df4af34..cb5c484def3b9 100644 --- a/docs/user/dashboard/aggregation-reference.asciidoc +++ b/docs/user/dashboard/aggregation-reference.asciidoc @@ -23,7 +23,7 @@ This reference can help simplify the comparison if you need a specific feature. | Table with summary row ^| X -| +^| X | | | @@ -65,7 +65,7 @@ This reference can help simplify the comparison if you need a specific feature. | Heat map ^| X -| +^| X | | ^| X @@ -190,8 +190,8 @@ For information about {es} metrics aggregations, refer to {ref}/search-aggregati | Metrics with filters | -^| X | +^| X | | Average @@ -333,7 +333,7 @@ build their advanced visualization. | Math on aggregated data | -| +^| X ^| X ^| X ^| X @@ -352,6 +352,13 @@ build their advanced visualization. ^| X ^| X +| Time shifts +| +^| X +^| X +^| X +^| X + | Fully custom {es} queries | | diff --git a/docs/user/dashboard/create-panels-with-editors.asciidoc b/docs/user/dashboard/create-panels-with-editors.asciidoc index 17d3b5fb8a8a5..77a4706e249fd 100644 --- a/docs/user/dashboard/create-panels-with-editors.asciidoc +++ b/docs/user/dashboard/create-panels-with-editors.asciidoc @@ -30,13 +30,16 @@ [[lens-editor]] === Lens -*Lens* is the drag and drop editor that creates visualizations of your data. +*Lens* is the drag and drop editor that creates visualizations of your data, recommended for most +users. With *Lens*, you can: * Use the automatically generated suggestions to change the visualization type. * Create visualizations with multiple layers and indices. * Change the aggregation and labels to customize the data. +* Perform math on aggregations using *Formula*. +* Use time shifts to compare data at two times, such as month over month. [role="screenshot"] image:dashboard/images/lens_advanced_1_1.png[Lens] diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 9f17a380bc209..7927489c596d7 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -300,7 +300,9 @@ image::images/lens_missing_values_strategy.png[Lens Missing values strategies me [[is-it-possible-to-change-the-scale-of-Y-axis]] ===== Is it possible to statically define the scale of the y-axis in a visualization? -The ability to start the y-axis from another value than 0, or use a logarithmic scale, is unsupported in *Lens*. +Yes, you can set the bounds on bar, line and area chart types in Lens, unless using percentage mode. Bar +and area charts must have 0 in the bounds. Logarithmic scales are unsupported in *Lens*. +To set the y-axis bounds, click the icon representing the axis you want to customize. [float] [[is-it-possible-to-have-pagination-for-datatable]] diff --git a/docs/user/production-considerations/production.asciidoc b/docs/user/production-considerations/production.asciidoc index 1ffca4b6ae6ab..b75b556588cfd 100644 --- a/docs/user/production-considerations/production.asciidoc +++ b/docs/user/production-considerations/production.asciidoc @@ -122,8 +122,6 @@ active in case of failure from the currently used instance. Kibana can be configured to connect to multiple Elasticsearch nodes in the same cluster. In situations where a node becomes unavailable, Kibana will transparently connect to an available node and continue operating. Requests to available hosts will be routed in a round robin fashion. -Currently the Console application is limited to connecting to the first node listed. - In kibana.yml: [source,js] -------- diff --git a/package.json b/package.json index 513352db3f81b..596bcff59797d 100644 --- a/package.json +++ b/package.json @@ -156,6 +156,7 @@ "@kbn/ui-framework": "link:packages/kbn-ui-framework", "@kbn/ui-shared-deps": "link:packages/kbn-ui-shared-deps", "@kbn/utility-types": "link:bazel-bin/packages/kbn-utility-types", + "@kbn/common-utils": "link:bazel-bin/packages/kbn-common-utils", "@kbn/utils": "link:bazel-bin/packages/kbn-utils", "@loaders.gl/core": "^2.3.1", "@loaders.gl/json": "^2.3.1", @@ -215,7 +216,6 @@ "cytoscape-dagre": "^2.2.2", "d3": "3.5.17", "d3-array": "1.2.4", - "d3-cloud": "1.2.5", "d3-scale": "1.0.7", "d3-shape": "^1.1.0", "d3-time": "^1.1.0", @@ -671,7 +671,7 @@ "callsites": "^3.1.0", "chai": "3.5.0", "chance": "1.0.18", - "chromedriver": "^90.0.0", + "chromedriver": "^91.0.1", "clean-webpack-plugin": "^3.0.0", "cmd-shim": "^2.1.0", "compression-webpack-plugin": "^4.0.0", @@ -839,4 +839,4 @@ "yargs": "^15.4.1", "zlib": "^1.0.5" } -} +} \ No newline at end of file diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 3e17d471a3cac..f2510a2386aa2 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -12,6 +12,7 @@ filegroup( "//packages/kbn-apm-utils:build", "//packages/kbn-babel-code-parser:build", "//packages/kbn-babel-preset:build", + "//packages/kbn-common-utils:build", "//packages/kbn-config:build", "//packages/kbn-config-schema:build", "//packages/kbn-crypto:build", diff --git a/packages/kbn-common-utils/BUILD.bazel b/packages/kbn-common-utils/BUILD.bazel new file mode 100644 index 0000000000000..0244684973353 --- /dev/null +++ b/packages/kbn-common-utils/BUILD.bazel @@ -0,0 +1,82 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-common-utils" +PKG_REQUIRE_NAME = "@kbn/common-utils" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = ["**/*.test.*"], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-config-schema", + "@npm//load-json-file", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = DEPS + [":tsc"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-common-utils/README.md b/packages/kbn-common-utils/README.md new file mode 100644 index 0000000000000..7b64c9f18fe89 --- /dev/null +++ b/packages/kbn-common-utils/README.md @@ -0,0 +1,3 @@ +# @kbn/common-utils + +Shared common (client and server sie) utilities shared across packages and plugins. \ No newline at end of file diff --git a/packages/kbn-common-utils/jest.config.js b/packages/kbn-common-utils/jest.config.js new file mode 100644 index 0000000000000..08f1995c47423 --- /dev/null +++ b/packages/kbn-common-utils/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-common-utils'], +}; diff --git a/packages/kbn-common-utils/package.json b/packages/kbn-common-utils/package.json new file mode 100644 index 0000000000000..db99f4d6afb98 --- /dev/null +++ b/packages/kbn-common-utils/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/common-utils", + "main": "./target/index.js", + "browser": "./target/index.js", + "types": "./target/index.d.ts", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true +} \ No newline at end of file diff --git a/packages/kbn-common-utils/src/index.ts b/packages/kbn-common-utils/src/index.ts new file mode 100644 index 0000000000000..1b8bffe4bf158 --- /dev/null +++ b/packages/kbn-common-utils/src/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './json'; diff --git a/packages/kbn-common-utils/src/json/index.ts b/packages/kbn-common-utils/src/json/index.ts new file mode 100644 index 0000000000000..96c94df1bb48e --- /dev/null +++ b/packages/kbn-common-utils/src/json/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { JsonArray, JsonValue, JsonObject } from './typed_json'; diff --git a/src/plugins/kibana_utils/common/typed_json.ts b/packages/kbn-common-utils/src/json/typed_json.ts similarity index 100% rename from src/plugins/kibana_utils/common/typed_json.ts rename to packages/kbn-common-utils/src/json/typed_json.ts diff --git a/packages/kbn-common-utils/tsconfig.json b/packages/kbn-common-utils/tsconfig.json new file mode 100644 index 0000000000000..98f1b30c0d7ff --- /dev/null +++ b/packages/kbn-common-utils/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": true, + "outDir": "target", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-common-utils/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-test/src/jest/utils/router_helpers.tsx b/packages/kbn-test/src/jest/utils/router_helpers.tsx index e2245440274d1..85ef27488a4ce 100644 --- a/packages/kbn-test/src/jest/utils/router_helpers.tsx +++ b/packages/kbn-test/src/jest/utils/router_helpers.tsx @@ -8,18 +8,39 @@ import React, { Component, ComponentType } from 'react'; import { MemoryRouter, Route, withRouter } from 'react-router-dom'; -import * as H from 'history'; +import { History, LocationDescriptor } from 'history'; -export const WithMemoryRouter = (initialEntries: string[] = ['/'], initialIndex: number = 0) => ( - WrappedComponent: ComponentType -) => (props: any) => ( +const stringifyPath = (path: LocationDescriptor): string => { + if (typeof path === 'string') { + return path; + } + + return path.pathname || '/'; +}; + +const locationDescriptorToRoutePath = ( + paths: LocationDescriptor | LocationDescriptor[] +): string | string[] => { + if (Array.isArray(paths)) { + return paths.map((path: LocationDescriptor) => { + return stringifyPath(path); + }); + } + + return stringifyPath(paths); +}; + +export const WithMemoryRouter = ( + initialEntries: LocationDescriptor[] = ['/'], + initialIndex: number = 0 +) => (WrappedComponent: ComponentType) => (props: any) => ( ); export const WithRoute = ( - componentRoutePath: string | string[] = '/', + componentRoutePath: LocationDescriptor | LocationDescriptor[] = ['/'], onRouter = (router: any) => {} ) => (WrappedComponent: ComponentType) => { // Create a class component that will catch the router @@ -40,16 +61,16 @@ export const WithRoute = ( return (props: any) => ( } /> ); }; interface Router { - history: Partial; + history: Partial; route: { - location: H.Location; + location: LocationDescriptor; }; } diff --git a/packages/kbn-test/src/jest/utils/testbed/types.ts b/packages/kbn-test/src/jest/utils/testbed/types.ts index fdc000215c4f1..bba504951c0bc 100644 --- a/packages/kbn-test/src/jest/utils/testbed/types.ts +++ b/packages/kbn-test/src/jest/utils/testbed/types.ts @@ -8,6 +8,7 @@ import { Store } from 'redux'; import { ReactWrapper } from 'enzyme'; +import { LocationDescriptor } from 'history'; export type SetupFunc = (props?: any) => TestBed | Promise>; @@ -161,11 +162,11 @@ export interface MemoryRouterConfig { /** Flag to add or not the `MemoryRouter`. If set to `false`, there won't be any router and the component won't be wrapped on a ``. */ wrapComponent?: boolean; /** The React Router **initial entries** setting ([see documentation](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/MemoryRouter.md)) */ - initialEntries?: string[]; + initialEntries?: LocationDescriptor[]; /** The React Router **initial index** setting ([see documentation](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/MemoryRouter.md)) */ initialIndex?: number; /** The route **path** for the mounted component (defaults to `"/"`) */ - componentRoutePath?: string | string[]; + componentRoutePath?: LocationDescriptor | LocationDescriptor[]; /** A callBack that will be called with the React Router instance once mounted */ onRouter?: (router: any) => void; } diff --git a/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts b/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts new file mode 100644 index 0000000000000..25651a0dd2190 --- /dev/null +++ b/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { parseArchive } from './parse_archive'; + +jest.mock('fs/promises', () => ({ + readFile: jest.fn(), +})); + +const mockReadFile = jest.requireMock('fs/promises').readFile; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('parses archives with \\n', async () => { + mockReadFile.mockResolvedValue( + `{ + "foo": "abc" + }\n\n{ + "foo": "xyz" + }` + ); + + const archive = await parseArchive('mock'); + expect(archive).toMatchInlineSnapshot(` + Array [ + Object { + "foo": "abc", + }, + Object { + "foo": "xyz", + }, + ] + `); +}); + +it('parses archives with \\r\\n', async () => { + mockReadFile.mockResolvedValue( + `{ + "foo": "123" + }\r\n\r\n{ + "foo": "456" + }` + ); + + const archive = await parseArchive('mock'); + expect(archive).toMatchInlineSnapshot(` + Array [ + Object { + "foo": "123", + }, + Object { + "foo": "456", + }, + ] + `); +}); diff --git a/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts b/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts new file mode 100644 index 0000000000000..b6b85ba521525 --- /dev/null +++ b/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Fs from 'fs/promises'; + +export interface SavedObject { + id: string; + type: string; + [key: string]: unknown; +} + +export async function parseArchive(path: string): Promise { + return (await Fs.readFile(path, 'utf-8')) + .split(/\r?\n\r?\n/) + .filter((line) => !!line) + .map((line) => JSON.parse(line)); +} diff --git a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts index 88953cdbaed7c..4adae7d1cd031 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts @@ -16,25 +16,12 @@ import { ToolingLog, isAxiosResponseError, createFailError, REPO_ROOT } from '@k import { KbnClientRequester, uriencode, ReqOptions } from './kbn_client_requester'; import { KbnClientSavedObjects } from './kbn_client_saved_objects'; +import { parseArchive } from './import_export/parse_archive'; interface ImportApiResponse { success: boolean; [key: string]: unknown; } - -interface SavedObject { - id: string; - type: string; - [key: string]: unknown; -} - -async function parseArchive(path: string): Promise { - return (await Fs.readFile(path, 'utf-8')) - .split('\n\n') - .filter((line) => !!line) - .map((line) => JSON.parse(line)); -} - export class KbnClientImportExport { constructor( public readonly log: ToolingLog, diff --git a/packages/kbn-tinymath/src/index.js b/packages/kbn-tinymath/src/index.js index 9f1bb7b851463..6fde4c202e2a7 100644 --- a/packages/kbn-tinymath/src/index.js +++ b/packages/kbn-tinymath/src/index.js @@ -7,12 +7,11 @@ */ const { get } = require('lodash'); +const memoizeOne = require('memoize-one'); // eslint-disable-next-line import/no-unresolved const { parse: parseFn } = require('../grammar'); const { functions: includedFunctions } = require('./functions'); -module.exports = { parse, evaluate, interpret }; - function parse(input, options) { if (input == null) { throw new Error('Missing expression'); @@ -29,9 +28,11 @@ function parse(input, options) { } } +const memoizedParse = memoizeOne(parse); + function evaluate(expression, scope = {}, injectedFunctions = {}) { scope = scope || {}; - return interpret(parse(expression), scope, injectedFunctions); + return interpret(memoizedParse(expression), scope, injectedFunctions); } function interpret(node, scope, injectedFunctions) { @@ -79,3 +80,5 @@ function isOperable(args) { return typeof arg === 'number' && !isNaN(arg); }); } + +module.exports = { parse: memoizedParse, evaluate, interpret }; diff --git a/renovate.json5 b/renovate.json5 index f533eac479650..2a3b9d740ee93 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -39,7 +39,7 @@ packageNames: ['@elastic/charts'], reviewers: ['markov00', 'nickofthyme'], matchBaseBranches: ['master'], - labels: ['release_note:skip', 'v8.0.0', 'v7.14.0'], + labels: ['release_note:skip', 'v8.0.0', 'v7.14.0', 'auto-backport'], enabled: true, }, { diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 5658d3f626077..3ed164088bf5c 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -497,6 +497,56 @@ describe('#start()', () => { expect(getUrlForApp('app1', { path: 'deep/link///' })).toBe('/base-path/app/app1/deep/link'); }); + describe('deepLinkId option', () => { + it('ignores the deepLinkId parameter if it is unknown', async () => { + service.setup(setupDeps); + + service.setup(setupDeps); + const { getUrlForApp } = await service.start(startDeps); + + expect(getUrlForApp('app1', { deepLinkId: 'unkown-deep-link' })).toBe( + '/base-path/app/app1' + ); + }); + + it('creates URLs with deepLinkId parameter', async () => { + const { register } = service.setup(setupDeps); + + register( + Symbol(), + createApp({ + id: 'app1', + appRoute: '/custom/app-path', + deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }], + }) + ); + + const { getUrlForApp } = await service.start(startDeps); + + expect(getUrlForApp('app1', { deepLinkId: 'dl1' })).toBe( + '/base-path/custom/app-path/deep-link' + ); + }); + + it('creates URLs with deepLinkId and path parameters', async () => { + const { register } = service.setup(setupDeps); + + register( + Symbol(), + createApp({ + id: 'app1', + appRoute: '/custom/app-path', + deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }], + }) + ); + + const { getUrlForApp } = await service.start(startDeps); + expect(getUrlForApp('app1', { deepLinkId: 'dl1', path: 'foo/bar' })).toBe( + '/base-path/custom/app-path/deep-link/foo/bar' + ); + }); + }); + it('does not append trailing slash if hash is provided in path parameter', async () => { service.setup(setupDeps); const { getUrlForApp } = await service.start(startDeps); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 32d45b32c32ff..8c6090caabce1 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -282,8 +282,19 @@ export class ApplicationService { history: this.history!, getUrlForApp: ( appId, - { path, absolute = false }: { path?: string; absolute?: boolean } = {} + { + path, + absolute = false, + deepLinkId, + }: { path?: string; absolute?: boolean; deepLinkId?: string } = {} ) => { + if (deepLinkId) { + const deepLinkPath = getAppDeepLinkPath(availableMounters, appId, deepLinkId); + if (deepLinkPath) { + path = appendAppPath(deepLinkPath, path); + } + } + const relUrl = http.basePath.prepend(getAppUrl(availableMounters, appId, path)); return absolute ? relativeToAbsolute(relUrl) : relUrl; }, diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 60b0dbf158dd9..5803f2e3779ab 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -780,7 +780,10 @@ export interface ApplicationStart { * @param options.path - optional path inside application to deep link to * @param options.absolute - if true, will returns an absolute url instead of a relative one */ - getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean }): string; + getUrlForApp( + appId: string, + options?: { path?: string; absolute?: boolean; deepLinkId?: string } + ): string; /** * An observable that emits the current application id and each subsequent id update. diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 53428edf4b345..06277d9351922 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -142,7 +142,7 @@ export class DocLinksService { dataStreams: `${ELASTICSEARCH_DOCS}data-streams.html`, indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`, indexSettings: `${ELASTICSEARCH_DOCS}index-modules.html#index-modules-settings`, - indexTemplates: `${ELASTICSEARCH_DOCS}indices-templates.html`, + indexTemplates: `${ELASTICSEARCH_DOCS}index-templates.html`, mapping: `${ELASTICSEARCH_DOCS}mapping.html`, mappingAnalyzer: `${ELASTICSEARCH_DOCS}analyzer.html`, mappingCoerce: `${ELASTICSEARCH_DOCS}coerce.html`, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 235110aeb4633..d3426b50f7614 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -150,6 +150,7 @@ export interface ApplicationStart { getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean; + deepLinkId?: string; }): string; navigateToApp(appId: string, options?: NavigateToAppOptions): Promise; navigateToUrl(url: string): Promise; diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_oss_sample_saved_objects.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_oss_sample_saved_objects.zip deleted file mode 100644 index abb8dd2b6d491..0000000000000 Binary files a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_oss_sample_saved_objects.zip and /dev/null differ diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip new file mode 100644 index 0000000000000..ff02fcf204845 Binary files /dev/null and b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip differ diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index f9d8e7cc4fbaa..f4e0dd8fffcab 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -21,13 +21,37 @@ import { Root } from '../../../root'; const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; -const logFilePath = Path.join(__dirname, 'migration_test_kibana.log'); +const logFilePath = Path.join(__dirname, 'migration_test_kibana_from_v1.log'); const asyncUnlink = Util.promisify(Fs.unlink); async function removeLogFile() { // ignore errors if it doesn't exist await asyncUnlink(logFilePath).catch(() => void 0); } +const assertMigratedDocuments = (arr: any[], target: any[]) => target.every((v) => arr.includes(v)); + +function sortByTypeAndId(a: { type: string; id: string }, b: { type: string; id: string }) { + return a.type.localeCompare(b.type) || a.id.localeCompare(b.id); +} + +async function fetchDocuments(esClient: ElasticsearchClient, index: string) { + const { body } = await esClient.search({ + index, + body: { + query: { + match_all: {}, + }, + _source: ['type', 'id'], + }, + }); + + return body.hits.hits + .map((h) => ({ + ...h._source, + id: h._id, + })) + .sort(sortByTypeAndId); +} describe('migration v2', () => { let esServer: kbnTestServer.TestElasticsearchUtils; @@ -40,7 +64,7 @@ describe('migration v2', () => { adjustTimeout: (t: number) => jest.setTimeout(t), settings: { es: { - license: 'trial', + license: 'basic', dataArchive, }, }, @@ -51,8 +75,8 @@ describe('migration v2', () => { migrations: { skip: false, enableV2: true, - // There are 53 docs in fixtures. Batch size configured to enforce 3 migration steps. - batchSize: 20, + // There are 40 docs in fixtures. Batch size configured to enforce 3 migration steps. + batchSize: 15, }, logging: { appenders: { @@ -85,8 +109,7 @@ describe('migration v2', () => { coreStart = start; esClient = coreStart.elasticsearch.client.asInternalUser; }); - - await Promise.all([startEsPromise, startKibanaPromise]); + return await Promise.all([startEsPromise, startKibanaPromise]); }; const getExpectedVersionPerType = () => @@ -192,15 +215,19 @@ describe('migration v2', () => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/91107 - describe.skip('migrating from the same Kibana version', () => { + describe('migrating from the same Kibana version that used v1 migrations', () => { + const originalIndex = `.kibana_1`; // v1 migrations index const migratedIndex = `.kibana_${kibanaVersion}_001`; beforeAll(async () => { await removeLogFile(); await startServers({ - oss: true, - dataArchive: Path.join(__dirname, 'archives', '8.0.0_oss_sample_saved_objects.zip'), + oss: false, + dataArchive: Path.join( + __dirname, + 'archives', + '8.0.0_v1_migrations_sample_data_saved_objects.zip' + ), }); }); @@ -215,7 +242,6 @@ describe('migration v2', () => { }, { ignore: [404] } ); - const response = body[migratedIndex]; expect(response).toBeDefined(); @@ -225,17 +251,23 @@ describe('migration v2', () => { ]); }); - it('copies all the document of the previous index to the new one', async () => { + it('copies the documents from the previous index to the new one', async () => { + // original assertion on document count comparison (how atteched are we to this assertion?) const migratedIndexResponse = await esClient.count({ index: migratedIndex, }); const oldIndexResponse = await esClient.count({ - index: '.kibana_1', + index: originalIndex, }); // Use a >= comparison since once Kibana has started it might create new // documents like telemetry tasks expect(migratedIndexResponse.body.count).toBeGreaterThanOrEqual(oldIndexResponse.body.count); + + // new assertion against a document array comparison + const originalDocs = await fetchDocuments(esClient, originalIndex); + const migratedDocs = await fetchDocuments(esClient, migratedIndex); + expect(assertMigratedDocuments(migratedDocs, originalDocs)); }); it('migrates the documents to the highest version', async () => { diff --git a/src/dev/typescript/build_ts_refs.ts b/src/dev/typescript/build_ts_refs.ts index 2e25827996e45..26425b7a3e61d 100644 --- a/src/dev/typescript/build_ts_refs.ts +++ b/src/dev/typescript/build_ts_refs.ts @@ -13,12 +13,20 @@ import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; export const REF_CONFIG_PATHS = [Path.resolve(REPO_ROOT, 'tsconfig.refs.json')]; -export async function buildAllTsRefs(log: ToolingLog) { +export async function buildAllTsRefs(log: ToolingLog): Promise<{ failed: boolean }> { for (const path of REF_CONFIG_PATHS) { const relative = Path.relative(REPO_ROOT, path); log.debug(`Building TypeScript projects refs for ${relative}...`); - await execa(require.resolve('typescript/bin/tsc'), ['-b', relative, '--pretty'], { - cwd: REPO_ROOT, - }); + const { failed, stdout } = await execa( + require.resolve('typescript/bin/tsc'), + ['-b', relative, '--pretty'], + { + cwd: REPO_ROOT, + reject: false, + } + ); + log.info(stdout); + if (failed) return { failed }; } + return { failed: false }; } diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts index f95c230f44b9e..d9e9eb036fe0f 100644 --- a/src/dev/typescript/run_type_check_cli.ts +++ b/src/dev/typescript/run_type_check_cli.ts @@ -69,7 +69,11 @@ export async function runTypeCheckCli() { process.exit(); } - await buildAllTsRefs(log); + const { failed } = await buildAllTsRefs(log); + if (failed) { + log.error('Unable to build TS project refs'); + process.exit(1); + } const tscArgs = [ // composite project cannot be used with --noEmit diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 1cfa39d5e0e79..e5f89bd6a8e90 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -132,7 +132,7 @@ export function DashboardTopNav({ const trackUiMetric = usageCollection?.reportUiCounter.bind( usageCollection, - DashboardConstants.DASHBOARDS_ID + DashboardConstants.DASHBOARD_ID ); useEffect(() => { @@ -163,6 +163,7 @@ export function DashboardTopNav({ notifications: core.notifications, overlays: core.overlays, SavedObjectFinder: getSavedObjectFinder(core.savedObjects, uiSettings), + reportUiCounter: usageCollection?.reportUiCounter, }), })); } @@ -174,6 +175,7 @@ export function DashboardTopNav({ core.savedObjects, core.overlays, uiSettings, + usageCollection, ]); const createNewVisType = useCallback( @@ -183,7 +185,7 @@ export function DashboardTopNav({ if (visType) { if (trackUiMetric) { - trackUiMetric(METRIC_TYPE.CLICK, visType.name); + trackUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`); } if ('aliasPath' in visType) { diff --git a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx index 90cf0fcd571a1..74d725bb4d104 100644 --- a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx @@ -51,7 +51,7 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => { const trackUiMetric = usageCollection?.reportUiCounter.bind( usageCollection, - DashboardConstants.DASHBOARDS_ID + DashboardConstants.DASHBOARD_ID ); const createNewAggsBasedVis = useCallback( diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.ts b/src/plugins/data/common/es_query/kuery/ast/ast.ts index 5b22e3b3a3e0e..be82128969968 100644 --- a/src/plugins/data/common/es_query/kuery/ast/ast.ts +++ b/src/plugins/data/common/es_query/kuery/ast/ast.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { JsonObject } from '@kbn/common-utils'; import { nodeTypes } from '../node_types/index'; import { KQLSyntaxError } from '../kuery_syntax_error'; import { KueryNode, DslQuery, KueryParseOptions } from '../types'; @@ -13,7 +14,6 @@ import { IIndexPattern } from '../../../index_patterns/types'; // @ts-ignore import { parse as parseKuery } from './_generated_/kuery'; -import { JsonObject } from '../../../../../kibana_utils/common'; const fromExpression = ( expression: string | DslQuery, diff --git a/src/plugins/data/common/es_query/kuery/node_types/named_arg.ts b/src/plugins/data/common/es_query/kuery/node_types/named_arg.ts index c65f195040b18..b1b202e4323af 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/named_arg.ts +++ b/src/plugins/data/common/es_query/kuery/node_types/named_arg.ts @@ -7,10 +7,10 @@ */ import _ from 'lodash'; +import { JsonObject } from '@kbn/common-utils'; import * as ast from '../ast'; import { nodeTypes } from '../node_types'; import { NamedArgTypeBuildNode } from './types'; -import { JsonObject } from '../../../../../kibana_utils/common'; export function buildNode(name: string, value: any): NamedArgTypeBuildNode { const argumentNode = diff --git a/src/plugins/data/common/es_query/kuery/node_types/types.ts b/src/plugins/data/common/es_query/kuery/node_types/types.ts index 196890ed0f7a3..b3247a0ad8dc2 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/types.ts +++ b/src/plugins/data/common/es_query/kuery/node_types/types.ts @@ -10,8 +10,8 @@ * WARNING: these typings are incomplete */ +import { JsonValue } from '@kbn/common-utils'; import { IIndexPattern } from '../../../index_patterns'; -import { JsonValue } from '../../../../../kibana_utils/common'; import { KueryNode } from '..'; export type FunctionName = diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts index 2aa0d346afe34..523bbe1f01018 100644 --- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts @@ -174,6 +174,57 @@ const nestedTermResponse = { status: 200, }; +const exhaustiveNestedTermResponse = { + took: 10, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: 14005, + max_score: 0, + hits: [], + }, + aggregations: { + '1': { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 8325, + buckets: [ + { + '2': { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'ios', doc_count: 2850 }, + { key: 'win xp', doc_count: 2830 }, + { key: '__missing__', doc_count: 1430 }, + ], + }, + key: 'US-with-dash', + doc_count: 2850, + }, + { + '2': { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'ios', doc_count: 1850 }, + { key: 'win xp', doc_count: 1830 }, + { key: '__missing__', doc_count: 130 }, + ], + }, + key: 'IN-with-dash', + doc_count: 2830, + }, + ], + }, + }, + status: 200, +}; + const nestedTermResponseNoResults = { took: 10, timed_out: false, @@ -326,6 +377,17 @@ describe('Terms Agg Other bucket helper', () => { } }); + test('does not build query if sum_other_doc_count is 0 (exhaustive terms)', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + expect( + buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[1] as IBucketAggConfig, + exhaustiveNestedTermResponse + ) + ).toBeFalsy(); + }); + test('excludes exists filter for scripted fields', () => { const aggConfigs = getAggConfigs(nestedTerm.aggs); aggConfigs.aggs[1].params.field.scripted = true; diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts index 372d487bcf7a3..2a1cd873f6282 100644 --- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts +++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts @@ -156,6 +156,7 @@ export const buildOtherBucketAgg = ( }; let noAggBucketResults = false; + let exhaustiveBuckets = true; // recursively create filters for all parent aggregation buckets const walkBucketTree = ( @@ -175,6 +176,9 @@ export const buildOtherBucketAgg = ( const newAggIndex = aggIndex + 1; const newAgg = bucketAggs[newAggIndex]; const currentAgg = bucketAggs[aggIndex]; + if (aggIndex === index && agg && agg.sum_other_doc_count > 0) { + exhaustiveBuckets = false; + } if (aggIndex < index) { each(agg.buckets, (bucket: any, bucketObjKey) => { const bucketKey = currentAgg.getKey( @@ -223,7 +227,7 @@ export const buildOtherBucketAgg = ( walkBucketTree(0, response.aggregations, bucketAggs[0].id, [], ''); // bail if there were no bucket results - if (noAggBucketResults) { + if (noAggBucketResults || exhaustiveBuckets) { return false; } diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index ba873952c9841..078dd3a9b7c5a 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -276,9 +276,8 @@ export { DuplicateIndexPatternError } from '../common/index_patterns/errors'; * Autocomplete query suggestions: */ -export { +export type { QuerySuggestion, - QuerySuggestionTypes, QuerySuggestionGetFn, QuerySuggestionGetFnArgs, QuerySuggestionBasic, @@ -286,6 +285,7 @@ export { AutocompleteStart, } from './autocomplete'; +export { QuerySuggestionTypes } from './autocomplete'; /* * Search: */ @@ -320,25 +320,23 @@ import { tabifyGetColumns, } from '../common'; -export { +export { AggGroupLabels, AggGroupNames, METRIC_TYPES, BUCKET_TYPES } from '../common'; + +export type { // aggs AggConfigSerialized, - AggGroupLabels, AggGroupName, - AggGroupNames, AggFunctionsMapping, AggParam, AggParamOption, AggParamType, AggConfigOptions, - BUCKET_TYPES, EsaggsExpressionFunctionDefinition, IAggConfig, IAggConfigs, IAggType, IFieldParamType, IMetricAggType, - METRIC_TYPES, OptionedParamType, OptionedValueProp, ParsedInterval, @@ -352,30 +350,23 @@ export { export type { AggConfigs, AggConfig } from '../common'; -export { +export type { // search ES_SEARCH_STRATEGY, EsQuerySortValue, - extractSearchSourceReferences, - getEsPreference, - getSearchParamsFromRequest, IEsSearchRequest, IEsSearchResponse, IKibanaSearchRequest, IKibanaSearchResponse, - injectSearchSourceReferences, ISearchSetup, ISearchStart, ISearchStartSearchSource, ISearchGeneric, ISearchSource, - parseSearchSourceJSON, SearchInterceptor, SearchInterceptorDeps, SearchRequest, SearchSourceFields, - SortDirection, - SearchSessionState, // expression functions and types EsdslExpressionFunctionDefinition, EsRawResponseExpressionTypeDefinition, @@ -386,11 +377,21 @@ export { TimeoutErrorMode, PainlessError, Reason, + WaitUntilNextSessionCompletesOptions, +} from './search'; + +export { + parseSearchSourceJSON, + injectSearchSourceReferences, + extractSearchSourceReferences, + getEsPreference, + getSearchParamsFromRequest, noSearchSessionStorageCapabilityMessage, SEARCH_SESSIONS_MANAGEMENT_ID, waitUntilNextSessionCompletes$, - WaitUntilNextSessionCompletesOptions, isEsError, + SearchSessionState, + SortDirection, } from './search'; export type { @@ -438,33 +439,36 @@ export const search = { * UI components */ -export { - SearchBar, +export type { SearchBarProps, StatefulSearchBarProps, IndexPatternSelectProps, - QueryStringInput, QueryStringInputProps, } from './ui'; +export { QueryStringInput, SearchBar } from './ui'; + /** * Types to be shared externally * @public */ -export { Filter, Query, RefreshInterval, TimeRange } from '../common'; +export type { Filter, Query, RefreshInterval, TimeRange } from '../common'; export { createSavedQueryService, connectToQueryState, syncQueryStateWithUrl, - QueryState, getDefaultQuery, FilterManager, + TimeHistory, +} from './query'; + +export type { + QueryState, SavedQuery, SavedQueryService, SavedQueryTimeFilter, InputTimeRange, - TimeHistory, TimefilterContract, TimeHistoryContract, QueryStateChange, @@ -472,7 +476,7 @@ export { AutoRefreshDoneFn, } from './query'; -export { AggsStart } from './search/aggs'; +export type { AggsStart } from './search/aggs'; export { getTime, @@ -496,7 +500,7 @@ export function plugin(initializerContext: PluginInitializerContext>; -export type Start = jest.Mocked>; +export type Setup = jest.Mocked>; +export type Start = jest.Mocked>; const autocompleteSetupMock: jest.Mocked = { getQuerySuggestions: jest.fn(), diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 67534577d99fc..d56727b468da6 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -53,6 +53,7 @@ import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource as ISearchSource_2 } from 'src/plugins/data/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IUiSettingsClient } from 'src/core/public'; +import { JsonValue } from '@kbn/common-utils'; import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { Location } from 'history'; import { LocationDescriptorObject } from 'history'; @@ -67,7 +68,7 @@ import { Observable } from 'rxjs'; import { PackageInfo } from '@kbn/config'; import { Path } from 'history'; import { PeerCertificate } from 'tls'; -import { Plugin as Plugin_2 } from 'src/core/public'; +import { Plugin } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public'; import { PopoverAnchorPosition } from '@elastic/eui'; @@ -621,6 +622,22 @@ export type CustomFilter = Filter & { query: any; }; +// Warning: (ae-forgotten-export) The symbol "DataSetupDependencies" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataStartDependencies" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "DataPublicPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class DataPlugin implements Plugin { + // Warning: (ae-forgotten-export) The symbol "ConfigSchema" needs to be exported by the entry point index.d.ts + constructor(initializerContext: PluginInitializerContext_2); + // (undocumented) + setup(core: CoreSetup, { bfetch, expressions, uiActions, usageCollection, inspector }: DataSetupDependencies): DataPublicPluginSetup; + // (undocumented) + start(core: CoreStart_2, { uiActions }: DataStartDependencies): DataPublicPluginStart; + // (undocumented) + stop(): void; + } + // Warning: (ae-missing-release-tag) "DataPublicPluginSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -840,7 +857,7 @@ export const esFilters: { export const esKuery: { nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("../../kibana_utils/common").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; }; // Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2004,27 +2021,11 @@ export type PhrasesFilter = Filter & { meta: PhrasesFilterMeta; }; -// Warning: (ae-forgotten-export) The symbol "DataSetupDependencies" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "DataStartDependencies" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "DataPublicPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export class Plugin implements Plugin_2 { - // Warning: (ae-forgotten-export) The symbol "ConfigSchema" needs to be exported by the entry point index.d.ts - constructor(initializerContext: PluginInitializerContext_2); - // (undocumented) - setup(core: CoreSetup, { bfetch, expressions, uiActions, usageCollection, inspector }: DataSetupDependencies): DataPublicPluginSetup; - // (undocumented) - start(core: CoreStart_2, { uiActions }: DataStartDependencies): DataPublicPluginStart; - // (undocumented) - stop(): void; - } - // Warning: (ae-forgotten-export) The symbol "PluginInitializerContext" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "plugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function plugin(initializerContext: PluginInitializerContext): Plugin; +export function plugin(initializerContext: PluginInitializerContext): DataPlugin; // Warning: (ae-missing-release-tag) "Query" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2772,20 +2773,20 @@ export interface WaitUntilNextSessionCompletesOptions { // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:434:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:432:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:435:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 783bd8d2fcd0e..c2b533bc42dc6 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -38,6 +38,7 @@ import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; import { IUiSettingsClient } from 'src/core/server'; import { IUiSettingsClient as IUiSettingsClient_3 } from 'kibana/server'; +import { JsonValue } from '@kbn/common-utils'; import { KibanaRequest } from 'src/core/server'; import { KibanaRequest as KibanaRequest_2 } from 'kibana/server'; import { Logger } from 'src/core/server'; @@ -460,7 +461,7 @@ export const esFilters: { export const esKuery: { nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("../../kibana_utils/common").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; }; // Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 1214625fe530f..8cf2de8c80743 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -14,6 +14,7 @@ import deepEqual from 'fast-deep-equal'; import { buildContextMenuForActions, UiActionsService, Action } from '../ui_actions'; import { CoreStart, OverlayStart } from '../../../../../core/public'; import { toMountPoint } from '../../../../kibana_react/public'; +import { UsageCollectionStart } from '../../../../usage_collection/public'; import { Start as InspectorStartContract } from '../inspector'; import { @@ -62,6 +63,7 @@ interface Props { SavedObjectFinder: React.ComponentType; stateTransfer?: EmbeddableStateTransfer; hideHeader?: boolean; + reportUiCounter?: UsageCollectionStart['reportUiCounter']; } interface State { @@ -312,7 +314,8 @@ export class EmbeddablePanel extends React.Component { this.props.getAllEmbeddableFactories, this.props.overlays, this.props.notifications, - this.props.SavedObjectFinder + this.props.SavedObjectFinder, + this.props.reportUiCounter ), inspectPanel: new InspectPanelAction(this.props.inspector), removePanel: new RemovePanelAction(), diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts index 8b6f81a199c44..49be1c3ce0123 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts @@ -13,6 +13,7 @@ import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; import { ViewMode } from '../../../../types'; import { openAddPanelFlyout } from './open_add_panel_flyout'; import { IContainer } from '../../../../containers'; +import { UsageCollectionStart } from '../../../../../../../usage_collection/public'; export const ACTION_ADD_PANEL = 'ACTION_ADD_PANEL'; @@ -29,7 +30,8 @@ export class AddPanelAction implements Action { private readonly getAllFactories: EmbeddableStart['getEmbeddableFactories'], private readonly overlays: OverlayStart, private readonly notifications: NotificationsStart, - private readonly SavedObjectFinder: React.ComponentType + private readonly SavedObjectFinder: React.ComponentType, + private readonly reportUiCounter?: UsageCollectionStart['reportUiCounter'] ) {} public getDisplayName() { @@ -60,6 +62,7 @@ export class AddPanelAction implements Action { overlays: this.overlays, notifications: this.notifications, SavedObjectFinder: this.SavedObjectFinder, + reportUiCounter: this.reportUiCounter, }); } } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index 6d6a68d7e5e2a..eb4f0b30c5110 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -9,15 +9,17 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { ReactElement } from 'react'; -import { CoreSetup } from 'src/core/public'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { CoreSetup, SavedObjectAttributes, SimpleSavedObject } from 'src/core/public'; import { EuiContextMenuItem, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; -import { EmbeddableStart } from 'src/plugins/embeddable/public'; +import { EmbeddableFactory, EmbeddableStart } from 'src/plugins/embeddable/public'; import { IContainer } from '../../../../containers'; import { EmbeddableFactoryNotFoundError } from '../../../../errors'; import { SavedObjectFinderCreateNew } from './saved_object_finder_create_new'; import { SavedObjectEmbeddableInput } from '../../../../embeddables'; +import { UsageCollectionStart } from '../../../../../../../usage_collection/public'; interface Props { onClose: () => void; @@ -27,6 +29,7 @@ interface Props { notifications: CoreSetup['notifications']; SavedObjectFinder: React.ComponentType; showCreateNewMenu?: boolean; + reportUiCounter?: UsageCollectionStart['reportUiCounter']; } interface State { @@ -84,7 +87,12 @@ export class AddPanelFlyout extends React.Component { } }; - public onAddPanel = async (savedObjectId: string, savedObjectType: string, name: string) => { + public onAddPanel = async ( + savedObjectId: string, + savedObjectType: string, + name: string, + so: SimpleSavedObject + ) => { const factoryForSavedObjectType = [...this.props.getAllFactories()].find( (factory) => factory.savedObjectMetaData && factory.savedObjectMetaData.type === savedObjectType @@ -98,9 +106,27 @@ export class AddPanelFlyout extends React.Component { { savedObjectId } ); + this.doTelemetryForAddEvent(this.props.container.type, factoryForSavedObjectType, so); + this.showToast(name); }; + private doTelemetryForAddEvent( + appName: string, + factoryForSavedObjectType: EmbeddableFactory, + so: SimpleSavedObject + ) { + const { reportUiCounter } = this.props; + + if (reportUiCounter) { + const type = factoryForSavedObjectType.savedObjectMetaData?.getSavedObjectSubType + ? factoryForSavedObjectType.savedObjectMetaData.getSavedObjectSubType(so) + : factoryForSavedObjectType.type; + + reportUiCounter(appName, METRIC_TYPE.CLICK, `${type}:add`); + } + } + private getCreateMenuItems(): ReactElement[] { return [...this.props.getAllFactories()] .filter( diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx index f0c6e81644b3d..fe54b3d134aa0 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx @@ -12,6 +12,7 @@ import { EmbeddableStart } from '../../../../../plugin'; import { toMountPoint } from '../../../../../../../kibana_react/public'; import { IContainer } from '../../../../containers'; import { AddPanelFlyout } from './add_panel_flyout'; +import { UsageCollectionStart } from '../../../../../../../usage_collection/public'; export function openAddPanelFlyout(options: { embeddable: IContainer; @@ -21,6 +22,7 @@ export function openAddPanelFlyout(options: { notifications: NotificationsStart; SavedObjectFinder: React.ComponentType; showCreateNewMenu?: boolean; + reportUiCounter?: UsageCollectionStart['reportUiCounter']; }): OverlayRef { const { embeddable, @@ -30,6 +32,7 @@ export function openAddPanelFlyout(options: { notifications, SavedObjectFinder, showCreateNewMenu, + reportUiCounter, } = options; const flyoutSession = overlays.openFlyout( toMountPoint( @@ -43,6 +46,7 @@ export function openAddPanelFlyout(options: { getFactory={getFactory} getAllFactories={getAllFactories} notifications={notifications} + reportUiCounter={reportUiCounter} SavedObjectFinder={SavedObjectFinder} showCreateNewMenu={showCreateNewMenu} /> diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 2a577e6167be5..af708f9a5e659 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -63,6 +63,7 @@ import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UiComponent } from 'src/plugins/kibana_utils/public'; +import { UiCounterMetricType } from '@kbn/analytics'; import { UnregisterCallback } from 'history'; import { URL } from 'url'; import { UserProvidedValues } from 'src/core/server/types'; @@ -95,7 +96,7 @@ export interface Adapters { // @public (undocumented) export class AddPanelAction implements Action_3 { // Warning: (ae-forgotten-export) The symbol "React" needs to be exported by the entry point index.d.ts - constructor(getFactory: EmbeddableStart_2['getEmbeddableFactory'], getAllFactories: EmbeddableStart_2['getEmbeddableFactories'], overlays: OverlayStart_2, notifications: NotificationsStart_2, SavedObjectFinder: React_2.ComponentType); + constructor(getFactory: EmbeddableStart_2['getEmbeddableFactory'], getAllFactories: EmbeddableStart_2['getEmbeddableFactories'], overlays: OverlayStart_2, notifications: NotificationsStart_2, SavedObjectFinder: React_2.ComponentType, reportUiCounter?: ((appName: string, type: import("@kbn/analytics").UiCounterMetricType, eventNames: string | string[], count?: number | undefined) => void) | undefined); // (undocumented) execute(context: ActionExecutionContext_2): Promise; // (undocumented) @@ -729,6 +730,7 @@ export function openAddPanelFlyout(options: { notifications: NotificationsStart_2; SavedObjectFinder: React.ComponentType; showCreateNewMenu?: boolean; + reportUiCounter?: UsageCollectionStart['reportUiCounter']; }): OverlayRef_2; // Warning: (ae-missing-release-tag) "OutputSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -890,6 +892,7 @@ export const withEmbeddableSubscription: . + */ + +export const PageError: React.FunctionComponent = ({ + title, + error, + actions, + isCentered, + ...rest +}) => { + const { + error: errorString, + cause, // wrapEsError() on the server adds a "cause" array + message, + } = error; + + const errorContent = ( + + {title}} + body={ + <> + {cause ? message || errorString :

{message || errorString}

} + {cause && ( + <> + +
    + {cause.map((causeMsg, i) => ( +
  • {causeMsg}
  • + ))} +
+ + )} + + } + iconType="alert" + actions={actions} + {...rest} + /> +
+ ); + + if (isCentered) { + return
{errorContent}
; + } + + return errorContent; +}; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx index c0b3533c8594b..a1652b4e153f5 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx @@ -8,12 +8,7 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import React, { Fragment } from 'react'; - -export interface Error { - error: string; - cause?: string[]; - message?: string; -} +import { Error } from '../types'; interface Props { title: React.ReactNode; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts index 089dc890c3e6c..e63d98512a2cd 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts @@ -12,8 +12,8 @@ export { AuthorizationProvider, AuthorizationContext, SectionError, - Error, + PageError, useAuthorizationContext, } from './components'; -export { Privileges, MissingPrivileges } from './types'; +export { Privileges, MissingPrivileges, Error } from './types'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts index b10318aa415b3..70b54b0b6e425 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts @@ -14,3 +14,9 @@ export interface Privileges { hasAllPrivileges: boolean; missingPrivileges: MissingPrivileges; } + +export interface Error { + error: string; + cause?: string[]; + message?: string; +} diff --git a/src/plugins/es_ui_shared/public/authorization/index.ts b/src/plugins/es_ui_shared/public/authorization/index.ts index 483fffd9c4859..f68ad3da2a4b5 100644 --- a/src/plugins/es_ui_shared/public/authorization/index.ts +++ b/src/plugins/es_ui_shared/public/authorization/index.ts @@ -14,6 +14,7 @@ export { NotAuthorizedSection, Privileges, SectionError, + PageError, useAuthorizationContext, WithPrivileges, } from '../../__packages_do_not_import__/authorization'; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index b46a23994fe93..7b9013c043a0e 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -40,6 +40,7 @@ export { Privileges, MissingPrivileges, SectionError, + PageError, Error, useAuthorizationContext, } from './authorization'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 181bd9959c1bb..fb334afb22b13 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -18,7 +18,7 @@ const DEFAULT_OPTIONS = { stripEmptyFields: true, }; -interface UseFormReturn { +export interface UseFormReturn { form: FormHook; } diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 69687f75f3098..feff425cc48ed 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -834,8 +834,8 @@ describe('Execution', () => { expect((chain[0].arguments.val[0] as ExpressionAstExpression).chain[0].debug!.args).toEqual( { - name: 'foo', - value: 5, + name: ['foo'], + value: [5], } ); }); diff --git a/src/plugins/expressions/common/expression_functions/specs/index.ts b/src/plugins/expressions/common/expression_functions/specs/index.ts index 20a6f9aac4567..e808021f75180 100644 --- a/src/plugins/expressions/common/expression_functions/specs/index.ts +++ b/src/plugins/expressions/common/expression_functions/specs/index.ts @@ -12,8 +12,10 @@ export * from './var_set'; export * from './var'; export * from './theme'; export * from './cumulative_sum'; +export * from './overall_metric'; export * from './derivative'; export * from './moving_average'; export * from './ui_setting'; export { mapColumn, MapColumnArguments } from './map_column'; export { math, MathArguments, MathInput } from './math'; +export { mathColumn, MathColumnArguments } from './math_column'; diff --git a/src/plugins/expressions/common/expression_functions/specs/math_column.ts b/src/plugins/expressions/common/expression_functions/specs/math_column.ts new file mode 100644 index 0000000000000..0ff8faf3ce55a --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/math_column.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../types'; +import { math, MathArguments } from './math'; +import { Datatable, DatatableColumn, getType } from '../../expression_types'; + +export type MathColumnArguments = MathArguments & { + id: string; + name?: string; + copyMetaFrom?: string | null; +}; + +export const mathColumn: ExpressionFunctionDefinition< + 'mathColumn', + Datatable, + MathColumnArguments, + Datatable +> = { + name: 'mathColumn', + type: 'datatable', + inputTypes: ['datatable'], + help: i18n.translate('expressions.functions.mathColumnHelpText', { + defaultMessage: + 'Adds a column calculated as the result of other columns. ' + + 'Changes are made only when you provide arguments.' + + 'See also {alterColumnFn} and {staticColumnFn}.', + values: { + alterColumnFn: '`alterColumn`', + staticColumnFn: '`staticColumn`', + }, + }), + args: { + ...math.args, + id: { + types: ['string'], + help: i18n.translate('expressions.functions.mathColumn.args.idHelpText', { + defaultMessage: 'id of the resulting column. Must be unique.', + }), + required: true, + }, + name: { + types: ['string'], + aliases: ['_', 'column'], + help: i18n.translate('expressions.functions.mathColumn.args.nameHelpText', { + defaultMessage: 'The name of the resulting column. Names are not required to be unique.', + }), + required: true, + }, + copyMetaFrom: { + types: ['string', 'null'], + help: i18n.translate('expressions.functions.mathColumn.args.copyMetaFromHelpText', { + defaultMessage: + "If set, the meta object from the specified column id is copied over to the specified target column. If the column doesn't exist it silently fails.", + }), + required: false, + default: null, + }, + }, + fn: (input, args, context) => { + const columns = [...input.columns]; + const existingColumnIndex = columns.findIndex(({ id }) => { + return id === args.id; + }); + if (existingColumnIndex > -1) { + throw new Error('ID must be unique'); + } + + const newRows = input.rows.map((row) => { + return { + ...row, + [args.id]: math.fn( + { + type: 'datatable', + columns: input.columns, + rows: [row], + }, + { + expression: args.expression, + onError: args.onError, + }, + context + ), + }; + }); + const type = newRows.length ? getType(newRows[0][args.id]) : 'null'; + const newColumn: DatatableColumn = { + id: args.id, + name: args.name ?? args.id, + meta: { type, params: { id: type } }, + }; + if (args.copyMetaFrom) { + const metaSourceFrom = columns.find(({ id }) => id === args.copyMetaFrom); + newColumn.meta = { ...newColumn.meta, ...(metaSourceFrom?.meta || {}) }; + } + + columns.push(newColumn); + + return { + type: 'datatable', + columns, + rows: newRows, + } as Datatable; + }, +}; diff --git a/src/plugins/expressions/common/expression_functions/specs/overall_metric.ts b/src/plugins/expressions/common/expression_functions/specs/overall_metric.ts new file mode 100644 index 0000000000000..e42112d3a23ed --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/overall_metric.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../types'; +import { Datatable } from '../../expression_types'; +import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers'; + +export interface OverallMetricArgs { + by?: string[]; + inputColumnId: string; + outputColumnId: string; + outputColumnName?: string; + metric: 'sum' | 'min' | 'max' | 'average'; +} + +export type ExpressionFunctionOverallMetric = ExpressionFunctionDefinition< + 'overall_metric', + Datatable, + OverallMetricArgs, + Datatable +>; + +function getValueAsNumberArray(value: unknown) { + if (Array.isArray(value)) { + return value.map((innerVal) => Number(innerVal)); + } else { + return [Number(value)]; + } +} + +/** + * Calculates the overall metric of a specified column in the data table. + * + * Also supports multiple series in a single data table - use the `by` argument + * to specify the columns to split the calculation by. + * For each unique combination of all `by` columns a separate overall metric will be calculated. + * The order of rows won't be changed - this function is not modifying any existing columns, it's only + * adding the specified `outputColumnId` column to every row of the table without adding or removing rows. + * + * Behavior: + * * Will write the overall metric of `inputColumnId` into `outputColumnId` + * * If provided will use `outputColumnName` as name for the newly created column. Otherwise falls back to `outputColumnId` + * * Each cell will contain the calculated metric based on the values of all cells belonging to the current series. + * + * Edge cases: + * * Will return the input table if `inputColumnId` does not exist + * * Will throw an error if `outputColumnId` exists already in provided data table + * * If the row value contains `null` or `undefined`, it will be ignored and overwritten with the overall metric of + * all cells of the same series. + * * For all values besides `null` and `undefined`, the value will be cast to a number before it's added to the + * overall metric of the current series - if this results in `NaN` (like in case of objects), all cells of the + * current series will be set to `NaN`. + * * To determine separate series defined by the `by` columns, the values of these columns will be cast to strings + * before comparison. If the values are objects, the return value of their `toString` method will be used for comparison. + * Missing values (`null` and `undefined`) will be treated as empty strings. + */ +export const overallMetric: ExpressionFunctionOverallMetric = { + name: 'overall_metric', + type: 'datatable', + + inputTypes: ['datatable'], + + help: i18n.translate('expressions.functions.overallMetric.help', { + defaultMessage: 'Calculates the overall sum, min, max or average of a column in a data table', + }), + + args: { + by: { + help: i18n.translate('expressions.functions.overallMetric.args.byHelpText', { + defaultMessage: 'Column to split the overall calculation by', + }), + multi: true, + types: ['string'], + required: false, + }, + metric: { + help: i18n.translate('expressions.functions.overallMetric.metricHelpText', { + defaultMessage: 'Metric to calculate', + }), + types: ['string'], + options: ['sum', 'min', 'max', 'average'], + }, + inputColumnId: { + help: i18n.translate('expressions.functions.overallMetric.args.inputColumnIdHelpText', { + defaultMessage: 'Column to calculate the overall metric of', + }), + types: ['string'], + required: true, + }, + outputColumnId: { + help: i18n.translate('expressions.functions.overallMetric.args.outputColumnIdHelpText', { + defaultMessage: 'Column to store the resulting overall metric in', + }), + types: ['string'], + required: true, + }, + outputColumnName: { + help: i18n.translate('expressions.functions.overallMetric.args.outputColumnNameHelpText', { + defaultMessage: 'Name of the column to store the resulting overall metric in', + }), + types: ['string'], + required: false, + }, + }, + + fn(input, { by, inputColumnId, outputColumnId, outputColumnName, metric }) { + const resultColumns = buildResultColumns( + input, + outputColumnId, + inputColumnId, + outputColumnName + ); + + if (!resultColumns) { + return input; + } + + const accumulators: Partial> = {}; + const valueCounter: Partial> = {}; + input.rows.forEach((row) => { + const bucketIdentifier = getBucketIdentifier(row, by); + const accumulatorValue = accumulators[bucketIdentifier] ?? 0; + + const currentValue = row[inputColumnId]; + if (currentValue != null) { + const currentNumberValues = getValueAsNumberArray(currentValue); + switch (metric) { + case 'average': + valueCounter[bucketIdentifier] = + (valueCounter[bucketIdentifier] ?? 0) + currentNumberValues.length; + case 'sum': + accumulators[bucketIdentifier] = + accumulatorValue + currentNumberValues.reduce((a, b) => a + b, 0); + break; + case 'min': + accumulators[bucketIdentifier] = Math.min(accumulatorValue, ...currentNumberValues); + break; + case 'max': + accumulators[bucketIdentifier] = Math.max(accumulatorValue, ...currentNumberValues); + break; + } + } + }); + if (metric === 'average') { + Object.keys(accumulators).forEach((bucketIdentifier) => { + accumulators[bucketIdentifier] = + accumulators[bucketIdentifier]! / valueCounter[bucketIdentifier]!; + }); + } + return { + ...input, + columns: resultColumns, + rows: input.rows.map((row) => { + const newRow = { ...row }; + const bucketIdentifier = getBucketIdentifier(row, by); + newRow[outputColumnId] = accumulators[bucketIdentifier]; + + return newRow; + }), + }; + }, +}; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts new file mode 100644 index 0000000000000..bc6699a2b689b --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { mathColumn } from '../math_column'; +import { functionWrapper, testTable } from './utils'; + +describe('mathColumn', () => { + const fn = functionWrapper(mathColumn); + + it('throws if the id is used', () => { + expect(() => fn(testTable, { id: 'price', name: 'price', expression: 'price * 2' })).toThrow( + `ID must be unique` + ); + }); + + it('applies math to each row by id', () => { + const result = fn(testTable, { id: 'output', name: 'output', expression: 'quantity * price' }); + expect(result.columns).toEqual([ + ...testTable.columns, + { id: 'output', name: 'output', meta: { params: { id: 'number' }, type: 'number' } }, + ]); + expect(result.rows[0]).toEqual({ + in_stock: true, + name: 'product1', + output: 60500, + price: 605, + quantity: 100, + time: 1517842800950, + }); + }); + + it('handles onError', () => { + const args = { + id: 'output', + name: 'output', + expression: 'quantity / 0', + }; + expect(() => fn(testTable, args)).toThrowError(`Cannot divide by 0`); + expect(() => fn(testTable, { ...args, onError: 'throw' })).toThrow(); + expect(fn(testTable, { ...args, onError: 'zero' }).rows[0].output).toEqual(0); + expect(fn(testTable, { ...args, onError: 'false' }).rows[0].output).toEqual(false); + expect(fn(testTable, { ...args, onError: 'null' }).rows[0].output).toEqual(null); + }); + + it('should copy over the meta information from the specified column', async () => { + const result = await fn( + { + ...testTable, + columns: [ + ...testTable.columns, + { + id: 'myId', + name: 'myName', + meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } }, + }, + ], + rows: testTable.rows.map((row) => ({ ...row, myId: Date.now() })), + }, + { id: 'output', name: 'name', copyMetaFrom: 'myId', expression: 'price + 2' } + ); + + expect(result.type).toBe('datatable'); + expect(result.columns[result.columns.length - 1]).toEqual({ + id: 'output', + name: 'name', + meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } }, + }); + }); +}); diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/overall_metric.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/overall_metric.test.ts new file mode 100644 index 0000000000000..30354c4e54dc7 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/tests/overall_metric.test.ts @@ -0,0 +1,450 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { functionWrapper } from './utils'; +import { ExecutionContext } from '../../../execution/types'; +import { Datatable } from '../../../expression_types/specs/datatable'; +import { overallMetric, OverallMetricArgs } from '../overall_metric'; + +describe('interpreter/functions#overall_metric', () => { + const fn = functionWrapper(overallMetric); + const runFn = (input: Datatable, args: OverallMetricArgs) => + fn(input, args, {} as ExecutionContext) as Datatable; + + it('calculates overall sum', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{ val: 5 }, { val: 7 }, { val: 3 }, { val: 2 }], + }, + { inputColumnId: 'val', outputColumnId: 'output', metric: 'sum' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'output', + meta: { type: 'number' }, + }); + expect(result.rows.map((row) => row.output)).toEqual([17, 17, 17, 17]); + }); + + it('ignores null or undefined', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{}, { val: null }, { val: undefined }, { val: 1 }, { val: 5 }], + }, + { inputColumnId: 'val', outputColumnId: 'output', metric: 'average' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'output', + meta: { type: 'number' }, + }); + expect(result.rows.map((row) => row.output)).toEqual([3, 3, 3, 3, 3]); + }); + + it('calculates overall sum for multiple series', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A' }, + { val: 2, split: 'B' }, + { val: 3, split: 'B' }, + { val: 4, split: 'A' }, + { val: 5, split: 'A' }, + { val: 6, split: 'A' }, + { val: 7, split: 'B' }, + { val: 8, split: 'B' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split'], metric: 'sum' } + ); + + expect(result.rows.map((row) => row.output)).toEqual([ + 1 + 4 + 5 + 6, + 2 + 3 + 7 + 8, + 2 + 3 + 7 + 8, + 1 + 4 + 5 + 6, + 1 + 4 + 5 + 6, + 1 + 4 + 5 + 6, + 2 + 3 + 7 + 8, + 2 + 3 + 7 + 8, + ]); + }); + + it('treats missing split column as separate series', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A' }, + { val: 2, split: 'B' }, + { val: 3 }, + { val: 4, split: 'A' }, + { val: 5 }, + { val: 6, split: 'A' }, + { val: 7, split: 'B' }, + { val: 8, split: 'B' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split'], metric: 'sum' } + ); + expect(result.rows.map((row) => row.output)).toEqual([ + 1 + 4 + 6, + 2 + 7 + 8, + 3 + 5, + 1 + 4 + 6, + 3 + 5, + 1 + 4 + 6, + 2 + 7 + 8, + 2 + 7 + 8, + ]); + }); + + it('treats null like undefined and empty string for split columns', () => { + const table: Datatable = { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A' }, + { val: 2, split: 'B' }, + { val: 3 }, + { val: 4, split: 'A' }, + { val: 5 }, + { val: 6, split: 'A' }, + { val: 7, split: null }, + { val: 8, split: 'B' }, + { val: 9, split: '' }, + ], + }; + + const result = runFn(table, { + inputColumnId: 'val', + outputColumnId: 'output', + by: ['split'], + metric: 'sum', + }); + expect(result.rows.map((row) => row.output)).toEqual([ + 1 + 4 + 6, + 2 + 8, + 3 + 5 + 7 + 9, + 1 + 4 + 6, + 3 + 5 + 7 + 9, + 1 + 4 + 6, + 3 + 5 + 7 + 9, + 2 + 8, + 3 + 5 + 7 + 9, + ]); + + const result2 = runFn(table, { + inputColumnId: 'val', + outputColumnId: 'output', + by: ['split'], + metric: 'max', + }); + expect(result2.rows.map((row) => row.output)).toEqual([6, 8, 9, 6, 9, 6, 9, 8, 9]); + }); + + it('handles array values', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{ val: 5 }, { val: [7, 10] }, { val: [3, 1] }, { val: 2 }], + }, + { inputColumnId: 'val', outputColumnId: 'output', metric: 'sum' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'output', + meta: { type: 'number' }, + }); + expect(result.rows.map((row) => row.output)).toEqual([28, 28, 28, 28]); + }); + + it('takes array values into account for average calculation', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{ val: [3, 4] }, { val: 2 }], + }, + { inputColumnId: 'val', outputColumnId: 'output', metric: 'average' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'output', + meta: { type: 'number' }, + }); + expect(result.rows.map((row) => row.output)).toEqual([3, 3]); + }); + + it('handles array values for split columns', () => { + const table: Datatable = { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A' }, + { val: [2, 11], split: 'B' }, + { val: 3 }, + { val: 4, split: 'A' }, + { val: 5 }, + { val: 6, split: 'A' }, + { val: 7, split: null }, + { val: 8, split: 'B' }, + { val: [9, 99], split: '' }, + ], + }; + + const result = runFn(table, { + inputColumnId: 'val', + outputColumnId: 'output', + by: ['split'], + metric: 'sum', + }); + expect(result.rows.map((row) => row.output)).toEqual([ + 1 + 4 + 6, + 2 + 11 + 8, + 3 + 5 + 7 + 9 + 99, + 1 + 4 + 6, + 3 + 5 + 7 + 9 + 99, + 1 + 4 + 6, + 3 + 5 + 7 + 9 + 99, + 2 + 11 + 8, + 3 + 5 + 7 + 9 + 99, + ]); + + const result2 = runFn(table, { + inputColumnId: 'val', + outputColumnId: 'output', + by: ['split'], + metric: 'max', + }); + expect(result2.rows.map((row) => row.output)).toEqual([6, 11, 99, 6, 99, 6, 99, 11, 99]); + }); + + it('calculates cumulative sum for multiple series by multiple split columns', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + { id: 'split2', name: 'split2', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A', split2: 'C' }, + { val: 2, split: 'B', split2: 'C' }, + { val: 3, split2: 'C' }, + { val: 4, split: 'A', split2: 'C' }, + { val: 5 }, + { val: 6, split: 'A', split2: 'D' }, + { val: 7, split: 'B', split2: 'D' }, + { val: 8, split: 'B', split2: 'D' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split', 'split2'], metric: 'sum' } + ); + expect(result.rows.map((row) => row.output)).toEqual([1 + 4, 2, 3, 1 + 4, 5, 6, 7 + 8, 7 + 8]); + }); + + it('splits separate series by the string representation of the cell values', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: { anObj: 3 } }, + { val: 2, split: { anotherObj: 5 } }, + { val: 10, split: 5 }, + { val: 11, split: '5' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split'], metric: 'sum' } + ); + + expect(result.rows.map((row) => row.output)).toEqual([1 + 2, 1 + 2, 10 + 11, 10 + 11]); + }); + + it('casts values to number before calculating cumulative sum', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{ val: 5 }, { val: '7' }, { val: '3' }, { val: 2 }], + }, + { inputColumnId: 'val', outputColumnId: 'output', metric: 'max' } + ); + expect(result.rows.map((row) => row.output)).toEqual([7, 7, 7, 7]); + }); + + it('casts values to number before calculating metric for NaN like values', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{ val: 5 }, { val: '7' }, { val: {} }, { val: 2 }], + }, + { inputColumnId: 'val', outputColumnId: 'output', metric: 'min' } + ); + expect(result.rows.map((row) => row.output)).toEqual([NaN, NaN, NaN, NaN]); + }); + + it('skips undefined and null values', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [ + { val: null }, + { val: 7 }, + { val: undefined }, + { val: undefined }, + { val: undefined }, + { val: undefined }, + { val: '3' }, + { val: 2 }, + { val: null }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', metric: 'average' } + ); + expect(result.rows.map((row) => row.output)).toEqual([4, 4, 4, 4, 4, 4, 4, 4, 4]); + }); + + it('copies over meta information from the source column', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { + id: 'val', + name: 'val', + meta: { + type: 'number', + + field: 'afield', + index: 'anindex', + params: { id: 'number', params: { pattern: '000' } }, + source: 'synthetic', + sourceParams: { + some: 'params', + }, + }, + }, + ], + rows: [{ val: 5 }], + }, + { inputColumnId: 'val', outputColumnId: 'output', metric: 'sum' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'output', + meta: { + type: 'number', + + field: 'afield', + index: 'anindex', + params: { id: 'number', params: { pattern: '000' } }, + source: 'synthetic', + sourceParams: { + some: 'params', + }, + }, + }); + }); + + it('sets output name on output column if specified', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { + id: 'val', + name: 'val', + meta: { + type: 'number', + }, + }, + ], + rows: [{ val: 5 }], + }, + { + inputColumnId: 'val', + outputColumnId: 'output', + outputColumnName: 'Output name', + metric: 'min', + } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'Output name', + meta: { type: 'number' }, + }); + }); + + it('returns source table if input column does not exist', () => { + const input: Datatable = { + type: 'datatable', + columns: [ + { + id: 'val', + name: 'val', + meta: { + type: 'number', + }, + }, + ], + rows: [{ val: 5 }], + }; + expect( + runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output', metric: 'sum' }) + ).toBe(input); + }); + + it('throws an error if output column exists already', () => { + expect(() => + runFn( + { + type: 'datatable', + columns: [ + { + id: 'val', + name: 'val', + meta: { + type: 'number', + }, + }, + ], + rows: [{ val: 5 }], + }, + { inputColumnId: 'val', outputColumnId: 'val', metric: 'max' } + ) + ).toThrow(); + }); +}); diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts index 0a9f022ce89ca..cdcae61215fa4 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts @@ -9,6 +9,8 @@ import { functionWrapper } from './utils'; import { variableSet } from '../var_set'; import { ExecutionContext } from '../../../execution/types'; +import { createUnitTestExecutor } from '../../../test_helpers'; +import { first } from 'rxjs/operators'; describe('expression_functions', () => { describe('var_set', () => { @@ -32,21 +34,49 @@ describe('expression_functions', () => { }); it('updates a variable', () => { - const actual = fn(input, { name: 'test', value: 2 }, context); + const actual = fn(input, { name: ['test'], value: [2] }, context); expect(variables.test).toEqual(2); expect(actual).toEqual(input); }); it('sets a new variable', () => { - const actual = fn(input, { name: 'new', value: 3 }, context); + const actual = fn(input, { name: ['new'], value: [3] }, context); expect(variables.new).toEqual(3); expect(actual).toEqual(input); }); it('stores context if value is not set', () => { - const actual = fn(input, { name: 'test' }, context); + const actual = fn(input, { name: ['test'], value: [] }, context); expect(variables.test).toEqual(input); expect(actual).toEqual(input); }); + + it('sets multiple variables', () => { + const actual = fn(input, { name: ['new1', 'new2', 'new3'], value: [1, , 3] }, context); + expect(variables.new1).toEqual(1); + expect(variables.new2).toEqual(input); + expect(variables.new3).toEqual(3); + expect(actual).toEqual(input); + }); + + describe('running function thru executor', () => { + const executor = createUnitTestExecutor(); + executor.registerFunction(variableSet); + + it('sets the variables', async () => { + const vars = {}; + const result = await executor + .run('var_set name=test1 name=test2 value=1', 2, { variables: vars }) + .pipe(first()) + .toPromise(); + + expect(result).toEqual(2); + + expect(vars).toEqual({ + test1: 1, + test2: 2, + }); + }); + }); }); }); diff --git a/src/plugins/expressions/common/expression_functions/specs/var_set.ts b/src/plugins/expressions/common/expression_functions/specs/var_set.ts index 490c7781a01a1..f3ac6a2ab80d4 100644 --- a/src/plugins/expressions/common/expression_functions/specs/var_set.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var_set.ts @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../types'; interface Arguments { - name: string; - value?: any; + name: string[]; + value: any[]; } export type ExpressionFunctionVarSet = ExpressionFunctionDefinition< @@ -31,12 +31,14 @@ export const variableSet: ExpressionFunctionVarSet = { types: ['string'], aliases: ['_'], required: true, + multi: true, help: i18n.translate('expressions.functions.varset.name.help', { defaultMessage: 'Specify the name of the variable.', }), }, value: { aliases: ['val'], + multi: true, help: i18n.translate('expressions.functions.varset.val.help', { defaultMessage: 'Specify the value for the variable. When unspecified, the input context is used.', @@ -45,7 +47,9 @@ export const variableSet: ExpressionFunctionVarSet = { }, fn(input, args, context) { const variables: Record = context.variables; - variables[args.name] = args.value === undefined ? input : args.value; + args.name.forEach((name, i) => { + variables[name] = args.value[i] === undefined ? input : args.value[i]; + }); return input; }, }; diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index e1378a27bdfc2..0ec61b39608a0 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -18,6 +18,7 @@ import { ExpressionFunctionCumulativeSum, ExpressionFunctionDerivative, ExpressionFunctionMovingAverage, + ExpressionFunctionOverallMetric, } from './specs'; import { ExpressionAstFunction } from '../ast'; import { PersistableStateDefinition } from '../../../kibana_utils/common'; @@ -119,6 +120,7 @@ export interface ExpressionFunctionDefinitions { var: ExpressionFunctionVar; theme: ExpressionFunctionTheme; cumulative_sum: ExpressionFunctionCumulativeSum; + overall_metric: ExpressionFunctionOverallMetric; derivative: ExpressionFunctionDerivative; moving_average: ExpressionFunctionMovingAverage; } diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index a8839c9b0d71e..b3c0167262661 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -29,7 +29,9 @@ import { derivative, movingAverage, mapColumn, + overallMetric, math, + mathColumn, } from '../expression_functions'; /** @@ -340,8 +342,10 @@ export class ExpressionsService implements PersistableStateService [ defaultMessage: '[eCommerce] Top Selling Products', }), visState: - '{"title":"[eCommerce] Top Selling Products","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"products.product_name.keyword","size":7,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[eCommerce] Top Selling Products","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"products.product_name.keyword","size":7,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts index 05a3d012d707c..816322dbe5299 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts @@ -242,7 +242,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Destination Weather', }), visState: - '{"title":"[Flights] Destination Weather","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"DestWeather","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[Flights] Destination Weather","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"DestWeather","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap index 21248ac9d1dc0..38a9e47014416 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -14,46 +14,46 @@ exports[`CreateIndexPatternWizard defaults to the loading state 1`] = ` exports[`CreateIndexPatternWizard renders index pattern step when there are indices 1`] = ` - -
- - + + - + } + showSystemIndices={true} + /> - -
- - + + - + } + showSystemIndices={true} + /> - -
- - + + - + } + /> - -
- - + + - + } + showSystemIndices={true} + /> - -
- - + + - + } + showSystemIndices={true} + /> } > -
- -

+ Create test index pattern - - - - Beta - - -

-
- + + + + + } + > +
-
- - -
+ Create test index pattern + + + + + + } + responsive={true} > -

- - multiple - , - "single": - filebeat-4-3-22 - , - "star": - filebeat-* - , - } - } +

+ - - An index pattern can match a single source, for example, - - - - - filebeat-4-3-22 - - - - - , or - - multiple - - data sources, - - + +
- - - filebeat-* - - - - - . - - -
- +

+ Create test index pattern + + + + Beta + + +

+ +
+
+
+ +
- - -

-
- - -
- -
- Test prompt -
-
+

+ + multiple + , + "single": + filebeat-4-3-22 + , + "star": + filebeat-* + , + } + } + > + + An index pattern can match a single source, for example, + + + + + filebeat-4-3-22 + + + + + , or + + multiple + + data sources, + + + + + filebeat-* + + + + + . + + +
+ + + +

+
+
+ +
+ +
+ Test prompt +
+
+
+ +
+
`; @@ -146,100 +203,145 @@ exports[`Header should render normally 1`] = ` } indexPatternName="test index pattern" > -
- -

+ Create test index pattern -

-
- + } + > +
-
- - -
+ Create test index pattern + + } + responsive={true} > -

- - multiple - , - "single": - filebeat-4-3-22 - , - "star": - filebeat-* - , - } - } +

+ - - An index pattern can match a single source, for example, - - - - - filebeat-4-3-22 - - - - - , or - - multiple - - data sources, - - + +
- - - filebeat-* - - - - - . - - -
- +

+ Create test index pattern +

+ +
+
+
+ +
- - -

-
- -
+

+ + multiple + , + "single": + filebeat-4-3-22 + , + "star": + filebeat-* + , + } + } + > + + An index pattern can match a single source, for example, + + + + + filebeat-4-3-22 + + + + + , or + + multiple + + data sources, + + + + + filebeat-* + + + + + . + + +
+ + + +

+
+ +
+ + +
+
`; @@ -254,99 +356,144 @@ exports[`Header should render without including system indices 1`] = ` } indexPatternName="test index pattern" > -
- -

+ Create test index pattern -

-
- + } + > +
-
- - -
+ Create test index pattern + + } + responsive={true} > -

- - multiple - , - "single": - filebeat-4-3-22 - , - "star": - filebeat-* - , - } - } +

+ - - An index pattern can match a single source, for example, - - - - - filebeat-4-3-22 - - - - - , or - - multiple - - data sources, - - + +
- - - filebeat-* - - - - - . - - -
- +

+ Create test index pattern +

+ +
+
+
+ +
- - -

-
- -
+

+ + multiple + , + "single": + filebeat-4-3-22 + , + "star": + filebeat-* + , + } + } + > + + An index pattern can match a single source, for example, + + + + + filebeat-4-3-22 + + + + + , or + + multiple + + data sources, + + + + + filebeat-* + + + + + . + + +
+ + + +

+
+ +
+ + +
+
`; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx index a7e3b2ded75dc..c708bd3cac33e 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx @@ -8,7 +8,7 @@ import React from 'react'; -import { EuiBetaBadge, EuiSpacer, EuiTitle, EuiText, EuiCode, EuiLink } from '@elastic/eui'; +import { EuiBetaBadge, EuiCode, EuiLink, EuiPageHeader, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -39,9 +39,9 @@ export const Header = ({ changeTitle(createIndexPatternHeader); return ( -
- -

+ {createIndexPatternHeader} {isBeta ? ( <> @@ -53,9 +53,10 @@ export const Header = ({ /> ) : null} -

-
- + + } + bottomBorder + >

) : null} -

+ ); }; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx index 633906feb785b..5bc53105dbcf8 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx @@ -6,17 +6,12 @@ * Side Public License, v 1. */ -import React, { ReactElement, Component } from 'react'; - -import { - EuiGlobalToastList, - EuiGlobalToastListToast, - EuiPageContent, - EuiHorizontalRule, -} from '@elastic/eui'; +import React, { Component, ReactElement } from 'react'; + +import { EuiGlobalToastList, EuiGlobalToastListToast, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; import { DocLinksStart } from 'src/core/public'; import { StepIndexPattern } from './components/step_index_pattern'; import { StepTimeField } from './components/step_time_field'; @@ -227,9 +222,9 @@ export class CreateIndexPatternWizard extends Component< const initialQuery = new URLSearchParams(location.search).get('id') || undefined; return ( - + <> {header} - + - + ); } if (step === 2) { return ( - + <> {header} - + - + ); } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx index 5aa9853c5e766..0c0adc6dd5029 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx @@ -7,15 +7,15 @@ */ import React from 'react'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { IndexPattern, IndexPatternField } from '../../../../../../plugins/data/public'; import { useKibana } from '../../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../../types'; import { IndexHeader } from '../index_header'; -import { TAB_SCRIPTED_FIELDS, TAB_INDEXED_FIELDS } from '../constants'; +import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS } from '../constants'; import { FieldEditor } from '../../field_editor'; @@ -76,26 +76,18 @@ export const CreateEditField = withRouter( if (spec) { return ( - - - - - - - - - - + <> + + + + ); } else { return <>; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index e314c00bc8176..6609605da87d1 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -17,7 +17,6 @@ import { EuiText, EuiLink, EuiCallOut, - EuiPanel, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -145,15 +144,13 @@ export const EditIndexPattern = withRouter( const kibana = useKibana(); const docsUrl = kibana.services.docLinks!.links.elasticsearch.mapping; return ( - -
- - +
+ {showTagsSection && ( {Boolean(indexPattern.timeFieldName) && ( @@ -193,19 +190,19 @@ export const EditIndexPattern = withRouter( )} - - { - setFields(indexPattern.getNonScriptedFields()); - }} - /> -
- +
+ + { + setFields(indexPattern.getNonScriptedFields()); + }} + /> +
); } ); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx index 482cd574c8f1d..c141c228a68f2 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiToolTip, EuiFlexItem, EuiTitle, EuiButtonIcon } from '@elastic/eui'; +import { EuiButtonIcon, EuiPageHeader, EuiToolTip } from '@elastic/eui'; import { IIndexPattern } from 'src/plugins/data/public'; interface IndexHeaderProps { @@ -40,50 +40,42 @@ const removeTooltip = i18n.translate('indexPatternManagement.editIndexPattern.re defaultMessage: 'Remove index pattern.', }); -export function IndexHeader({ +export const IndexHeader: React.FC = ({ defaultIndex, indexPattern, setDefault, deleteIndexPatternClick, -}: IndexHeaderProps) { + children, +}) => { return ( - - - -

{indexPattern.title}

-
-
- - - {defaultIndex !== indexPattern.id && setDefault && ( - - - - - - )} - - {deleteIndexPatternClick && ( - - - - - - )} - - -
+ {indexPattern.title}} + rightSideItems={[ + defaultIndex !== indexPattern.id && setDefault && ( + + + + ), + deleteIndexPatternClick && ( + + + + ), + ].filter(Boolean)} + > + {children} + ); -} +}; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap index c5e6d1220d8bf..bc69fa29e6904 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap @@ -3,9 +3,11 @@ exports[`EmptyIndexPatternPrompt should render normally 1`] = ` diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap index 1310488c65fab..957c94c80680d 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap @@ -4,9 +4,11 @@ exports[`EmptyState should render normally 1`] = ` diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx index 240e732752916..c05f6a1f193b7 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx @@ -63,8 +63,10 @@ export const EmptyState = ({ diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index f018294f27c84..6bd06528084ce 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -8,24 +8,20 @@ import { EuiBadge, + EuiBadgeGroup, EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, EuiInMemoryTable, + EuiPageHeader, EuiSpacer, - EuiText, - EuiBadgeGroup, - EuiPageContent, - EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; -import React, { useState, useEffect } from 'react'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; +import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { reactRouterNavigate, useKibana } from '../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../types'; import { CreateButton } from '../create_button'; -import { IndexPatternTableItem, IndexPatternCreationOption } from '../types'; +import { IndexPatternCreationOption, IndexPatternTableItem } from '../types'; import { getIndexPatterns } from '../utils'; import { getListBreadcrumbs } from '../breadcrumbs'; import { EmptyState } from './empty_state'; @@ -54,10 +50,6 @@ const search = { }, }; -const ariaRegion = i18n.translate('indexPatternManagement.editIndexPatternLiveRegionAriaLabel', { - defaultMessage: 'Index patterns', -}); - const title = i18n.translate('indexPatternManagement.indexPatternTable.title', { defaultMessage: 'Index patterns', }); @@ -197,25 +189,21 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { } return ( - - - - -

{title}

-
- - -

- -

-
-
- {createButton} -
- +
+ + } + bottomBorder + rightSideItems={[createButton]} + /> + + + { sorting={sorting} search={search} /> - +
); }; diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index 76a7cb2855c6e..773c0b96d6413 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -11,7 +11,6 @@ export * from './field_wildcard'; export * from './of'; export * from './ui'; export * from './state_containers'; -export * from './typed_json'; export * from './errors'; export { AbortError, abortSignalToPromise } from './abort_utils'; export { createGetterSetter, Get, Set } from './create_getter_setter'; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 75c52e1301ea5..3d9b5db062955 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -15,9 +15,6 @@ export { fieldWildcardFilter, fieldWildcardMatcher, Get, - JsonArray, - JsonObject, - JsonValue, of, Set, UiComponent, diff --git a/src/plugins/newsfeed/kibana.json b/src/plugins/newsfeed/kibana.json index b9f37b67f6921..0e7ae7cd11c35 100644 --- a/src/plugins/newsfeed/kibana.json +++ b/src/plugins/newsfeed/kibana.json @@ -2,5 +2,6 @@ "id": "newsfeed", "version": "kibana", "server": true, - "ui": true + "ui": true, + "requiredPlugins": ["screenshotMode"] } diff --git a/src/plugins/newsfeed/public/lib/api.test.mocks.ts b/src/plugins/newsfeed/public/lib/api.test.mocks.ts index 677bc203cbef3..8ac66eae6c2f6 100644 --- a/src/plugins/newsfeed/public/lib/api.test.mocks.ts +++ b/src/plugins/newsfeed/public/lib/api.test.mocks.ts @@ -8,6 +8,7 @@ import { storageMock } from './storage.mock'; import { driverMock } from './driver.mock'; +import { NeverFetchNewsfeedApiDriver } from './never_fetch_driver'; export const storageInstanceMock = storageMock.create(); jest.doMock('./storage', () => ({ @@ -18,3 +19,7 @@ export const driverInstanceMock = driverMock.create(); jest.doMock('./driver', () => ({ NewsfeedApiDriver: jest.fn().mockImplementation(() => driverInstanceMock), })); + +jest.doMock('./never_fetch_driver', () => ({ + NeverFetchNewsfeedApiDriver: jest.fn(() => new NeverFetchNewsfeedApiDriver()), +})); diff --git a/src/plugins/newsfeed/public/lib/api.test.ts b/src/plugins/newsfeed/public/lib/api.test.ts index a4894573932e6..58d06e72cd77c 100644 --- a/src/plugins/newsfeed/public/lib/api.test.ts +++ b/src/plugins/newsfeed/public/lib/api.test.ts @@ -7,12 +7,16 @@ */ import { driverInstanceMock, storageInstanceMock } from './api.test.mocks'; + import moment from 'moment'; import { getApi } from './api'; import { TestScheduler } from 'rxjs/testing'; import { FetchResult, NewsfeedPluginBrowserConfig } from '../types'; import { take } from 'rxjs/operators'; +import { NewsfeedApiDriver as MockNewsfeedApiDriver } from './driver'; +import { NeverFetchNewsfeedApiDriver as MockNeverFetchNewsfeedApiDriver } from './never_fetch_driver'; + const kibanaVersion = '8.0.0'; const newsfeedId = 'test'; @@ -46,6 +50,8 @@ describe('getApi', () => { afterEach(() => { storageInstanceMock.isAnyUnread$.mockReset(); driverInstanceMock.fetchNewsfeedItems.mockReset(); + (MockNewsfeedApiDriver as jest.Mock).mockClear(); + (MockNeverFetchNewsfeedApiDriver as jest.Mock).mockClear(); }); it('merges the newsfeed and unread observables', () => { @@ -60,7 +66,7 @@ describe('getApi', () => { a: createFetchResult({ feedItems: ['item' as any] }), }) ); - const api = getApi(createConfig(1000), kibanaVersion, newsfeedId); + const api = getApi(createConfig(1000), kibanaVersion, newsfeedId, false); expectObservable(api.fetchResults$.pipe(take(1))).toBe('(a|)', { a: createFetchResult({ @@ -83,7 +89,7 @@ describe('getApi', () => { a: createFetchResult({ feedItems: ['item' as any] }), }) ); - const api = getApi(createConfig(2), kibanaVersion, newsfeedId); + const api = getApi(createConfig(2), kibanaVersion, newsfeedId, false); expectObservable(api.fetchResults$.pipe(take(2))).toBe('a-(b|)', { a: createFetchResult({ @@ -111,7 +117,7 @@ describe('getApi', () => { a: createFetchResult({}), }) ); - const api = getApi(createConfig(10), kibanaVersion, newsfeedId); + const api = getApi(createConfig(10), kibanaVersion, newsfeedId, false); expectObservable(api.fetchResults$.pipe(take(2))).toBe('a--(b|)', { a: createFetchResult({ @@ -123,4 +129,16 @@ describe('getApi', () => { }); }); }); + + it('uses the news feed API driver if in not screenshot mode', () => { + getApi(createConfig(10), kibanaVersion, newsfeedId, false); + expect(MockNewsfeedApiDriver).toHaveBeenCalled(); + expect(MockNeverFetchNewsfeedApiDriver).not.toHaveBeenCalled(); + }); + + it('uses the never fetch news feed API driver if in not screenshot mode', () => { + getApi(createConfig(10), kibanaVersion, newsfeedId, true); + expect(MockNewsfeedApiDriver).not.toHaveBeenCalled(); + expect(MockNeverFetchNewsfeedApiDriver).toHaveBeenCalled(); + }); }); diff --git a/src/plugins/newsfeed/public/lib/api.ts b/src/plugins/newsfeed/public/lib/api.ts index 4fbbd8687b73f..7aafc9fd27625 100644 --- a/src/plugins/newsfeed/public/lib/api.ts +++ b/src/plugins/newsfeed/public/lib/api.ts @@ -11,6 +11,7 @@ import { map, catchError, filter, mergeMap, tap } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { FetchResult, NewsfeedPluginBrowserConfig } from '../types'; import { NewsfeedApiDriver } from './driver'; +import { NeverFetchNewsfeedApiDriver } from './never_fetch_driver'; import { NewsfeedStorage } from './storage'; export enum NewsfeedApiEndpoint { @@ -40,13 +41,23 @@ export interface NewsfeedApi { export function getApi( config: NewsfeedPluginBrowserConfig, kibanaVersion: string, - newsfeedId: string + newsfeedId: string, + isScreenshotMode: boolean ): NewsfeedApi { - const userLanguage = i18n.getLocale(); - const fetchInterval = config.fetchInterval.asMilliseconds(); - const mainInterval = config.mainInterval.asMilliseconds(); const storage = new NewsfeedStorage(newsfeedId); - const driver = new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval, storage); + const mainInterval = config.mainInterval.asMilliseconds(); + + const createNewsfeedApiDriver = () => { + if (isScreenshotMode) { + return new NeverFetchNewsfeedApiDriver(); + } + + const userLanguage = i18n.getLocale(); + const fetchInterval = config.fetchInterval.asMilliseconds(); + return new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval, storage); + }; + + const driver = createNewsfeedApiDriver(); const results$ = timer(0, mainInterval).pipe( filter(() => driver.shouldFetch()), diff --git a/src/plugins/newsfeed/public/lib/driver.ts b/src/plugins/newsfeed/public/lib/driver.ts index 0efa981e8c89d..1762c4a428784 100644 --- a/src/plugins/newsfeed/public/lib/driver.ts +++ b/src/plugins/newsfeed/public/lib/driver.ts @@ -10,6 +10,7 @@ import moment from 'moment'; import * as Rx from 'rxjs'; import { NEWSFEED_DEFAULT_SERVICE_BASE_URL } from '../../common/constants'; import { ApiItem, FetchResult, NewsfeedPluginBrowserConfig } from '../types'; +import { INewsfeedApiDriver } from './types'; import { convertItems } from './convert_items'; import type { NewsfeedStorage } from './storage'; @@ -19,7 +20,7 @@ interface NewsfeedResponse { items: ApiItem[]; } -export class NewsfeedApiDriver { +export class NewsfeedApiDriver implements INewsfeedApiDriver { private readonly kibanaVersion: string; private readonly loadedTime = moment().utc(); // the date is compared to time in UTC format coming from the service diff --git a/src/plugins/newsfeed/public/lib/never_fetch_driver.ts b/src/plugins/newsfeed/public/lib/never_fetch_driver.ts new file mode 100644 index 0000000000000..e95ca9c2d499a --- /dev/null +++ b/src/plugins/newsfeed/public/lib/never_fetch_driver.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Observable } from 'rxjs'; +import { FetchResult } from '../types'; +import { INewsfeedApiDriver } from './types'; + +/** + * NewsfeedApiDriver variant that never fetches results. This is useful for instances where Kibana is started + * without any user interaction like when generating a PDF or PNG report. + */ +export class NeverFetchNewsfeedApiDriver implements INewsfeedApiDriver { + shouldFetch(): boolean { + return false; + } + + fetchNewsfeedItems(): Observable { + throw new Error('Not implemented!'); + } +} diff --git a/src/plugins/newsfeed/public/lib/types.ts b/src/plugins/newsfeed/public/lib/types.ts new file mode 100644 index 0000000000000..5a62a929eeb7f --- /dev/null +++ b/src/plugins/newsfeed/public/lib/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Observable } from 'rxjs'; +import type { FetchResult, NewsfeedPluginBrowserConfig } from '../types'; + +export interface INewsfeedApiDriver { + /** + * Check whether newsfeed items should be (re-)fetched + */ + shouldFetch(): boolean; + + fetchNewsfeedItems(config: NewsfeedPluginBrowserConfig['service']): Observable; +} diff --git a/src/plugins/newsfeed/public/plugin.test.ts b/src/plugins/newsfeed/public/plugin.test.ts new file mode 100644 index 0000000000000..4be69feb79f55 --- /dev/null +++ b/src/plugins/newsfeed/public/plugin.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { take } from 'rxjs/operators'; +import { coreMock } from '../../../core/public/mocks'; +import { NewsfeedPublicPlugin } from './plugin'; +import { NewsfeedApiEndpoint } from './lib/api'; + +describe('Newsfeed plugin', () => { + let plugin: NewsfeedPublicPlugin; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + plugin = new NewsfeedPublicPlugin(coreMock.createPluginInitializerContext()); + }); + + describe('#start', () => { + beforeEach(() => { + plugin.setup(coreMock.createSetup()); + }); + + beforeEach(() => { + /** + * We assume for these tests that the newsfeed stream exposed by start will fetch newsfeed items + * on the first tick for new subscribers + */ + jest.spyOn(window, 'fetch'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('base case', () => { + it('makes fetch requests', () => { + const startContract = plugin.start(coreMock.createStart(), { + screenshotMode: { isScreenshotMode: () => false }, + }); + const sub = startContract + .createNewsFeed$(NewsfeedApiEndpoint.KIBANA) // Any endpoint will do + .pipe(take(1)) + .subscribe(() => {}); + jest.runOnlyPendingTimers(); + expect(window.fetch).toHaveBeenCalled(); + sub.unsubscribe(); + }); + }); + + describe('when in screenshot mode', () => { + it('makes no fetch requests in screenshot mode', () => { + const startContract = plugin.start(coreMock.createStart(), { + screenshotMode: { isScreenshotMode: () => true }, + }); + const sub = startContract + .createNewsFeed$(NewsfeedApiEndpoint.KIBANA) // Any endpoint will do + .pipe(take(1)) + .subscribe(() => {}); + jest.runOnlyPendingTimers(); + expect(window.fetch).not.toHaveBeenCalled(); + sub.unsubscribe(); + }); + }); + }); +}); diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx index fdda0a24b8bd5..656fc2ef00bb9 100644 --- a/src/plugins/newsfeed/public/plugin.tsx +++ b/src/plugins/newsfeed/public/plugin.tsx @@ -13,7 +13,7 @@ import React from 'react'; import moment from 'moment'; import { I18nProvider } from '@kbn/i18n/react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { NewsfeedPluginBrowserConfig } from './types'; +import { NewsfeedPluginBrowserConfig, NewsfeedPluginStartDependencies } from './types'; import { NewsfeedNavButton } from './components/newsfeed_header_nav_button'; import { getApi, NewsfeedApi, NewsfeedApiEndpoint } from './lib/api'; @@ -41,8 +41,10 @@ export class NewsfeedPublicPlugin return {}; } - public start(core: CoreStart) { - const api = this.createNewsfeedApi(this.config, NewsfeedApiEndpoint.KIBANA); + public start(core: CoreStart, { screenshotMode }: NewsfeedPluginStartDependencies) { + const isScreenshotMode = screenshotMode.isScreenshotMode(); + + const api = this.createNewsfeedApi(this.config, NewsfeedApiEndpoint.KIBANA, isScreenshotMode); core.chrome.navControls.registerRight({ order: 1000, mount: (target) => this.mount(api, target), @@ -56,7 +58,7 @@ export class NewsfeedPublicPlugin pathTemplate: `/${endpoint}/v{VERSION}.json`, }, }); - const { fetchResults$ } = this.createNewsfeedApi(config, endpoint); + const { fetchResults$ } = this.createNewsfeedApi(config, endpoint, isScreenshotMode); return fetchResults$; }, }; @@ -68,9 +70,10 @@ export class NewsfeedPublicPlugin private createNewsfeedApi( config: NewsfeedPluginBrowserConfig, - newsfeedId: NewsfeedApiEndpoint + newsfeedId: NewsfeedApiEndpoint, + isScreenshotMode: boolean ): NewsfeedApi { - const api = getApi(config, this.kibanaVersion, newsfeedId); + const api = getApi(config, this.kibanaVersion, newsfeedId, isScreenshotMode); return { markAsRead: api.markAsRead, fetchResults$: api.fetchResults$.pipe( diff --git a/src/plugins/newsfeed/public/types.ts b/src/plugins/newsfeed/public/types.ts index cca656565f4ca..a7ff917f6f975 100644 --- a/src/plugins/newsfeed/public/types.ts +++ b/src/plugins/newsfeed/public/types.ts @@ -7,6 +7,10 @@ */ import { Duration, Moment } from 'moment'; +import type { ScreenshotModePluginStart } from 'src/plugins/screenshot_mode/public'; +export interface NewsfeedPluginStartDependencies { + screenshotMode: ScreenshotModePluginStart; +} // Ideally, we may want to obtain the type from the configSchema and exposeToBrowser keys... export interface NewsfeedPluginBrowserConfig { diff --git a/src/plugins/newsfeed/tsconfig.json b/src/plugins/newsfeed/tsconfig.json index 66244a22336c7..18e6f2de1bc6f 100644 --- a/src/plugins/newsfeed/tsconfig.json +++ b/src/plugins/newsfeed/tsconfig.json @@ -7,13 +7,9 @@ "declaration": true, "declarationMap": true }, - "include": [ - "public/**/*", - "server/**/*", - "common/*", - "../../../typings/**/*" - ], + "include": ["public/**/*", "server/**/*", "common/*", "../../../typings/**/*"], "references": [ - { "path": "../../core/tsconfig.json" } + { "path": "../../core/tsconfig.json" }, + { "path": "../screenshot_mode/tsconfig.json" } ] } diff --git a/src/plugins/presentation_util/public/mocks.ts b/src/plugins/presentation_util/public/mocks.ts new file mode 100644 index 0000000000000..91c461646c280 --- /dev/null +++ b/src/plugins/presentation_util/public/mocks.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreStart } from 'kibana/public'; +import { PresentationUtilPluginStart } from './types'; +import { pluginServices } from './services'; +import { registry } from './services/kibana'; + +const createStartContract = (coreStart: CoreStart): PresentationUtilPluginStart => { + pluginServices.setRegistry(registry.start({ coreStart, startPlugins: {} as any })); + + const startContract: PresentationUtilPluginStart = { + ContextProvider: pluginServices.getContextProvider(), + labsService: pluginServices.getServices().labs, + }; + return startContract; +}; + +export const presentationUtilPluginMock = { + createStartContract, +}; diff --git a/src/plugins/saved_objects/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects/public/finder/saved_object_finder.tsx index 8d5e89664212c..da65b5b9fdda8 100644 --- a/src/plugins/saved_objects/public/finder/saved_object_finder.tsx +++ b/src/plugins/saved_objects/public/finder/saved_object_finder.tsx @@ -46,6 +46,7 @@ export interface SavedObjectMetaData { getIconForSavedObject(savedObject: SimpleSavedObject): IconType; getTooltipForSavedObject?(savedObject: SimpleSavedObject): string; showSavedObject?(savedObject: SimpleSavedObject): boolean; + getSavedObjectSubType?(savedObject: SimpleSavedObject): string; includeFields?: string[]; } diff --git a/src/plugins/screenshot_mode/public/index.ts b/src/plugins/screenshot_mode/public/index.ts index a5ad37dd5b760..012f57e837f41 100644 --- a/src/plugins/screenshot_mode/public/index.ts +++ b/src/plugins/screenshot_mode/public/index.ts @@ -18,4 +18,4 @@ export { KBN_SCREENSHOT_MODE_ENABLED_KEY, } from '../common'; -export { ScreenshotModePluginSetup } from './types'; +export { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types'; diff --git a/src/plugins/screenshot_mode/public/plugin.test.ts b/src/plugins/screenshot_mode/public/plugin.test.ts index 33ae501466876..f2c0970d0ff60 100644 --- a/src/plugins/screenshot_mode/public/plugin.test.ts +++ b/src/plugins/screenshot_mode/public/plugin.test.ts @@ -21,7 +21,7 @@ describe('Screenshot mode public', () => { setScreenshotModeDisabled(); }); - describe('setup contract', () => { + describe('public contract', () => { it('detects screenshot mode "true"', () => { setScreenshotModeEnabled(); const screenshotMode = plugin.setup(coreMock.createSetup()); @@ -34,10 +34,4 @@ describe('Screenshot mode public', () => { expect(screenshotMode.isScreenshotMode()).toBe(false); }); }); - - describe('start contract', () => { - it('returns nothing', () => { - expect(plugin.start(coreMock.createStart())).toBe(undefined); - }); - }); }); diff --git a/src/plugins/screenshot_mode/public/plugin.ts b/src/plugins/screenshot_mode/public/plugin.ts index 7a166566a0173..a005bb7c3d055 100644 --- a/src/plugins/screenshot_mode/public/plugin.ts +++ b/src/plugins/screenshot_mode/public/plugin.ts @@ -8,18 +8,22 @@ import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; -import { ScreenshotModePluginSetup } from './types'; +import { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types'; import { getScreenshotMode } from '../common'; export class ScreenshotModePlugin implements Plugin { + private publicContract = Object.freeze({ + isScreenshotMode: () => getScreenshotMode() === true, + }); + public setup(core: CoreSetup): ScreenshotModePluginSetup { - return { - isScreenshotMode: () => getScreenshotMode() === true, - }; + return this.publicContract; } - public start(core: CoreStart) {} + public start(core: CoreStart): ScreenshotModePluginStart { + return this.publicContract; + } public stop() {} } diff --git a/src/plugins/screenshot_mode/public/types.ts b/src/plugins/screenshot_mode/public/types.ts index 744ea8615f2a7..f6963de0cbd63 100644 --- a/src/plugins/screenshot_mode/public/types.ts +++ b/src/plugins/screenshot_mode/public/types.ts @@ -15,3 +15,4 @@ export interface IScreenshotModeService { } export type ScreenshotModePluginSetup = IScreenshotModeService; +export type ScreenshotModePluginStart = IScreenshotModeService; diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap index 17a91a4d43cc7..cbfece0b081c6 100644 --- a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap +++ b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap @@ -5,6 +5,7 @@ Object { "as": "tagloud_vis", "type": "render", "value": Object { + "syncColors": false, "visData": Object { "columns": Array [ Object { @@ -20,6 +21,12 @@ Object { "type": "datatable", }, "visParams": Object { + "bucket": Object { + "accessor": 1, + "format": Object { + "id": "number", + }, + }, "maxFontSize": 72, "metric": Object { "accessor": 0, @@ -29,6 +36,10 @@ Object { }, "minFontSize": 18, "orientation": "single", + "palette": Object { + "name": "default", + "type": "palette", + }, "scale": "linear", "showLabel": true, }, diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap index a8bc0b4c51678..fed6fb54288f2 100644 --- a/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap @@ -84,6 +84,9 @@ Object { "orientation": Array [ "single", ], + "palette": Array [ + "default", + ], "scale": Array [ "linear", ], diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap deleted file mode 100644 index 88ed7c66a79a2..0000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`tag cloud tests tagcloudscreenshot should render simple image 1`] = `"foobarfoobar"`; diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap deleted file mode 100644 index d7707f64d8a4f..0000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TagCloudVisualizationTest TagCloudVisualization - basics simple draw 1`] = `"CNINUSDEBR"`; - -exports[`TagCloudVisualizationTest TagCloudVisualization - basics with param change 1`] = `"CNINUSDEBR"`; - -exports[`TagCloudVisualizationTest TagCloudVisualization - basics with resize 1`] = `"CNINUSDEBR"`; diff --git a/src/plugins/vis_type_tagcloud/public/components/feedback_message.js b/src/plugins/vis_type_tagcloud/public/components/feedback_message.js deleted file mode 100644 index 9e1d66b0a2faa..0000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/feedback_message.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { Component, Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiIconTip } from '@elastic/eui'; - -export class FeedbackMessage extends Component { - constructor() { - super(); - this.state = { shouldShowTruncate: false, shouldShowIncomplete: false }; - } - - render() { - if (!this.state.shouldShowTruncate && !this.state.shouldShowIncomplete) { - return ''; - } - - return ( - - {this.state.shouldShowTruncate && ( -

- -

- )} - {this.state.shouldShowIncomplete && ( -

- -

- )} -
- } - /> - ); - } -} diff --git a/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx new file mode 100644 index 0000000000000..82663bbf7070c --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { lazy } from 'react'; +import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; +import { TagCloudVisParams, TagCloudTypeProps } from '../types'; + +const TagCloudOptionsLazy = lazy(() => import('./tag_cloud_options')); + +export const getTagCloudOptions = ({ palettes }: TagCloudTypeProps) => ( + props: VisEditorOptionsProps +) => ; diff --git a/src/plugins/vis_type_tagcloud/public/components/label.js b/src/plugins/vis_type_tagcloud/public/components/label.js deleted file mode 100644 index 028a001cfbe63..0000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/label.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { Component } from 'react'; - -export class Label extends Component { - constructor() { - super(); - this.state = { label: '', shouldShowLabel: true }; - } - - render() { - return ( -
- {this.state.label} -
- ); - } -} diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js deleted file mode 100644 index 254d210eebf37..0000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import d3 from 'd3'; -import d3TagCloud from 'd3-cloud'; -import { EventEmitter } from 'events'; - -const ORIENTATIONS = { - single: () => 0, - 'right angled': (tag) => { - return hashWithinRange(tag.text, 2) * 90; - }, - multiple: (tag) => { - return hashWithinRange(tag.text, 12) * 15 - 90; //fan out 12 * 15 degrees over top-right and bottom-right quadrant (=-90 deg offset) - }, -}; -const D3_SCALING_FUNCTIONS = { - linear: () => d3.scale.linear(), - log: () => d3.scale.log(), - 'square root': () => d3.scale.sqrt(), -}; - -export class TagCloud extends EventEmitter { - constructor(domNode, colorScale) { - super(); - - //DOM - this._element = domNode; - this._d3SvgContainer = d3.select(this._element).append('svg'); - this._svgGroup = this._d3SvgContainer.append('g'); - this._size = [1, 1]; - this.resize(); - - //SETTING (non-configurable) - /** - * the fontFamily should be set explicitly for calculating a layout - * and to avoid words overlapping - */ - this._fontFamily = 'Inter UI, sans-serif'; - this._fontStyle = 'normal'; - this._fontWeight = 'normal'; - this._spiral = 'archimedean'; //layout shape - this._timeInterval = 1000; //time allowed for layout algorithm - this._padding = 5; - - //OPTIONS - this._orientation = 'single'; - this._minFontSize = 10; - this._maxFontSize = 36; - this._textScale = 'linear'; - this._optionsAsString = null; - - //DATA - this._words = null; - - //UTIL - this._colorScale = colorScale; - this._setTimeoutId = null; - this._pendingJob = null; - this._layoutIsUpdating = null; - this._allInViewBox = false; - this._DOMisUpdating = false; - } - - setOptions(options) { - if (JSON.stringify(options) === this._optionsAsString) { - return; - } - this._optionsAsString = JSON.stringify(options); - this._orientation = options.orientation; - this._minFontSize = Math.min(options.minFontSize, options.maxFontSize); - this._maxFontSize = Math.max(options.minFontSize, options.maxFontSize); - this._textScale = options.scale; - this._invalidate(false); - } - - resize() { - const newWidth = this._element.offsetWidth; - const newHeight = this._element.offsetHeight; - - if (newWidth === this._size[0] && newHeight === this._size[1]) { - return; - } - - const wasInside = this._size[0] >= this._cloudWidth && this._size[1] >= this._cloudHeight; - const willBeInside = this._cloudWidth <= newWidth && this._cloudHeight <= newHeight; - this._size[0] = newWidth; - this._size[1] = newHeight; - if (wasInside && willBeInside && this._allInViewBox) { - this._invalidate(true); - } else { - this._invalidate(false); - } - } - - setData(data) { - this._words = data; - this._invalidate(false); - } - - destroy() { - clearTimeout(this._setTimeoutId); - this._element.innerHTML = ''; - } - - getStatus() { - return this._allInViewBox ? TagCloud.STATUS.COMPLETE : TagCloud.STATUS.INCOMPLETE; - } - - _updateContainerSize() { - this._d3SvgContainer.attr('width', this._size[0]); - this._d3SvgContainer.attr('height', this._size[1]); - this._svgGroup.attr('width', this._size[0]); - this._svgGroup.attr('height', this._size[1]); - } - - _isJobRunning() { - return this._setTimeoutId || this._layoutIsUpdating || this._DOMisUpdating; - } - - async _processPendingJob() { - if (!this._pendingJob) { - return; - } - - if (this._isJobRunning()) { - return; - } - - this._completedJob = null; - const job = await this._pickPendingJob(); - if (job.words.length) { - if (job.refreshLayout) { - await this._updateLayout(job); - } - await this._updateDOM(job); - const cloudBBox = this._svgGroup[0][0].getBBox(); - this._cloudWidth = cloudBBox.width; - this._cloudHeight = cloudBBox.height; - this._allInViewBox = - cloudBBox.x >= 0 && - cloudBBox.y >= 0 && - cloudBBox.x + cloudBBox.width <= this._element.offsetWidth && - cloudBBox.y + cloudBBox.height <= this._element.offsetHeight; - } else { - this._emptyDOM(job); - } - - if (this._pendingJob) { - this._processPendingJob(); //pick up next job - } else { - this._completedJob = job; - this.emit('renderComplete'); - } - } - - async _pickPendingJob() { - return await new Promise((resolve) => { - this._setTimeoutId = setTimeout(async () => { - const job = this._pendingJob; - this._pendingJob = null; - this._setTimeoutId = null; - resolve(job); - }, 0); - }); - } - - _emptyDOM() { - this._svgGroup.selectAll('text').remove(); - this._cloudWidth = 0; - this._cloudHeight = 0; - this._allInViewBox = true; - this._DOMisUpdating = false; - } - - async _updateDOM(job) { - const canSkipDomUpdate = this._pendingJob || this._setTimeoutId; - if (canSkipDomUpdate) { - this._DOMisUpdating = false; - return; - } - - this._DOMisUpdating = true; - const affineTransform = positionWord.bind( - null, - this._element.offsetWidth / 2, - this._element.offsetHeight / 2 - ); - const svgTextNodes = this._svgGroup.selectAll('text'); - const stage = svgTextNodes.data(job.words, getText); - - await new Promise((resolve) => { - const enterSelection = stage.enter(); - const enteringTags = enterSelection.append('text'); - enteringTags.style('font-size', getSizeInPixels); - enteringTags.style('font-style', this._fontStyle); - enteringTags.style('font-weight', () => this._fontWeight); - enteringTags.style('font-family', () => this._fontFamily); - enteringTags.style('fill', this.getFill.bind(this)); - enteringTags.attr('text-anchor', () => 'middle'); - enteringTags.attr('transform', affineTransform); - enteringTags.attr('data-test-subj', getDisplayText); - enteringTags.text(getDisplayText); - - const self = this; - enteringTags.on({ - click: function (event) { - self.emit('select', event); - }, - mouseover: function () { - d3.select(this).style('cursor', 'pointer'); - }, - mouseout: function () { - d3.select(this).style('cursor', 'default'); - }, - }); - - const movingTags = stage.transition(); - movingTags.duration(600); - movingTags.style('font-size', getSizeInPixels); - movingTags.style('font-style', this._fontStyle); - movingTags.style('font-weight', () => this._fontWeight); - movingTags.style('font-family', () => this._fontFamily); - movingTags.attr('transform', affineTransform); - - const exitingTags = stage.exit(); - const exitTransition = exitingTags.transition(); - exitTransition.duration(200); - exitingTags.style('fill-opacity', 1e-6); - exitingTags.attr('font-size', 1); - exitingTags.remove(); - - let exits = 0; - let moves = 0; - const resolveWhenDone = () => { - if (exits === 0 && moves === 0) { - this._DOMisUpdating = false; - resolve(true); - } - }; - exitTransition.each(() => exits++); - exitTransition.each('end', () => { - exits--; - resolveWhenDone(); - }); - movingTags.each(() => moves++); - movingTags.each('end', () => { - moves--; - resolveWhenDone(); - }); - }); - } - - _makeTextSizeMapper() { - const mapSizeToFontSize = D3_SCALING_FUNCTIONS[this._textScale](); - const range = - this._words.length === 1 - ? [this._maxFontSize, this._maxFontSize] - : [this._minFontSize, this._maxFontSize]; - mapSizeToFontSize.range(range); - if (this._words) { - mapSizeToFontSize.domain(d3.extent(this._words, getValue)); - } - return mapSizeToFontSize; - } - - _makeNewJob() { - return { - refreshLayout: true, - size: this._size.slice(), - words: this._words, - }; - } - - _makeJobPreservingLayout() { - return { - refreshLayout: false, - size: this._size.slice(), - words: this._completedJob.words.map((tag) => { - return { - x: tag.x, - y: tag.y, - rotate: tag.rotate, - size: tag.size, - rawText: tag.rawText || tag.text, - displayText: tag.displayText, - meta: tag.meta, - }; - }), - }; - } - - _invalidate(keepLayout) { - if (!this._words) { - return; - } - - this._updateContainerSize(); - - const canReuseLayout = keepLayout && !this._isJobRunning() && this._completedJob; - this._pendingJob = canReuseLayout ? this._makeJobPreservingLayout() : this._makeNewJob(); - this._processPendingJob(); - } - - async _updateLayout(job) { - if (job.size[0] <= 0 || job.size[1] <= 0) { - // If either width or height isn't above 0 we don't relayout anything, - // since the d3-cloud will be stuck in an infinite loop otherwise. - return; - } - - const mapSizeToFontSize = this._makeTextSizeMapper(); - const tagCloudLayoutGenerator = d3TagCloud(); - tagCloudLayoutGenerator.size(job.size); - tagCloudLayoutGenerator.padding(this._padding); - tagCloudLayoutGenerator.rotate(ORIENTATIONS[this._orientation]); - tagCloudLayoutGenerator.font(this._fontFamily); - tagCloudLayoutGenerator.fontStyle(this._fontStyle); - tagCloudLayoutGenerator.fontWeight(this._fontWeight); - tagCloudLayoutGenerator.fontSize((tag) => mapSizeToFontSize(tag.value)); - tagCloudLayoutGenerator.random(seed); - tagCloudLayoutGenerator.spiral(this._spiral); - tagCloudLayoutGenerator.words(job.words); - tagCloudLayoutGenerator.text(getDisplayText); - tagCloudLayoutGenerator.timeInterval(this._timeInterval); - - this._layoutIsUpdating = true; - await new Promise((resolve) => { - tagCloudLayoutGenerator.on('end', () => { - this._layoutIsUpdating = false; - resolve(true); - }); - tagCloudLayoutGenerator.start(); - }); - } - - /** - * Returns debug info. For debugging only. - * @return {*} - */ - getDebugInfo() { - const debug = {}; - debug.positions = this._completedJob - ? this._completedJob.words.map((tag) => { - return { - displayText: tag.displayText, - rawText: tag.rawText || tag.text, - x: tag.x, - y: tag.y, - rotate: tag.rotate, - }; - }) - : []; - debug.size = { - width: this._size[0], - height: this._size[1], - }; - return debug; - } - - getFill(tag) { - return this._colorScale(tag.text); - } -} - -TagCloud.STATUS = { COMPLETE: 0, INCOMPLETE: 1 }; - -function seed() { - return 0.5; //constant seed (not random) to ensure constant layouts for identical data -} - -function getText(word) { - return word.rawText; -} - -function getDisplayText(word) { - return word.displayText; -} - -function positionWord(xTranslate, yTranslate, word) { - if (isNaN(word.x) || isNaN(word.y) || isNaN(word.rotate)) { - //move off-screen - return `translate(${xTranslate * 3}, ${yTranslate * 3})rotate(0)`; - } - - return `translate(${word.x + xTranslate}, ${word.y + yTranslate})rotate(${word.rotate})`; -} - -function getValue(tag) { - return tag.value; -} - -function getSizeInPixels(tag) { - return `${tag.size}px`; -} - -function hashWithinRange(str, max) { - str = JSON.stringify(str); - let hash = 0; - for (const ch of str) { - hash = (hash * 31 + ch.charCodeAt(0)) % max; - } - return Math.abs(hash) % max; -} diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss index 37867f1ed1c17..51b5e9dedd844 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss @@ -5,18 +5,14 @@ // tgcChart__legend--small // tgcChart__legend-isLoading -.tgcChart__container, .tgcChart__wrapper { +.tgcChart__wrapper { flex: 1 1 0; display: flex; + flex-direction: column; } -.tgcChart { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - overflow: hidden; +.tgcChart__wrapper text { + cursor: pointer; } .tgcChart__label { @@ -24,3 +20,7 @@ text-align: center; font-weight: $euiFontWeightBold; } + +.tgcChart__warning { + width: $euiSize; +} diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js deleted file mode 100644 index eb575457146c5..0000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js +++ /dev/null @@ -1,507 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import d3 from 'd3'; -import 'jest-canvas-mock'; - -import { fromNode, delay } from 'bluebird'; -import { TagCloud } from './tag_cloud'; -import { setHTMLElementOffset, setSVGElementGetBBox } from '@kbn/test/jest'; - -describe('tag cloud tests', () => { - let SVGElementGetBBoxSpyInstance; - let HTMLElementOffsetMockInstance; - - beforeEach(() => { - setupDOM(); - }); - - afterEach(() => { - SVGElementGetBBoxSpyInstance.mockRestore(); - HTMLElementOffsetMockInstance.mockRestore(); - }); - - const minValue = 1; - const maxValue = 9; - const midValue = (minValue + maxValue) / 2; - const baseTest = { - data: [ - { rawText: 'foo', displayText: 'foo', value: minValue }, - { rawText: 'bar', displayText: 'bar', value: midValue }, - { rawText: 'foobar', displayText: 'foobar', value: maxValue }, - ], - options: { - orientation: 'single', - scale: 'linear', - minFontSize: 10, - maxFontSize: 36, - }, - expected: [ - { - text: 'foo', - fontSize: '10px', - }, - { - text: 'bar', - fontSize: '23px', - }, - { - text: 'foobar', - fontSize: '36px', - }, - ], - }; - - const singleLayoutTest = _.cloneDeep(baseTest); - - const rightAngleLayoutTest = _.cloneDeep(baseTest); - rightAngleLayoutTest.options.orientation = 'right angled'; - - const multiLayoutTest = _.cloneDeep(baseTest); - multiLayoutTest.options.orientation = 'multiple'; - - const mapWithLog = d3.scale.log(); - mapWithLog.range([baseTest.options.minFontSize, baseTest.options.maxFontSize]); - mapWithLog.domain([minValue, maxValue]); - const logScaleTest = _.cloneDeep(baseTest); - logScaleTest.options.scale = 'log'; - logScaleTest.expected[1].fontSize = Math.round(mapWithLog(midValue)) + 'px'; - - const mapWithSqrt = d3.scale.sqrt(); - mapWithSqrt.range([baseTest.options.minFontSize, baseTest.options.maxFontSize]); - mapWithSqrt.domain([minValue, maxValue]); - const sqrtScaleTest = _.cloneDeep(baseTest); - sqrtScaleTest.options.scale = 'square root'; - sqrtScaleTest.expected[1].fontSize = Math.round(mapWithSqrt(midValue)) + 'px'; - - const biggerFontTest = _.cloneDeep(baseTest); - biggerFontTest.options.minFontSize = 36; - biggerFontTest.options.maxFontSize = 72; - biggerFontTest.expected[0].fontSize = '36px'; - biggerFontTest.expected[1].fontSize = '54px'; - biggerFontTest.expected[2].fontSize = '72px'; - - const trimDataTest = _.cloneDeep(baseTest); - trimDataTest.data.splice(1, 1); - trimDataTest.expected.splice(1, 1); - - let domNode; - let tagCloud; - - const colorScale = d3.scale - .ordinal() - .range(['#00a69b', '#57c17b', '#6f87d8', '#663db8', '#bc52bc', '#9e3533', '#daa05d']); - - function setupDOM() { - domNode = document.createElement('div'); - SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(); - HTMLElementOffsetMockInstance = setHTMLElementOffset(512, 512); - - document.body.appendChild(domNode); - } - - function teardownDOM() { - domNode.innerHTML = ''; - document.body.removeChild(domNode); - } - - [ - singleLayoutTest, - rightAngleLayoutTest, - multiLayoutTest, - logScaleTest, - sqrtScaleTest, - biggerFontTest, - trimDataTest, - ].forEach(function (currentTest) { - describe(`should position elements correctly for options: ${JSON.stringify( - currentTest.options - )}`, () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(currentTest.data); - tagCloud.setOptions(currentTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(currentTest.expected, textElements, tagCloud); - }) - ); - }); - }); - - [5, 100, 200, 300, 500].forEach((timeout) => { - // FLAKY: https://github.com/elastic/kibana/issues/94043 - describe.skip(`should only send single renderComplete event at the very end, using ${timeout}ms timeout`, () => { - beforeEach(async () => { - //TagCloud takes at least 600ms to complete (due to d3 animation) - //renderComplete should only notify at the last one - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - - //this timeout modifies the settings before the cloud is rendered. - //the cloud needs to use the correct options - setTimeout(() => tagCloud.setOptions(logScaleTest.options), timeout); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(logScaleTest.expected, textElements, tagCloud); - }) - ); - }); - }); - - describe('should use the latest state before notifying (when modifying options multiple times)', () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - tagCloud.setOptions(logScaleTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(logScaleTest.expected, textElements, tagCloud); - }) - ); - }); - - describe('should use the latest state before notifying (when modifying data multiple times)', () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - tagCloud.setData(trimDataTest.data); - - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(trimDataTest.expected, textElements, tagCloud); - }) - ); - }); - - describe('should not get multiple render-events', () => { - let counter; - beforeEach(() => { - counter = 0; - - return new Promise((resolve, reject) => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - - setTimeout(() => { - //this should be overridden by later changes - tagCloud.setData(sqrtScaleTest.data); - tagCloud.setOptions(sqrtScaleTest.options); - }, 100); - - setTimeout(() => { - //latest change - tagCloud.setData(logScaleTest.data); - tagCloud.setOptions(logScaleTest.options); - }, 300); - - tagCloud.on('renderComplete', function onRender() { - if (counter > 0) { - reject('Should not get multiple render events'); - } - counter += 1; - resolve(true); - }); - }); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(logScaleTest.expected, textElements, tagCloud); - }) - ); - }); - - describe('should show correct data when state-updates are interleaved with resize event', () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(logScaleTest.data); - tagCloud.setOptions(logScaleTest.options); - - await delay(1000); //let layout run - - SVGElementGetBBoxSpyInstance.mockRestore(); - SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(600, 600); - - tagCloud.resize(); //triggers new layout - setTimeout(() => { - //change the options at the very end too - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - }, 200); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(baseTest.expected, textElements, tagCloud); - }) - ); - }); - - describe(`should not put elements in view when container is too small`, () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test('completeness should not be ok', () => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE); - }); - test('positions should not be ok', () => { - const textElements = domNode.querySelectorAll('text'); - for (let i = 0; i < textElements; i++) { - const bbox = textElements[i].getBoundingClientRect(); - verifyBbox(bbox, false, tagCloud); - } - }); - }); - - describe(`tags should fit after making container bigger`, () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - - //make bigger - tagCloud._size = [600, 600]; - tagCloud.resize(); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - }); - - describe(`tags should no longer fit after making container smaller`, () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - - //make smaller - tagCloud._size = []; - tagCloud.resize(); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test('completeness should not be ok', () => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE); - }); - }); - - describe('tagcloudscreenshot', () => { - afterEach(teardownDOM); - - test('should render simple image', async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - - expect(domNode.innerHTML).toMatchSnapshot(); - }); - }); - - function verifyTagProperties(expectedValues, actualElements, tagCloud) { - expect(actualElements.length).toEqual(expectedValues.length); - expectedValues.forEach((test, index) => { - try { - expect(actualElements[index].style.fontSize).toEqual(test.fontSize); - } catch (e) { - throw new Error('fontsize is not correct: ' + e.message); - } - try { - expect(actualElements[index].innerHTML).toEqual(test.text); - } catch (e) { - throw new Error('fontsize is not correct: ' + e.message); - } - isInsideContainer(actualElements[index], tagCloud); - }); - } - - function isInsideContainer(actualElement, tagCloud) { - const bbox = actualElement.getBoundingClientRect(); - verifyBbox(bbox, true, tagCloud); - } - - function verifyBbox(bbox, shouldBeInside, tagCloud) { - const message = ` | bbox-of-tag: ${JSON.stringify([ - bbox.left, - bbox.top, - bbox.right, - bbox.bottom, - ])} vs - bbox-of-container: ${domNode.offsetWidth},${domNode.offsetHeight} - debugInfo: ${JSON.stringify(tagCloud.getDebugInfo())}`; - - try { - expect(bbox.top >= 0 && bbox.top <= domNode.offsetHeight).toBe(shouldBeInside); - } catch (e) { - throw new Error( - 'top boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message - ); - } - try { - expect(bbox.bottom >= 0 && bbox.bottom <= domNode.offsetHeight).toBe(shouldBeInside); - } catch (e) { - throw new Error( - 'bottom boundary of tag should have been ' + - (shouldBeInside ? 'inside' : 'outside') + - message - ); - } - try { - expect(bbox.left >= 0 && bbox.left <= domNode.offsetWidth).toBe(shouldBeInside); - } catch (e) { - throw new Error( - 'left boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message - ); - } - try { - expect(bbox.right >= 0 && bbox.right <= domNode.offsetWidth).toBe(shouldBeInside); - } catch (e) { - throw new Error( - 'right boundary of tag should have been ' + - (shouldBeInside ? 'inside' : 'outside') + - message - ); - } - } - - /** - * In CI, this entire suite "blips" about 1/5 times. - * This blip causes the majority of these tests fail for the exact same reason: One tag is centered inside the container, - * while the others are moved out. - * This has not been reproduced locally yet. - * It may be an issue with the 3rd party d3-cloud that snags. - * - * The test suite should continue to catch reliably catch regressions of other sorts: unexpected and other uncaught errors, - * scaling issues, ordering issues - * - */ - function shouldAssert() { - const debugInfo = tagCloud.getDebugInfo(); - const count = debugInfo.positions.length; - const largest = debugInfo.positions.pop(); //test suite puts largest tag at the end. - - const centered = largest[1] === 0 && largest[2] === 0; - const halfWidth = debugInfo.size.width / 2; - const halfHeight = debugInfo.size.height / 2; - const inside = debugInfo.positions.filter((position) => { - const x = position.x + halfWidth; - const y = position.y + halfHeight; - return 0 <= x && x <= debugInfo.size.width && 0 <= y && y <= debugInfo.size.height; - }); - - return centered && inside.length === count - 1; - } - - function handleExpectedBlip(assertion) { - return () => { - if (!shouldAssert()) { - return; - } - assertion(); - }; - } -}); diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx new file mode 100644 index 0000000000000..b4d4e70d5ffe3 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { Wordcloud, Settings } from '@elastic/charts'; +import { chartPluginMock } from '../../../charts/public/mocks'; +import type { Datatable } from '../../../expressions/public'; +import { mount } from 'enzyme'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import TagCloudChart, { TagCloudChartProps } from './tag_cloud_chart'; +import { TagCloudVisParams } from '../types'; + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => { + return { + deserialize: jest.fn(), + }; + }), +})); + +const palettesRegistry = chartPluginMock.createPaletteRegistry(); +const visData = ({ + columns: [ + { + id: 'col-0', + name: 'geo.dest: Descending', + }, + { + id: 'col-1', + name: 'Count', + }, + ], + rows: [ + { 'col-0': 'CN', 'col-1': 26 }, + { 'col-0': 'IN', 'col-1': 17 }, + { 'col-0': 'US', 'col-1': 6 }, + { 'col-0': 'DE', 'col-1': 4 }, + { 'col-0': 'BR', 'col-1': 3 }, + ], +} as unknown) as Datatable; + +const visParams = { + bucket: { accessor: 0, format: {} }, + metric: { accessor: 1, format: {} }, + scale: 'linear', + orientation: 'single', + palette: { + type: 'palette', + name: 'default', + }, + minFontSize: 12, + maxFontSize: 70, + showLabel: true, +} as TagCloudVisParams; + +describe('TagCloudChart', function () { + let wrapperProps: TagCloudChartProps; + + beforeAll(() => { + wrapperProps = { + visData, + visParams, + palettesRegistry, + fireEvent: jest.fn(), + renderComplete: jest.fn(), + syncColors: false, + visType: 'tagcloud', + }; + }); + + it('renders the Wordcloud component', async () => { + const component = mount(); + expect(component.find(Wordcloud).length).toBe(1); + }); + + it('renders the label correctly', async () => { + const component = mount(); + const label = findTestSubject(component, 'tagCloudLabel'); + expect(label.text()).toEqual('geo.dest: Descending - Count'); + }); + + it('not renders the label if showLabel setting is off', async () => { + const newVisParams = { ...visParams, showLabel: false }; + const newProps = { ...wrapperProps, visParams: newVisParams }; + const component = mount(); + const label = findTestSubject(component, 'tagCloudLabel'); + expect(label.length).toBe(0); + }); + + it('receives the data on the correct format', () => { + const component = mount(); + expect(component.find(Wordcloud).prop('data')).toStrictEqual([ + { + color: 'black', + text: 'CN', + weight: 1, + }, + { + color: 'black', + text: 'IN', + weight: 0.6086956521739131, + }, + { + color: 'black', + text: 'US', + weight: 0.13043478260869565, + }, + { + color: 'black', + text: 'DE', + weight: 0.043478260869565216, + }, + { + color: 'black', + text: 'BR', + weight: 0, + }, + ]); + }); + + it('sets the angles correctly', async () => { + const newVisParams = { ...visParams, orientation: 'right angled' } as TagCloudVisParams; + const newProps = { ...wrapperProps, visParams: newVisParams }; + const component = mount(); + expect(component.find(Wordcloud).prop('endAngle')).toBe(90); + expect(component.find(Wordcloud).prop('angleCount')).toBe(2); + }); + + it('calls filter callback', () => { + const component = mount(); + component.find(Settings).prop('onElementClick')!([ + [ + { + text: 'BR', + weight: 0.17391304347826086, + color: '#d36086', + }, + { + specId: 'tagCloud', + key: 'tagCloud', + }, + ], + ]); + expect(wrapperProps.fireEvent).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx index f668e22815b60..b89fe2fa90ede 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx @@ -6,64 +6,225 @@ * Side Public License, v 1. */ -import React, { useEffect, useMemo, useRef } from 'react'; -import { EuiResizeObserver } from '@elastic/eui'; +import React, { useCallback, useState, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { throttle } from 'lodash'; - -import { TagCloudVisDependencies } from '../plugin'; +import { EuiIconTip, EuiResizeObserver } from '@elastic/eui'; +import { Chart, Settings, Wordcloud, RenderChangeListener } from '@elastic/charts'; +import type { PaletteRegistry } from '../../../charts/public'; +import type { IInterpreterRenderHandlers } from '../../../expressions/public'; +import { getFormatService } from '../services'; import { TagCloudVisRenderValue } from '../tag_cloud_fn'; -// @ts-ignore -import { TagCloudVisualization } from './tag_cloud_visualization'; import './tag_cloud.scss'; -type TagCloudChartProps = TagCloudVisDependencies & - TagCloudVisRenderValue & { - fireEvent: (event: any) => void; - renderComplete: () => void; - }; +const MAX_TAG_COUNT = 200; + +export type TagCloudChartProps = TagCloudVisRenderValue & { + fireEvent: IInterpreterRenderHandlers['event']; + renderComplete: IInterpreterRenderHandlers['done']; + palettesRegistry: PaletteRegistry; +}; + +const calculateWeight = (value: number, x1: number, y1: number, x2: number, y2: number) => + ((value - x1) * (y2 - x2)) / (y1 - x1) + x2; + +const getColor = ( + palettes: PaletteRegistry, + activePalette: string, + text: string, + values: string[], + syncColors: boolean +) => { + return palettes?.get(activePalette).getCategoricalColor( + [ + { + name: text, + rankAtDepth: values.length ? values.findIndex((name) => name === text) : 0, + totalSeriesAtDepth: values.length || 1, + }, + ], + { + maxDepth: 1, + totalSeries: values.length || 1, + behindText: false, + syncColors, + } + ); +}; + +const ORIENTATIONS = { + single: { + endAngle: 0, + angleCount: 360, + }, + 'right angled': { + endAngle: 90, + angleCount: 2, + }, + multiple: { + endAngle: -90, + angleCount: 12, + }, +}; export const TagCloudChart = ({ - colors, visData, visParams, + palettesRegistry, fireEvent, renderComplete, + syncColors, }: TagCloudChartProps) => { - const chartDiv = useRef(null); - const visController = useRef(null); + const [warning, setWarning] = useState(false); + const { bucket, metric, scale, palette, showLabel, orientation } = visParams; + const bucketFormatter = bucket ? getFormatService().deserialize(bucket.format) : null; - useEffect(() => { - if (chartDiv.current) { - visController.current = new TagCloudVisualization(chartDiv.current, colors, fireEvent); - } - return () => { - visController.current.destroy(); - visController.current = null; - }; - }, [colors, fireEvent]); - - useEffect(() => { - if (visController.current) { - visController.current.render(visData, visParams).then(renderComplete); - } - }, [visData, visParams, renderComplete]); + const tagCloudData = useMemo(() => { + const tagColumn = bucket ? visData.columns[bucket.accessor].id : -1; + const metricColumn = visData.columns[metric.accessor]?.id; + + const metrics = visData.rows.map((row) => row[metricColumn]); + const values = bucket ? visData.rows.map((row) => row[tagColumn]) : []; + const maxValue = Math.max(...metrics); + const minValue = Math.min(...metrics); + + return visData.rows.map((row) => { + const tag = row[tagColumn] === undefined ? 'all' : row[tagColumn]; + return { + text: (bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag) as string, + weight: + tag === 'all' || visData.rows.length <= 1 + ? 1 + : calculateWeight(row[metricColumn], minValue, maxValue, 0, 1) || 0, + color: getColor(palettesRegistry, palette.name, tag, values, syncColors) || 'rgba(0,0,0,0)', + }; + }); + }, [ + bucket, + bucketFormatter, + metric.accessor, + palette.name, + palettesRegistry, + syncColors, + visData.columns, + visData.rows, + ]); + + const label = bucket + ? `${visData.columns[bucket.accessor].name} - ${visData.columns[metric.accessor].name}` + : ''; + + const onRenderChange = useCallback( + (isRendered) => { + if (isRendered) { + renderComplete(); + } + }, + [renderComplete] + ); - const updateChartSize = useMemo( + const updateChart = useMemo( () => throttle(() => { - if (visController.current) { - visController.current.render(visData, visParams).then(renderComplete); - } + setWarning(false); }, 300), - [renderComplete, visData, visParams] + [] + ); + + const handleWordClick = useCallback( + (d) => { + if (!bucket) { + return; + } + const termsBucket = visData.columns[bucket.accessor]; + const clickedValue = d[0][0].text; + + const rowIndex = visData.rows.findIndex((row) => { + const formattedValue = bucketFormatter + ? bucketFormatter.convert(row[termsBucket.id], 'text') + : row[termsBucket.id]; + return formattedValue === clickedValue; + }); + + if (rowIndex < 0) { + return; + } + + fireEvent({ + name: 'filterBucket', + data: { + data: [ + { + table: visData, + column: bucket.accessor, + row: rowIndex, + }, + ], + }, + }); + }, + [bucket, bucketFormatter, fireEvent, visData] ); return ( - + {(resizeRef) => ( -
-
+
+ + + { + setWarning(true); + }} + /> + + {label && showLabel && ( +
+ {label} +
+ )} + {warning && ( +
+ + } + /> +
+ )} + {tagCloudData.length > MAX_TAG_COUNT && ( +
+ + } + /> +
+ )}
)} diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index d5e005a638680..6682799a8038a 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -6,16 +6,22 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; -import { SelectOption, SwitchOption } from '../../../vis_default_editor/public'; +import type { PaletteRegistry } from '../../../charts/public'; +import { VisEditorOptionsProps } from '../../../visualizations/public'; +import { SelectOption, SwitchOption, PalettePicker } from '../../../vis_default_editor/public'; import { ValidatedDualRange } from '../../../kibana_react/public'; -import { TagCloudVisParams } from '../types'; +import { TagCloudVisParams, TagCloudTypeProps } from '../types'; import { collections } from './collections'; -function TagCloudOptions({ stateParams, setValue }: VisEditorOptionsProps) { +interface TagCloudOptionsProps + extends VisEditorOptionsProps, + TagCloudTypeProps {} + +function TagCloudOptions({ stateParams, setValue, palettes }: TagCloudOptionsProps) { + const [palettesRegistry, setPalettesRegistry] = useState(undefined); const handleFontSizeChange = ([minFontSize, maxFontSize]: [string | number, string | number]) => { setValue('minFontSize', Number(minFontSize)); setValue('maxFontSize', Number(maxFontSize)); @@ -24,6 +30,14 @@ function TagCloudOptions({ stateParams, setValue }: VisEditorOptionsProps { + const fetchPalettes = async () => { + const palettesService = await palettes?.getPalettes(); + setPalettesRegistry(palettesService); + }; + fetchPalettes(); + }, [palettes]); + return ( + {palettesRegistry && ( + { + setValue(paramName, value); + }} + /> + )} + { - if (!this._visParams.bucket) { - return; - } - - fireEvent({ - name: 'filterBucket', - data: { - data: [ - { - table: event.meta.data, - column: 0, - row: event.meta.rowIndex, - }, - ], - }, - }); - }); - this._renderComplete$ = Rx.fromEvent(this._tagCloud, 'renderComplete'); - - this._feedbackNode = document.createElement('div'); - this._containerNode.appendChild(this._feedbackNode); - this._feedbackMessage = React.createRef(); - render( - - - , - this._feedbackNode - ); - - this._labelNode = document.createElement('div'); - this._containerNode.appendChild(this._labelNode); - this._label = React.createRef(); - render(