diff --git a/.backportrc.json b/.backportrc.json index a97c82ca0efa9..e44d3ce114299 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -3,6 +3,7 @@ "targetBranchChoices": [ { "name": "master", "checked": true }, { "name": "7.x", "checked": true }, + "7.11", "7.10", "7.9", "7.8", @@ -28,7 +29,7 @@ "targetPRLabels": ["backport"], "branchLabelMapping": { "^v8.0.0$": "master", - "^v7.11.0$": "7.x", + "^v7.12.0$": "7.x", "^v(\\d+).(\\d+).\\d+$": "$1.$2" } } diff --git a/.ci/Dockerfile b/.ci/Dockerfile index ec7befe05f0d4..cf827fc0ed08f 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=14.15.1 +ARG NODE_VERSION=14.15.2 FROM node:${NODE_VERSION} AS base diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0993876f98a6a..94afc5dd22ba9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -258,7 +258,7 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/ @elastic/kibana-alerting-services /docs/user/alerting/ @elastic/kibana-alerting-services /docs/management/alerting/ @elastic/kibana-alerting-services -#CC# /x-pack/plugins/stack_alerts @elastic/kibana-alerting-services +#CC# /x-pack/plugins/stack_alerts/ @elastic/kibana-alerting-services # Enterprise Search # Shared diff --git a/data/.empty b/data/.empty deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/docs/api/upgrade-assistant.asciidoc b/docs/api/upgrade-assistant.asciidoc index 15d87fbd0dc9d..39d3cb9ecb76b 100644 --- a/docs/api/upgrade-assistant.asciidoc +++ b/docs/api/upgrade-assistant.asciidoc @@ -22,5 +22,6 @@ include::upgrade-assistant/status.asciidoc[] include::upgrade-assistant/reindexing.asciidoc[] include::upgrade-assistant/batch_reindexing.asciidoc[] include::upgrade-assistant/batch_queue.asciidoc[] +include::upgrade-assistant/default-field.asciidoc[] include::upgrade-assistant/check_reindex_status.asciidoc[] include::upgrade-assistant/cancel_reindex.asciidoc[] diff --git a/docs/api/upgrade-assistant/check_reindex_status.asciidoc b/docs/api/upgrade-assistant/check_reindex_status.asciidoc index 98cf263673f73..75aac7b3699f5 100644 --- a/docs/api/upgrade-assistant/check_reindex_status.asciidoc +++ b/docs/api/upgrade-assistant/check_reindex_status.asciidoc @@ -6,8 +6,6 @@ experimental[] Check the status of the reindex operation. -Check the status of the reindex operation. - [[check-reindex-status-request]] ==== Request diff --git a/docs/api/upgrade-assistant/default-field.asciidoc b/docs/api/upgrade-assistant/default-field.asciidoc new file mode 100644 index 0000000000000..8bdcd359d5668 --- /dev/null +++ b/docs/api/upgrade-assistant/default-field.asciidoc @@ -0,0 +1,113 @@ +[[upgrade-assistant-api-default-field]] +=== Add default field API +++++ +Add default field +++++ + +experimental[] In {es} 7.0 and later, some query types, such as Simple Query String, have a limit to the number of fields they can query against. +To configure the cap in {es}, set the `indices.query.bool.max_clause_count` cluster setting, which is 1024 by default. + +For indices with more fields than the cap, add the `index.query.default_field` index setting to inform {es} which +fields to use by default when no field is specified for a query. Use the add default field API to add the `index.query.default_field` setting to an {es} index. + +[[upgrade-assistant-api-default-field-request]] +==== Request + +To add the `index.query.default_field` setting to an {es} index, submit a POST request to `/api/upgrade_assistant/add_query_default_field/`: + +[source,js] +-------------------------------------------------- +GET /api/upgrade_assistant/add_query_default_field/myIndex +{ + "fieldTypes": ["text", "keyword"], <1> + "otherFields": ["myField.*"] <2> +} +-------------------------------------------------- +// KIBANA + +<1> A required array of {es} field types that generate the list of fields. +<2> An optional array of additional field names, dot-deliminated. + +To add the `index.query.default_field` index setting to the specified index, {kib} generates an array of all fields from the index mapping. +The fields contain the types specified in `fieldTypes`. {kib} appends any other fields specified in `otherFields` to the array of default fields. + +[[upgrade-assistant-api-default-field-response-codes]] +==== Response codes + +`200`:: + Indicates a successful call. + + `400`:: + Indicates that the index already has the `index.query.default_field` setting. No changes are made to the index. + +[[upgrade-assistant-api-default-field-response-body]] +==== Response body + +The response body contains a JSON structure, similar to the following: + +[source,js] +-------------------------------------------------- +{ + "acknowledged": true +} +-------------------------------------------------- + +[[upgrade-assistant-api-default-field-example]] +==== Example + +Your index contains following mappings: + +[source,js] +-------------------------------------------------- +GET /myIndex/_mappings +{ + "myIndex": { + "mappings": { + "properties": { + "field1": { "type": "text" }, + "field2": { "type": "float" }, + "nestedfield": { + "properties": { + "field3": { "type": "keyword" }, + "field4": { "type": "long" }, + } + } + } + } + } +} +-------------------------------------------------- +// CONSOLE + +Make the following request to {kib}: + +[source,js] +-------------------------------------------------- +GET /api/upgrade_assistant/add_query_default_field/myIndex +{ + "fieldTypes": ["text", "long"], + "otherFields": ["field2"] +} +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,js] +-------------------------------------------------- +GET /myIndex/_settings?flat_settings=true +{ + "myIndex": { + "settings": { + "index.query.default_field": [ + "field1", + "nestedfield.field4", + "field2", + ] + } + } +} +-------------------------------------------------- +// CONSOLE + +{kib} generates the `field1` and `nestedfield.field4` values based on the specified `fieldTypes`, then appends the `otherFields` to the array. \ No newline at end of file diff --git a/docs/canvas/canvas-share-workpad.asciidoc b/docs/canvas/canvas-share-workpad.asciidoc index f49e2a944c900..0df8357bb3bd6 100644 --- a/docs/canvas/canvas-share-workpad.asciidoc +++ b/docs/canvas/canvas-share-workpad.asciidoc @@ -36,7 +36,7 @@ For more information, refer to <> or a script. -To begin, click *Share > PDF reports > Copy POST URL*. +To begin, click *Share > PDF reports > Advanced options > Copy POST URL*. [role="screenshot"] image::images/canvas-create-URL.gif[Image showing how to create POST URL] diff --git a/docs/canvas/images/canvas-create-URL.gif b/docs/canvas/images/canvas-create-URL.gif index 11327224fc897..60d69cdd599a3 100644 Binary files a/docs/canvas/images/canvas-create-URL.gif and b/docs/canvas/images/canvas-create-URL.gif differ diff --git a/docs/canvas/images/canvas-generate-pdf.gif b/docs/canvas/images/canvas-generate-pdf.gif index 513f6b3b960f9..24711d01fbe0c 100644 Binary files a/docs/canvas/images/canvas-generate-pdf.gif and b/docs/canvas/images/canvas-generate-pdf.gif differ diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index e32984f911d97..d4d2b229eeba7 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -272,7 +272,7 @@ heatmap charts. |{kib-repo}blob/{branch}/src/plugins/vis_type_xy/README.md[visTypeXy] |Contains the new xy-axis chart using the elastic-charts library, which will eventually -replace the vislib xy-axis (bar, area, line) charts. +replace the vislib xy-axis charts including bar, area, and line. |{kib-repo}blob/{branch}/src/plugins/visualizations/README.md[visualizations] diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index 8e8bae5ad9c58..b24ced68b7d38 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -24,10 +24,10 @@ export interface App | [exactRoute](./kibana-plugin-core-public.app.exactroute.md) | boolean | If set to true, the application's route will only be checked against an exact match. Defaults to false. | | [icon](./kibana-plugin-core-public.app.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | | [id](./kibana-plugin-core-public.app.id.md) | string | The unique identifier of the application | +| [meta](./kibana-plugin-core-public.app.meta.md) | AppMeta | Meta data for an application that represent additional information for the app. See [AppMeta](./kibana-plugin-core-public.appmeta.md) | | [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState> | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-core-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md). | | [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | | [order](./kibana-plugin-core-public.app.order.md) | number | An ordinal used to sort nav links relative to one another for display. | -| [searchDeepLinks](./kibana-plugin-core-public.app.searchdeeplinks.md) | AppSearchDeepLink[] | Array of links that represent secondary in-app locations for the app. | | [status](./kibana-plugin-core-public.app.status.md) | AppStatus | The initial status of the application. Defaulting to accessible | | [title](./kibana-plugin-core-public.app.title.md) | string | The title of the application. | | [tooltip](./kibana-plugin-core-public.app.tooltip.md) | string | A tooltip shown when hovering over app link. | diff --git a/docs/development/core/public/kibana-plugin-core-public.app.meta.md b/docs/development/core/public/kibana-plugin-core-public.app.meta.md new file mode 100644 index 0000000000000..574fa11605aec --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.meta.md @@ -0,0 +1,43 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [meta](./kibana-plugin-core-public.app.meta.md) + +## App.meta property + +Meta data for an application that represent additional information for the app. See [AppMeta](./kibana-plugin-core-public.appmeta.md) + +Signature: + +```typescript +meta?: AppMeta; +``` + +## Remarks + +Used to populate navigational search results (where available). Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) for more details. + +## Example + + +```ts +core.application.register({ + id: 'my_app', + title: 'Translated title', + meta: { + keywords: ['translated keyword1', 'translated keyword2'], + searchDeepLinks: [ + { id: 'sub1', title: 'Sub1', path: '/sub1', keywords: ['subpath1'] }, + { + id: 'sub2', + title: 'Sub2', + searchDeepLinks: [ + { id: 'subsub', title: 'SubSub', path: '/sub2/sub', keywords: ['subpath2'] } + ] + } + ], + }, + mount: () => { ... } +}) + +``` + diff --git a/docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md b/docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md deleted file mode 100644 index 667fddbc212a5..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md +++ /dev/null @@ -1,42 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [searchDeepLinks](./kibana-plugin-core-public.app.searchdeeplinks.md) - -## App.searchDeepLinks property - -Array of links that represent secondary in-app locations for the app. - -Signature: - -```typescript -searchDeepLinks?: AppSearchDeepLink[]; -``` - -## Remarks - -Used to populate navigational search results (where available). Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See for more details. - -## Example - -The `path` property on deep links should not include the application's `appRoute`: - -```ts -core.application.register({ - id: 'my_app', - title: 'My App', - searchDeepLinks: [ - { id: 'sub1', title: 'Sub1', path: '/sub1' }, - { - id: 'sub2', - title: 'Sub2', - searchDeepLinks: [ - { id: 'subsub', title: 'SubSub', path: '/sub2/sub' } - ] - } - ], - mount: () => { ... }, -}) - -``` -Will produce deep links on these paths: - `/app/my_app/sub1` - `/app/my_app/sub2/sub` - diff --git a/docs/development/core/public/kibana-plugin-core-public.appmeta.keywords.md b/docs/development/core/public/kibana-plugin-core-public.appmeta.keywords.md new file mode 100644 index 0000000000000..13709df68e76a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appmeta.keywords.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMeta](./kibana-plugin-core-public.appmeta.md) > [keywords](./kibana-plugin-core-public.appmeta.keywords.md) + +## AppMeta.keywords property + +Keywords to represent this application + +Signature: + +```typescript +keywords?: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appmeta.md b/docs/development/core/public/kibana-plugin-core-public.appmeta.md new file mode 100644 index 0000000000000..a2b72f7ec799d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appmeta.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMeta](./kibana-plugin-core-public.appmeta.md) + +## AppMeta interface + +Input type for meta data for an application. + +Meta fields include `keywords` and `searchDeepLinks` Keywords is an array of string with which to associate the app, must include at least one unique string as an array. `searchDeepLinks` is an array of links that represent secondary in-app locations for the app. + +Signature: + +```typescript +export interface AppMeta +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [keywords](./kibana-plugin-core-public.appmeta.keywords.md) | string[] | Keywords to represent this application | +| [searchDeepLinks](./kibana-plugin-core-public.appmeta.searchdeeplinks.md) | AppSearchDeepLink[] | Array of links that represent secondary in-app locations for the app. | + diff --git a/docs/development/core/public/kibana-plugin-core-public.appmeta.searchdeeplinks.md b/docs/development/core/public/kibana-plugin-core-public.appmeta.searchdeeplinks.md new file mode 100644 index 0000000000000..7ec0bbaa4b418 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appmeta.searchdeeplinks.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMeta](./kibana-plugin-core-public.appmeta.md) > [searchDeepLinks](./kibana-plugin-core-public.appmeta.searchdeeplinks.md) + +## AppMeta.searchDeepLinks property + +Array of links that represent secondary in-app locations for the app. + +Signature: + +```typescript +searchDeepLinks?: AppSearchDeepLink[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md b/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md index 7e5ccf7d06ed1..29aad675fb105 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md +++ b/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md @@ -17,8 +17,10 @@ export declare type AppSearchDeepLink = { } & ({ path: string; searchDeepLinks?: AppSearchDeepLink[]; + keywords?: string[]; } | { path?: string; searchDeepLinks: AppSearchDeepLink[]; + keywords?: string[]; }); ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md index b6f404c3d11aa..55672d9339f61 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md @@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug Signature: ```typescript -export declare type AppUpdatableFields = Pick; +export declare type AppUpdatableFields = Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 522c01124de82..d73ed716e6b19 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -102,6 +102,7 @@ readonly links: { }; readonly management: Record; readonly ml: Record; + readonly transforms: Record; readonly visualize: Record; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 2bb885cba434f..7aa170eef9b50 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly visualize: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
} | | diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index da19377054499..7f671d9edcd86 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -37,6 +37,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppLeaveDefaultAction](./kibana-plugin-core-public.appleavedefaultaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md) to execute the default behaviour when leaving the application.See | | [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | | | [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | | +| [AppMeta](./kibana-plugin-core-public.appmeta.md) | Input type for meta data for an application.Meta fields include keywords and searchDeepLinks Keywords is an array of string with which to associate the app, must include at least one unique string as an array. searchDeepLinks is an array of links that represent secondary in-app locations for the app. | | [AppMountContext](./kibana-plugin-core-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md). | | [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) | | | [Capabilities](./kibana-plugin-core-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | @@ -164,6 +165,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | | [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) | Public information about a registered [application](./kibana-plugin-core-public.app.md) | +| [PublicAppMetaInfo](./kibana-plugin-core-public.publicappmetainfo.md) | Public information about a registered app's [keywords](./kibana-plugin-core-public.appmeta.md) | | [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) | Public information about a registered app's [searchDeepLinks](./kibana-plugin-core-public.appsearchdeeplink.md) | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md index d56b0ac58cd9b..9f45a06935fe4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md +++ b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md @@ -9,10 +9,10 @@ Public information about a registered [application](./kibana-plugin-core-public. Signature: ```typescript -export declare type PublicAppInfo = Omit & { +export declare type PublicAppInfo = Omit & { status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; + meta: PublicAppMetaInfo; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappmetainfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappmetainfo.md new file mode 100644 index 0000000000000..3ef0460aec467 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.publicappmetainfo.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppMetaInfo](./kibana-plugin-core-public.publicappmetainfo.md) + +## PublicAppMetaInfo type + +Public information about a registered app's [keywords](./kibana-plugin-core-public.appmeta.md) + +Signature: + +```typescript +export declare type PublicAppMetaInfo = Omit & { + keywords: string[]; + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; +}; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md index 9814f0408d047..e88cdb7d55edd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md +++ b/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md @@ -9,7 +9,8 @@ Public information about a registered app's [searchDeepLinks](./kibana-plugin-co Signature: ```typescript -export declare type PublicAppSearchDeepLinkInfo = Omit & { +export declare type PublicAppSearchDeepLinkInfo = Omit & { searchDeepLinks: PublicAppSearchDeepLinkInfo[]; + keywords: string[]; }; ``` 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.plugin.setup.md index 1ed6059c23062..20181a5208b52 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.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, { bfetch, expressions, uiActions, usageCollection }: DataSetupDependencies): DataPublicPluginSetup; +setup(core: CoreSetup, { bfetch, expressions, uiActions, usageCollection, inspector }: DataSetupDependencies): DataPublicPluginSetup; ``` ## Parameters @@ -15,7 +15,7 @@ setup(core: CoreSetup, { bfetch, e | Parameter | Type | Description | | --- | --- | --- | | core | CoreSetup<DataStartDependencies, DataPublicPluginStart> | | -| { bfetch, expressions, uiActions, usageCollection } | DataSetupDependencies | | +| { bfetch, expressions, uiActions, usageCollection, inspector } | DataSetupDependencies | | Returns: diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.data.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.data.md deleted file mode 100644 index 0ddbcb3546d1e..0000000000000 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.data.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [Adapters](./kibana-plugin-plugins-embeddable-public.adapters.md) > [data](./kibana-plugin-plugins-embeddable-public.adapters.data.md) - -## Adapters.data property - -Signature: - -```typescript -data?: DataAdapter; -``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.md index 47484dc79d88c..8ba759e333fa3 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.md @@ -16,6 +16,5 @@ export interface Adapters | Property | Type | Description | | --- | --- | --- | -| [data](./kibana-plugin-plugins-embeddable-public.adapters.data.md) | DataAdapter | | | [requests](./kibana-plugin-plugins-embeddable-public.adapters.requests.md) | RequestAdapter | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md index a03ea32482011..1b97c9e11f83c 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md @@ -20,6 +20,7 @@ | [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) | ExpressionsService class is used for multiple purposes:1. It implements the same Expressions service that can be used on both: (1) server-side and (2) browser-side. 2. It implements the same Expressions service that users can fork/clone, thus have their own instance of the Expressions plugin. 3. ExpressionsService defines the public contracts of \*setup\* and \*start\* Kibana Platform life-cycles for ease-of-use on server-side and browser-side. 4. ExpressionsService creates a bound version of all exported contract functions. 5. Functions are bound the way there are:\`\`\`ts registerFunction = (...args: Parameters<Executor\['registerFunction'\]> ): ReturnType<Executor\['registerFunction'\]> => this.executor.registerFunction(...args); \`\`\`so that JSDoc appears in developers IDE when they use those plugins.expressions.registerFunction(. | | [ExpressionType](./kibana-plugin-plugins-expressions-public.expressiontype.md) | | | [FunctionsRegistry](./kibana-plugin-plugins-expressions-public.functionsregistry.md) | | +| [TablesAdapter](./kibana-plugin-plugins-expressions-public.tablesadapter.md) | | | [TypesRegistry](./kibana-plugin-plugins-expressions-public.typesregistry.md) | | ## Enumerations diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.tablesadapter.logdatatable.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.tablesadapter.logdatatable.md new file mode 100644 index 0000000000000..281f48918416b --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.tablesadapter.logdatatable.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [TablesAdapter](./kibana-plugin-plugins-expressions-public.tablesadapter.md) > [logDatatable](./kibana-plugin-plugins-expressions-public.tablesadapter.logdatatable.md) + +## TablesAdapter.logDatatable() method + +Signature: + +```typescript +logDatatable(name: string, datatable: Datatable): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | +| datatable | Datatable | | + +Returns: + +`void` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.tablesadapter.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.tablesadapter.md new file mode 100644 index 0000000000000..c489eff4cc252 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.tablesadapter.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [TablesAdapter](./kibana-plugin-plugins-expressions-public.tablesadapter.md) + +## TablesAdapter class + +Signature: + +```typescript +export declare class TablesAdapter extends EventEmitter +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [tables](./kibana-plugin-plugins-expressions-public.tablesadapter.tables.md) | | {
[key: string]: Datatable;
} | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [logDatatable(name, datatable)](./kibana-plugin-plugins-expressions-public.tablesadapter.logdatatable.md) | | | + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.tablesadapter.tables.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.tablesadapter.tables.md new file mode 100644 index 0000000000000..ef5ada66e50b3 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.tablesadapter.tables.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [TablesAdapter](./kibana-plugin-plugins-expressions-public.tablesadapter.md) > [tables](./kibana-plugin-plugins-expressions-public.tablesadapter.tables.md) + +## TablesAdapter.tables property + +Signature: + +```typescript +get tables(): { + [key: string]: Datatable; + }; +``` diff --git a/docs/fleet/fleet.asciidoc b/docs/fleet/fleet.asciidoc index aac733ad8468c..4777800ce5d57 100644 --- a/docs/fleet/fleet.asciidoc +++ b/docs/fleet/fleet.asciidoc @@ -24,4 +24,4 @@ image::fleet/images/fleet-start.png[{fleet} app in {kib}] == Get started To get started with {fleet}, refer to the -{ingest-guide}/index.html[{fleet}] docs. +{fleet-guide}/index.html[{fleet}] docs. diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index ccb6e931d69e3..02abde9d03a3b 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -140,4 +140,4 @@ For more information, refer to <>. If you are you ready to add your own data, refer to <>. -If you want to ingest your data, refer to {ingest-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack]. +If you want to ingest your data, refer to {fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack]. diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 6244a43b54f72..9a87d4c9d886a 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -453,6 +453,9 @@ of buckets to try to represent. ==== Visualization [horizontal] +[[visualization-visualize-chartslibrary]]`visualization:visualize:chartsLibrary`:: +Enables the new charts library for area, line, and bar charts in visualization panels. Does *not* support the split chart aggregation. + [[visualization-colormapping]]`visualization:colorMapping`:: **This setting is deprecated and will not be supported as of 8.0.** Maps values to specific colors in *Visualize* charts and *TSVB*. This setting does not apply to *Lens*. diff --git a/docs/management/images/tags/bulk-assign-selection.png b/docs/management/images/tags/bulk-assign-selection.png new file mode 100644 index 0000000000000..1c8687226b51f Binary files /dev/null and b/docs/management/images/tags/bulk-assign-selection.png differ diff --git a/docs/management/images/tags/create-tag.png b/docs/management/images/tags/create-tag.png new file mode 100644 index 0000000000000..a88e754457b9f Binary files /dev/null and b/docs/management/images/tags/create-tag.png differ diff --git a/docs/management/images/tags/manage-assignments-flyout.png b/docs/management/images/tags/manage-assignments-flyout.png new file mode 100644 index 0000000000000..a4e0b7a49d96a Binary files /dev/null and b/docs/management/images/tags/manage-assignments-flyout.png differ diff --git a/docs/management/images/tags/tag-management-section.png b/docs/management/images/tags/tag-management-section.png new file mode 100644 index 0000000000000..4aae3ea067820 Binary files /dev/null and b/docs/management/images/tags/tag-management-section.png differ diff --git a/docs/management/managing-tags.asciidoc b/docs/management/managing-tags.asciidoc new file mode 100644 index 0000000000000..3da98b2281fdc --- /dev/null +++ b/docs/management/managing-tags.asciidoc @@ -0,0 +1,75 @@ +[role="xpack"] +[[managing-tags]] +== Tags + +Tags enable you to categorize your saved objects. You can then easily filter for related objects based on shared tags. + +To begin, open the main menu, click *Stack Management*, then click *Tags*. + +[role="screenshot"] +image::images/tags/tag-management-section.png[Tags management section] + +[float] +=== Required permissions + +Access to *Tags* requires the `Tag Management` {kib} privilege. To add the privilege, open the menu, +click *Stack Management*, then click *Roles*. + +In addition: + +* The `read` privilege allows you to assign tags to the saved objects for which you have write permission. +* The `write` privilege enables you to create, edit, and delete tags. + + +NOTE: Having the `Tag Management` {kib} privilege is not required to +view tags assigned on objects the user has `read` access to, or to filter objects by tags +in {kib} applications or from the navigational search. + +[float] +[[settings-create-tag]] +=== Create a tag + +Create a tag to assign to your saved objects. + +. Click *Create tag*. ++ +[role="screenshot"] +image::images/tags/create-tag.png[Tag creation popin] +. Enter a name and select a color for the new tag. ++ +The name can include alphanumeric characters (English letters and digits), `:`, `-`, `_` and the space character, +and cannot be longer than 50 characters. +. Click *Create tag*. + +[float] +[[settings-assign-tag]] +=== Assign a tag to saved objects + +Assign or remove tags to one or more saved objects. You must have `write` permission +on the objects to which you assign the tags. + +. Click the action (...) icon in the tag row, and then select the *Manage assignments* action. ++ +[role="screenshot"] +image::images/tags/manage-assignments-flyout.png[Assign flyout] +. Select the objects to which you want to assign or remove tags. +. Click on *Save tag assignments*. + +TIP: To assign multiple tags to objects at once, select their checkboxes +and then select *Manage tag assignments* from the *selected tags* menu. + +[role="screenshot"] +image::images/tags/bulk-assign-selection.png[Bulk assign tags] + +[float] +[[settings-delete-tag]] +=== Delete a tag + +Delete a tag and remove it from any saved objects. + +. Click the action (...) icon in the tag row, and then select the *Delete* action. + +. Click *Delete tag*. + +TIP: To delete multiple tags at once, select their checkboxes in the list view, +and then select *Delete* action from the *selected tags* menu. \ No newline at end of file diff --git a/docs/maps/connect-to-ems.asciidoc b/docs/maps/connect-to-ems.asciidoc index 45ced2e64aa73..a5b8010f21f97 100644 --- a/docs/maps/connect-to-ems.asciidoc +++ b/docs/maps/connect-to-ems.asciidoc @@ -35,3 +35,188 @@ To disable EMS, change your <> file. . Set `map.includeElasticMapsService` to `false` to turn off the EMS connection. . Set `map.tilemap.url` to the URL of your tile server. This configures the default tile layer of Maps. . (Optional) Set `map.regionmap` to the vector shapes of the administrative boundaries that you want to use. + +[float] +[id=elastic-maps-server] +=== Host Elastic Maps Service locally + +beta::[] + +If you cannot connect to Elastic Maps Service from the {kib} server or browser clients, and your cluster has the appropriate license level, you can opt to host the service on your own infrastructure. + +{hosted-ems} is a self-managed version of Elastic Maps Service offered as a Docker image that provides both the EMS basemaps and EMS boundaries. You must first download and run the image. After connecting it to your {es} cluster for license validation, you're guided to download and configure the basemaps database, which must be retrieved separately. + +IMPORTANT: {hosted-ems} does not serve raster tiles, needed by Vega, coordinate, and region map visualizations. + +You can use +docker pull+ to download the {hosted-ems} image from the Elastic Docker registry. + +ifeval::["{release-state}"=="unreleased"] +Version {version} of {hosted-ems} has not yet been released, so no Docker image is currently available for this version. +endif::[] + +ifeval::["{release-state}"!="unreleased"] + +["source","bash",subs="attributes"] +---------------------------------- +docker pull {ems-docker-image} +---------------------------------- + +Start {hosted-ems} and expose the default port `8080`: + +["source","bash",subs="attributes"] +---------------------------------- +docker run --rm --init --publish 8080:8080 \ + {ems-docker-image} +---------------------------------- + +Once {hosted-ems} is running, follow instructions from the webpage at `localhost:8080` to define a configuration file and download the basemaps database. + +[role="screenshot"] +image::images/elastic-maps-server-instructions.png[Set-up instructions] + +endif::[] + +[float] +[[elastic-maps-server-configuration]] +==== Configuration + +{hosted-ems} reads properties from a configuration file in YAML format that is validated on startup. The location of this file is provided by the `EMS_PATH_CONF` environment variable and defaults to `/usr/src/app/server/config/elastic-maps-server.yml`. + +*General settings* + +[cols="2*<"] +|=== +| [[ems-hostname]]`hostname` + | Specifies the host of the backend server. To allow remote users to connect, set the value to the IP address or DNS name of the {hosted-ems} container. *Default: _your-hostname_*. <>. + +| `port` + | Specifies the port used by the backend server. Default: *`8080`*. <>. + +| `ui` + | Controls the display of the status page and the layer preview. *Default: `true`* + +| `logging.level` + | Verbosity of {hosted-ems} logs. Valid values are `trace`, `debug`, `info`, `warn`, `error`, `fatal`, and `silent`. *Default: `info`* + +| `path.planet` + | Path of the basemaps database. *Default: `/usr/src/app/data/planet.mbtiles`* +|=== + + +*{es} connection and security settings* + +[cols="2*<"] +|=== + +| `elasticsearch.host` + | URL of the {es} instance to use for license validation. + +| `elasticsearch.username` and `elasticsearch.password` + | Credentials of a user with at least the `monitor` role. + +| `elasticsearch.ssl.certificateAuthorities` + | Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates that make up a trusted certificate chain for {hosted-ems}. This chain is used by {hosted-ems} to establish trust when connecting to your {es} cluster. <>. + +| `elasticsearch.ssl.certificate` and `elasticsearch.ssl.key`, and `elasticsearch.ssl.keyPassphrase` + | Optional settings that provide the paths to the PEM-format SSL certificate and key files and the key password. These files are used to verify the identity of {hosted-ems} to {es} and are required when `xpack.security.http.ssl.client_authentication` in {es} is set to `required`. <>. + +| `elasticsearch.ssl.verificationMode` + | Controls the verification of the server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection to {es}. Valid values are "`full`", "`certificate`", and "`none`". Using "`full`" performs hostname verification, using "`certificate`" skips hostname verification, and using "`none`" skips verification entirely. *Default: `full`*. <>. + +|=== + +*Server security settings* + +[cols="2*<"] +|=== + +| `ssl.enabled` + | Enables SSL/TLS for inbound connections to {hosted-ems}. When set to `true`, a certificate and its corresponding private key must be provided. *Default: `false`*. <>. + +| `ssl.certificateAuthorities` + | Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates that make up a trusted certificate chain for {hosted-ems}. This chain is used by the {hosted-ems} to establish trust when receiving inbound SSL/TLS connections from end users. <>. + +| `ssl.key`, `ssl.certificate`, and `ssl.keyPassphrase` + | Location of yor SSL key and certificate files and the password that decrypts the private key that is specified via `ssl.key`. This password is optional, as the key may not be encrypted. <>. + +| `ssl.supportedProtocols` + | An array of supported protocols with versions. +Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2`, `TLSv1.3`. *Default: `TLSv1.1`, `TLSv1.2`, `TLSv1.3`*. <>. + +| `ssl.cipherSuites` + | Details on the format, and the valid options, are available via the +https://www.openssl.org/docs/man1.1.1/man1/ciphers.html#CIPHER-LIST-FORMAT[OpenSSL cipher list format documentation]. +*Default: `TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256 ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-GCM-SHA384, DHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, DHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-SHA384, DHE-RSA-AES256-SHA384, ECDHE-RSA-AES256-SHA256, DHE-RSA-AES256-SHA256, HIGH,!aNULL, !eNULL, !EXPORT, !DES, !RC4, !MD5, !PSK, !SRP, !CAMELLIA`*. <>. +|=== + +[float] +[[elastic-maps-server-bind-mount-config]] +===== Bind-mounted configuration + +One way to configure {hosted-ems} is to provide `elastic-maps-server.yml` via bind-mounting. With +docker-compose+, the bind-mount can be specified like this: + +["source","yaml",subs="attributes"] +-------------------------------------------- +version: '2' +services: + {hosted-ems}: + image: {ems-docker-image} + volumes: + - ./elastic-maps-server.yml:/usr/src/app/config/elastic-maps-server.yml +-------------------------------------------- + +[float] +[[elastic-maps-server-envvar-config]] +===== Environment variable configuration +All configuration settings can be overridden by environment variables that are named with all uppercase letters and by replacing YAML periods with underscores. For example `elasticsearch.ssl.certificate` could be overridden by the environment variable `ELASTICSEARCH_SSL_CERTIFICATE`. Boolean variables must use the `true` or `false` strings. + +WARNING: All information that you include in environment variables is visible through the `ps` command, including sensitive information. + +These variables can be set with +docker-compose+ like this: + +["source","yaml",subs="attributes"] +---------------------------------------------------------- +version: '2' +services: + {hosted-ems}: + image: {ems-docker-image} + environment: + ELASTICSEARCH_HOST: http://elasticsearch.example.org + ELASTICSEARCH_USERNAME: 'ems' + ELASTICSEARCH_PASSWORD: 'changeme' +---------------------------------------------------------- + +[float] +[[elastic-maps-server-data]] +==== Data + +{hosted-ems} hosts vector layer boundaries and vector tile basemaps for the entire planet. Boundaries include world countries, global administrative regions, and specific country regions. A minimal basemap is provided with {hosted-ems}. This can be used for testing environments but won't be functional for normal operations. The full basemap (around 90GB file) needs to be mounted on the Docker container for {hosted-ems} to run normally. + +TIP: The available basemaps and boundaries can be explored from the `/maps` endpoint in a web page that is your self-managed equivalent to https://maps.elastic.co + + +[float] +[[elastic-maps-server-kibana]] +==== Kibana configuration + +With {hosted-ems} running, add the `map.emsUrl` configuration key in your <> file pointing to the root of the service. This setting will point {kib} to request EMS basemaps and boundaries from {hosted-ems}. Typically this will be the URL to the <> of {hosted-ems}. For example, `map.emsUrl: https://my-ems-server:8080`. + + +[float] +[[elastic-maps-server-check]] +==== Status check + +{hosted-ems} periodically runs a status check that is exposed in three different forms: + +* At the root of {hosted-ems}, a web page will render the status of the different services. +* A JSON representation of {hosted-ems} status is available at the `/status` endpoint. +* The Docker https://docs.docker.com/engine/reference/builder/#healthcheck[`HEALTHCHECK`] instruction is run by default and will inform about the health of the service, running a process equivalent to the `/status` endpoint. + +IMPORTANT: {hosted-ems} won't respond to any data request if the license validation is not fulfilled. + + +[float] +[[elastic-maps-server-logging]] +==== Logging + +Logs are generated in {ecs-ref}[ECS JSON format] and emitted to the standard output and to `/var/log/elastic-maps-server/elastic-maps-server.log`. The server won't rotate the logs automatically but the `logrotate` tool is installed in the image. Mount `/dev/null` to the default log path if you want to disable the output to that file. diff --git a/docs/maps/images/elastic-maps-server-instructions.png b/docs/maps/images/elastic-maps-server-instructions.png new file mode 100644 index 0000000000000..17e9163a845c2 Binary files /dev/null and b/docs/maps/images/elastic-maps-server-instructions.png differ diff --git a/docs/maps/index.asciidoc b/docs/maps/index.asciidoc index 3c3537826a6a8..59b592ba1ec59 100644 --- a/docs/maps/index.asciidoc +++ b/docs/maps/index.asciidoc @@ -1,3 +1,7 @@ +:ems-docker-repo: docker.elastic.co/elastic-maps-service/elastic-maps-server-ubi8 +:ems-docker-image: {ems-docker-repo}:{version} +:hosted-ems: Elastic Maps Server + [role="xpack"] [[maps]] = Maps diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc index abfd2d3a95bed..2d330445d9ced 100644 --- a/docs/settings/fleet-settings.asciidoc +++ b/docs/settings/fleet-settings.asciidoc @@ -10,7 +10,7 @@ experimental[] You can configure `xpack.fleet` settings in your `kibana.yml`. By default, {fleet} is enabled. To use {fleet}, you also need to configure {kib} and {es} hosts. -See the {ingest-guide}/index.html[{fleet}] docs for more information. +See the {fleet-guide}/index.html[{fleet}] docs for more information. [[general-fleet-settings-kb]] ==== General {fleet} settings diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 0af953ec2cb09..8c0aa12ffc4c6 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -37,7 +37,7 @@ ship with dashboards and visualizations, so you can quickly get insights into your data. To get started, refer to -{ingest-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack]. +{fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack]. [role="screenshot"] image::images/add-data-fleet.png[Add data using Fleet] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 8b50fc38167d3..febdf707dce9a 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -192,7 +192,7 @@ In addition to this setting, trusted certificates may be specified via <>. If the trust store has no password, leave this as blank. If the trust store has an empty password, set this to `""`. -| `elasticsearch.ssl.verificationMode:` +|[[elasticsearch-ssl-verificationMode]] `elasticsearch.ssl.verificationMode:` | Controls the verification of the server certificate that {kib} receives when making an outbound SSL/TLS connection to {es}. Valid values are `"full"`, `"certificate"`, and `"none"`. Using `"full"` performs hostname verification, @@ -526,7 +526,7 @@ users. If PKI authentication is enabled, this chain is also used by {kib} to ver In addition to this setting, trusted certificates may be specified via <> and/or <>. -| `server.ssl.cipherSuites:` +| [[server-ssl-cipherSuites]] `server.ssl.cipherSuites:` | Details on the format, and the valid options, are available via the https://www.openssl.org/docs/man1.1.1/man1/ciphers.html#CIPHER-LIST-FORMAT[OpenSSL cipher list format documentation]. *Default: `TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256 ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-GCM-SHA384, DHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, DHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-SHA384, DHE-RSA-AES256-SHA384, ECDHE-RSA-AES256-SHA256, DHE-RSA-AES256-SHA256, HIGH,!aNULL, !eNULL, !EXPORT, !DES, !RC4, !MD5, !PSK, !SRP, !CAMELLIA`*. @@ -585,7 +585,7 @@ the trust store has no password, leave this unset. If the trust store has an emp | {kib} binds to this port and redirects all http requests to https over the port configured as <>. -| `server.ssl.supportedProtocols:` +| [[server-ssl-supportedProtocols]] `server.ssl.supportedProtocols:` | An array of supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2`, `TLSv1.3`. *Default: TLSv1.1, TLSv1.2, TLSv1.3* diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 8933bf64d2736..9d2b0ae593b34 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -139,6 +139,9 @@ a| <> | Copy, edit, delete, import, and export your saved objects. These include dashboards, visualizations, maps, index patterns, Canvas workpads, and more. +| <> +|Create, manage, and assign tags to your saved objects. + | <> | Create spaces to organize your dashboards and other saved objects into categories. A space is isolated from all other spaces, @@ -196,6 +199,8 @@ include::{kib-repo-dir}/management/rollups/create_and_manage_rollups.asciidoc[] include::{kib-repo-dir}/management/managing-saved-objects.asciidoc[] +include::{kib-repo-dir}/management/managing-tags.asciidoc[] + include::security/index.asciidoc[] include::{kib-repo-dir}/management/snapshot-restore/index.asciidoc[] diff --git a/docs/user/reporting/images/canvas-full-page-layout.png b/docs/user/reporting/images/canvas-full-page-layout.png new file mode 100644 index 0000000000000..06c94f05531b2 Binary files /dev/null and b/docs/user/reporting/images/canvas-full-page-layout.png differ diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index 224973d3c840c..6ebeafef1013e 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -92,6 +92,16 @@ To create a printer-friendly PDF with multiple A4 portrait pages and two visuali image::user/reporting/images/preserve-layout-switch.png["Share"] +[float] +[[full-page-pdf]] +== Full page PDF layout —Canvas only + +To create a PDF without margins surrounding the Canvas workpad, turn on *Full page layout* before generating the PDF. + +[role="screenshot"] +image::user/reporting/images/canvas-full-page-layout.png["Full Page Layout"] + + [float] [[manage-report-history]] == View and manage report history diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index 4b3512ae3056b..7facde28e956f 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -58,7 +58,6 @@ authorization checks have passed, but before the response from {es} is received. Refer to the corresponding {es} logs for potential write errors. ============================================================================ - [cols="3*<"] |====== 3+a| @@ -89,9 +88,12 @@ Refer to the corresponding {es} logs for potential write errors. | `failure` | User is not authorized to create a connector. .2+| `alert_create` -| `unknown` | User is creating an alert rule. -| `failure` | User is not authorized to create an alert rule. +| `unknown` | User is creating an alert. +| `failure` | User is not authorized to create an alert. +.2+| `space_create` +| `unknown` | User is creating a space. +| `failure` | User is not authorized to create a space. 3+a| ====== Type: change @@ -121,28 +123,28 @@ Refer to the corresponding {es} logs for potential write errors. | `failure` | User is not authorized to update a connector. .2+| `alert_update` -| `unknown` | User is updating an alert rule. -| `failure` | User is not authorized to update an alert rule. +| `unknown` | User is updating an alert. +| `failure` | User is not authorized to update an alert. .2+| `alert_update_api_key` -| `unknown` | User is updating the API key of an alert rule. -| `failure` | User is not authorized to update the API key of an alert rule. +| `unknown` | User is updating the API key of an alert. +| `failure` | User is not authorized to update the API key of an alert. .2+| `alert_enable` -| `unknown` | User is enabling an alert rule. -| `failure` | User is not authorized to enable an alert rule. +| `unknown` | User is enabling an alert. +| `failure` | User is not authorized to enable an alert. .2+| `alert_disable` -| `unknown` | User is disabling an alert rule. -| `failure` | User is not authorized to disable an alert rule. +| `unknown` | User is disabling an alert. +| `failure` | User is not authorized to disable an alert. .2+| `alert_mute` -| `unknown` | User is muting an alert rule. -| `failure` | User is not authorized to mute an alert rule. +| `unknown` | User is muting an alert. +| `failure` | User is not authorized to mute an alert. .2+| `alert_unmute` -| `unknown` | User is unmuting an alert rule. -| `failure` | User is not authorized to unmute an alert rule. +| `unknown` | User is unmuting an alert. +| `failure` | User is not authorized to unmute an alert. .2+| `alert_instance_mute` | `unknown` | User is muting an alert instance. @@ -152,6 +154,9 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is unmuting an alert instance. | `failure` | User is not authorized to unmute an alert instance. +.2+| `space_update` +| `unknown` | User is updating a space. +| `failure` | User is not authorized to update a space. 3+a| ====== Type: deletion @@ -169,8 +174,12 @@ Refer to the corresponding {es} logs for potential write errors. | `failure` | User is not authorized to delete a connector. .2+| `alert_delete` -| `unknown` | User is deleting an alert rule. -| `failure` | User is not authorized to delete an alert rule. +| `unknown` | User is deleting an alert. +| `failure` | User is not authorized to delete an alert. + +.2+| `space_delete` +| `unknown` | User is deleting a space. +| `failure` | User is not authorized to delete a space. 3+a| ====== Type: access @@ -196,13 +205,20 @@ Refer to the corresponding {es} logs for potential write errors. | `failure` | User is not authorized to search for connectors. .2+| `alert_get` -| `success` | User has accessed an alert rule. -| `failure` | User is not authorized to access an alert rule. +| `success` | User has accessed an alert. +| `failure` | User is not authorized to access an alert. .2+| `alert_find` -| `success` | User has accessed an alert rule as part of a search operation. -| `failure` | User is not authorized to search for alert rules. +| `success` | User has accessed an alert as part of a search operation. +| `failure` | User is not authorized to search for alerts. + +.2+| `space_get` +| `success` | User has accessed a space. +| `failure` | User is not authorized to access a space. +.2+| `space_find` +| `success` | User has accessed a space as part of a search operation. +| `failure` | User is not authorized to search for spaces. 3+a| ===== Category: web diff --git a/jest.config.oss.js b/jest.config.oss.js index e9235069687e0..874fd1e929a7a 100644 --- a/jest.config.oss.js +++ b/jest.config.oss.js @@ -18,6 +18,7 @@ */ module.exports = { + preset: '@kbn/test', rootDir: '.', projects: [ '/packages/*/jest.config.js', @@ -26,5 +27,4 @@ module.exports = { '/src/plugins/*/jest.config.js', '/test/*/jest.config.js', ], - reporters: ['default', '/packages/kbn-test/target/jest/junit_reporter'], }; diff --git a/package.json b/package.json index 9bc363242a332..b4b3cbe22b715 100644 --- a/package.json +++ b/package.json @@ -609,7 +609,7 @@ "cpy": "^8.1.1", "cronstrue": "^1.51.0", "css-loader": "^3.4.2", - "cypress": "^6.0.1", + "cypress": "^6.1.0", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", "d3": "3.5.17", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index c58d010a1f317..08d883a7cbb4d 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -6,7 +6,7 @@ pageLoadAssetSize: beatsManagement: 188135 bfetch: 41874 canvas: 1066647 - charts: 159211 + charts: 195358 cloud: 21076 console: 46091 core: 692106 @@ -98,7 +98,7 @@ pageLoadAssetSize: visTypeTimeseries: 155203 visTypeVega: 153573 visTypeVislib: 242838 - visTypeXy: 20255 + visTypeXy: 113478 visualizations: 295025 visualize: 57431 watcher: 43598 diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 89ed60909aa55..55514da3e231f 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -111,7 +111,7 @@ module.exports = { // An array of regexp pattern strings that are matched against all source file paths, matched files to include/exclude for code coverage collectCoverageFrom: [ '**/*.{js,mjs,jsx,ts,tsx}', - '!**/{__test__,__snapshots__,__examples__,mocks,tests,test_helpers,integration_tests,types}/**/*', + '!**/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*', '!**/*mock*.ts', '!**/*.test.ts', '!**/*.d.ts', diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index b39aa70c888fe..96d669a7b96f4 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -31,6 +31,7 @@ export { AppNavLinkStatus, AppUpdatableFields, AppUpdater, + AppMeta, AppSearchDeepLink, ApplicationSetup, ApplicationStart, @@ -41,6 +42,7 @@ export { AppLeaveConfirmAction, NavigateToAppOptions, PublicAppInfo, + PublicAppMetaInfo, PublicAppSearchDeepLinkInfo, // Internal types InternalApplicationSetup, diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index c161a7f166541..0a31490ad664c 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -83,7 +83,7 @@ export enum AppNavLinkStatus { */ export type AppUpdatableFields = Pick< App, - 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'searchDeepLinks' + 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'meta' >; /** @@ -237,39 +237,76 @@ export interface App { exactRoute?: boolean; /** - * Array of links that represent secondary in-app locations for the app. + * Meta data for an application that represent additional information for the app. + * See {@link AppMeta} * * @remarks * Used to populate navigational search results (where available). - * Can be updated using the {@link App.updater$} observable. See {@link AppSubLink} for more details. + * Can be updated using the {@link App.updater$} observable. See {@link PublicAppSearchDeepLinkInfo} for more details. * * @example - * The `path` property on deep links should not include the application's `appRoute`: * ```ts * core.application.register({ * id: 'my_app', - * title: 'My App', - * searchDeepLinks: [ - * { id: 'sub1', title: 'Sub1', path: '/sub1' }, + * title: 'Translated title', + * meta: { + * keywords: ['translated keyword1', 'translated keyword2'], + * searchDeepLinks: [ + * { id: 'sub1', title: 'Sub1', path: '/sub1', keywords: ['subpath1'] }, * { * id: 'sub2', * title: 'Sub2', * searchDeepLinks: [ - * { id: 'subsub', title: 'SubSub', path: '/sub2/sub' } + * { id: 'subsub', title: 'SubSub', path: '/sub2/sub', keywords: ['subpath2'] } * ] * } * ], - * mount: () => { ... }, + * }, + * mount: () => { ... } * }) * ``` - * - * Will produce deep links on these paths: - * - `/app/my_app/sub1` - * - `/app/my_app/sub2/sub` */ + meta?: AppMeta; +} + +/** + * Input type for meta data for an application. + * + * Meta fields include `keywords` and `searchDeepLinks` + * Keywords is an array of string with which to associate the app, must include at least one unique string as an array. + * `searchDeepLinks` is an array of links that represent secondary in-app locations for the app. + * @public + */ +export interface AppMeta { + /** Keywords to represent this application */ + keywords?: string[]; + /** Array of links that represent secondary in-app locations for the app. */ searchDeepLinks?: AppSearchDeepLink[]; } +/** + * Public information about a registered app's {@link AppMeta | keywords } + * + * @public + */ +export type PublicAppMetaInfo = Omit & { + keywords: string[]; + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; +}; + +/** + * Public information about a registered app's {@link AppSearchDeepLink | searchDeepLinks} + * + * @public + */ +export type PublicAppSearchDeepLinkInfo = Omit< + AppSearchDeepLink, + 'searchDeepLinks' | 'keywords' +> & { + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; + keywords: string[]; +}; + /** * Input type for registering secondary in-app locations for an application. * @@ -289,35 +326,30 @@ export type AppSearchDeepLink = { path: string; /** Optional array of links that are 'underneath' this section in the hierarchy */ searchDeepLinks?: AppSearchDeepLink[]; + /** Optional keywords to match with in deep links search for the page at the path */ + keywords?: string[]; } | { /** Optional path to access this section. Omit if this part of the hierarchy does not have a page URL. */ path?: string; /** Array links that are 'underneath' this section in this hierarchy. */ searchDeepLinks: AppSearchDeepLink[]; + /** Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. */ + keywords?: string[]; } ); -/** - * Public information about a registered app's {@link AppSearchDeepLink | searchDeepLinks} - * - * @public - */ -export type PublicAppSearchDeepLinkInfo = Omit & { - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; -}; - /** * Public information about a registered {@link App | application} * * @public */ -export type PublicAppInfo = Omit & { +export type PublicAppInfo = Omit & { // remove optional on fields populated with default values status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; + meta: PublicAppMetaInfo; }; /** diff --git a/src/core/public/application/utils/get_app_info.test.ts b/src/core/public/application/utils/get_app_info.test.ts index ee0bd4f1eadfa..ff09a18c8b655 100644 --- a/src/core/public/application/utils/get_app_info.test.ts +++ b/src/core/public/application/utils/get_app_info.test.ts @@ -43,19 +43,24 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, - searchDeepLinks: [], + meta: { + keywords: [], + searchDeepLinks: [], + }, }); }); it('populates default values for nested searchDeepLinks', () => { const app = createApp({ - searchDeepLinks: [ - { - id: 'sub-id', - title: 'sub-title', - searchDeepLinks: [{ id: 'sub-sub-id', title: 'sub-sub-title', path: '/sub-sub' }], - }, - ], + meta: { + searchDeepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + searchDeepLinks: [{ id: 'sub-sub-id', title: 'sub-sub-title', path: '/sub-sub' }], + }, + ], + }, }); const info = getAppInfo(app); @@ -65,20 +70,25 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, - searchDeepLinks: [ - { - id: 'sub-id', - title: 'sub-title', - searchDeepLinks: [ - { - id: 'sub-sub-id', - title: 'sub-sub-title', - path: '/sub-sub', - searchDeepLinks: [], // default empty array added - }, - ], - }, - ], + meta: { + keywords: [], + searchDeepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + keywords: [], + searchDeepLinks: [ + { + id: 'sub-sub-id', + title: 'sub-sub-title', + path: '/sub-sub', + keywords: [], + searchDeepLinks: [], // default empty array added + }, + ], + }, + ], + }, }); }); @@ -108,4 +118,53 @@ describe('getAppInfo', () => { }) ); }); + + it('adds default meta fields to sublinks when needed', () => { + const app = createApp({ + meta: { + searchDeepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + searchDeepLinks: [ + { + id: 'sub-sub-id', + title: 'sub-sub-title', + path: '/sub-sub', + keywords: ['sub sub'], + }, + ], + }, + ], + }, + }); + const info = getAppInfo(app); + + expect(info).toEqual({ + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.visible, + appRoute: `/app/some-id`, + meta: { + keywords: [], + searchDeepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + keywords: [], // default empty array + searchDeepLinks: [ + { + id: 'sub-sub-id', + title: 'sub-sub-title', + path: '/sub-sub', + keywords: ['sub sub'], + searchDeepLinks: [], + }, + ], + }, + ], + }, + }); + }); }); diff --git a/src/core/public/application/utils/get_app_info.ts b/src/core/public/application/utils/get_app_info.ts index 7316080816da7..574696c3bd5f2 100644 --- a/src/core/public/application/utils/get_app_info.ts +++ b/src/core/public/application/utils/get_app_info.ts @@ -39,7 +39,10 @@ export function getAppInfo(app: App): PublicAppInfo { status: app.status!, navLinkStatus, appRoute: app.appRoute!, - searchDeepLinks: getSearchDeepLinkInfos(app, app.searchDeepLinks), + meta: { + keywords: app.meta?.keywords ?? [], + searchDeepLinks: getSearchDeepLinkInfos(app, app.meta?.searchDeepLinks), + }, }; } @@ -57,6 +60,7 @@ function getSearchDeepLinkInfos( id: rawDeepLink.id, title: rawDeepLink.title, path: rawDeepLink.path, + keywords: rawDeepLink.keywords ?? [], searchDeepLinks: getSearchDeepLinkInfos(app, rawDeepLink.searchDeepLinks), }; } diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index 606370c5afd0a..44feb133cfcec 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -28,7 +28,10 @@ const app = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.default, appRoute: `/app/some-id`, - searchDeepLinks: [], + meta: { + keywords: [], + searchDeepLinks: [], + }, ...props, }); diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 07c07728d9084..b8843b5c85595 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -148,6 +148,9 @@ export class DocLinksService { outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-roc`, regressionEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`, }, + transforms: { + guide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/transforms.html`, + }, visualize: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/visualize.html`, timelionDeprecation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html#timelion-deprecation`, @@ -258,6 +261,7 @@ export interface DocLinksStart { }; readonly management: Record; readonly ml: Record; + readonly transforms: Record; readonly visualize: Record; }; } diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 2e1238df350e0..51375072d3e5a 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -108,10 +108,12 @@ export { AppLeaveConfirmAction, AppStatus, AppNavLinkStatus, + AppMeta, AppUpdatableFields, AppUpdater, AppSearchDeepLink, PublicAppInfo, + PublicAppMetaInfo, PublicAppSearchDeepLinkInfo, ScopedHistory, NavigateToAppOptions, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 64833c21df6bd..0303eb62b6419 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -56,11 +56,10 @@ export interface App { exactRoute?: boolean; icon?: string; id: string; + meta?: AppMeta; mount: AppMount | AppMountDeprecated; navLinkStatus?: AppNavLinkStatus; order?: number; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AppSubLink" - searchDeepLinks?: AppSearchDeepLink[]; status?: AppStatus; title: string; tooltip?: string; @@ -137,6 +136,12 @@ export interface ApplicationStart { registerMountContext(contextName: T, provider: IContextProvider): void; } +// @public +export interface AppMeta { + keywords?: string[]; + searchDeepLinks?: AppSearchDeepLink[]; +} + // @public export type AppMount = (params: AppMountParameters) => AppUnmount | Promise; @@ -186,9 +191,11 @@ export type AppSearchDeepLink = { } & ({ path: string; searchDeepLinks?: AppSearchDeepLink[]; + keywords?: string[]; } | { path?: string; searchDeepLinks: AppSearchDeepLink[]; + keywords?: string[]; }); // @public @@ -201,7 +208,7 @@ export enum AppStatus { export type AppUnmount = () => void; // @public -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick; // @public export type AppUpdater = (app: App) => Partial | undefined; @@ -574,6 +581,7 @@ export interface DocLinksStart { }; readonly management: Record; readonly ml: Record; + readonly transforms: Record; readonly visualize: Record; }; } @@ -1007,16 +1015,23 @@ export interface PluginInitializerContext export type PluginOpaqueId = symbol; // @public -export type PublicAppInfo = Omit & { +export type PublicAppInfo = Omit & { status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; + meta: PublicAppMetaInfo; +}; + +// @public +export type PublicAppMetaInfo = Omit & { + keywords: string[]; searchDeepLinks: PublicAppSearchDeepLinkInfo[]; }; // @public -export type PublicAppSearchDeepLinkInfo = Omit & { +export type PublicAppSearchDeepLinkInfo = Omit & { searchDeepLinks: PublicAppSearchDeepLinkInfo[]; + keywords: string[]; }; // @public diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index e04131e5f0e5e..71040598d34b1 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -20,6 +20,7 @@ import { Server } from 'http'; import { readFileSync } from 'fs'; import supertest from 'supertest'; +import { omit } from 'lodash'; import { ByteSizeValue, schema } from '@kbn/config-schema'; import { HttpConfig } from './http_config'; @@ -887,6 +888,53 @@ describe('conditional compression', () => { expect(response.header).not.toHaveProperty('content-encoding'); }); }); + + describe('response headers', () => { + it('allows to configure "keep-alive" header', async () => { + const { registerRouter, server: innerServer } = await server.setup({ + ...config, + keepaliveTimeout: 100_000, + }); + + const router = new Router('', logger, enhanceWithContext); + router.get({ path: '/', validate: false }, (context, req, res) => + res.ok({ body: req.route }) + ); + registerRouter(router); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/') + .set('Connection', 'keep-alive') + .expect(200); + + expect(response.header.connection).toBe('keep-alive'); + expect(response.header['keep-alive']).toBe('timeout=100'); + }); + + it('default headers', async () => { + const { registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router('', logger, enhanceWithContext); + router.get({ path: '/', validate: false }, (context, req, res) => + res.ok({ body: req.route }) + ); + registerRouter(router); + + await server.start(); + const response = await supertest(innerServer.listener).get('/').expect(200); + + const restHeaders = omit(response.header, ['date', 'content-length']); + expect(restHeaders).toMatchInlineSnapshot(` + Object { + "accept-ranges": "bytes", + "cache-control": "private, no-cache, no-store, must-revalidate", + "connection": "close", + "content-type": "application/json; charset=utf-8", + } + `); + }); + }); }); test('exposes route details of incoming request to a route handler (POST + payload options)', async () => { diff --git a/src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.test.ts b/src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.test.ts index 3186d7456383a..33787e3fce53a 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.test.ts @@ -22,14 +22,14 @@ import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors'; describe('catchRetryableEsClientErrors', () => { - it('rejects non-retryable response errors', () => { + it('rejects non-retryable response errors', async () => { const error = new esErrors.ResponseError( elasticsearchClientMock.createApiResponse({ body: { error: { type: 'cluster_block_exception' } }, statusCode: 400, }) ); - return expect(Promise.reject(error).catch(catchRetryableEsClientErrors)).rejects.toBe(error); + await expect(Promise.reject(error).catch(catchRetryableEsClientErrors)).rejects.toBe(error); }); describe('returns left retryable_es_client_error for', () => { it('NoLivingConnectionsError', async () => { diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 8947a5ec2171c..05432d65c0558 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -100,8 +100,9 @@ describe('migration actions', () => { describe('fetchIndices', () => { it('resolves right empty record if no indices were found', async () => { + expect.assertions(1); const task = fetchIndices(client, ['no_such_index']); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", "right": Object {}, @@ -109,12 +110,13 @@ describe('migration actions', () => { `); }); it('resolves right record with found indices', async () => { + expect.assertions(1); const res = (await fetchIndices(client, [ 'no_such_index', 'existing_index_with_docs', ])()) as Either.Right; - return expect(res.right).toEqual( + expect(res.right).toEqual( expect.objectContaining({ existing_index_with_docs: { aliases: {}, @@ -131,17 +133,19 @@ describe('migration actions', () => { await createIndex(client, 'new_index_without_write_block', { properties: {} })(); }); it('resolves right when setting the write block succeeds', async () => { + expect.assertions(1); const task = setWriteBlock(client, 'new_index_without_write_block'); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", "right": "set_write_block_succeeded", } `); }); - it('resolves right when setting a write block on an index that already has one', () => { + it('resolves right when setting a write block on an index that already has one', async () => { + expect.assertions(1); const task = setWriteBlock(client, 'existing_index_with_write_block'); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", "right": "set_write_block_succeeded", @@ -149,6 +153,7 @@ describe('migration actions', () => { `); }); it('once resolved, prevents further writes to the index', async () => { + expect.assertions(1); const task = setWriteBlock(client, 'new_index_without_write_block'); await task(); const sourceDocs = ([ @@ -157,13 +162,14 @@ describe('migration actions', () => { { _source: { title: 'doc 3' } }, { _source: { title: 'doc 4' } }, ] as unknown) as SavedObjectsRawDoc[]; - return expect( + await expect( bulkOverwriteTransformedDocuments(client, 'new_index_without_write_block', sourceDocs)() ).rejects.toMatchObject(expect.anything()); }); - it('resolves left index_not_found_exception when the index does not exist', () => { + it('resolves left index_not_found_exception when the index does not exist', async () => { + expect.assertions(1); const task = setWriteBlock(client, 'no_such_index'); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", "left": Object { @@ -180,43 +186,51 @@ describe('migration actions', () => { await createIndex(client, 'existing_index_with_write_block_2', { properties: {} })(); await setWriteBlock(client, 'existing_index_with_write_block_2')(); }); - it('resolves right if successful when an index already has a write block', () => { + it('resolves right if successful when an index already has a write block', async () => { + expect.assertions(1); const task = removeWriteBlock(client, 'existing_index_with_write_block_2'); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", "right": "remove_write_block_succeeded", } `); }); - it('resolves right if successful when an index does not have a write block', () => { + it('resolves right if successful when an index does not have a write block', async () => { + expect.assertions(1); const task = removeWriteBlock(client, 'existing_index_without_write_block_2'); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", "right": "remove_write_block_succeeded", } `); }); - it('rejects if there is a non-retryable error', () => { + it('rejects if there is a non-retryable error', async () => { + expect.assertions(1); const task = removeWriteBlock(client, 'no_such_index'); - return expect(task()).rejects.toMatchInlineSnapshot( + await expect(task()).rejects.toMatchInlineSnapshot( `[ResponseError: index_not_found_exception]` ); }); }); describe('cloneIndex', () => { - afterEach(async () => { + afterAll(async () => { try { - await client.indices.delete({ index: 'yellow_then_green_index' }); + await client.indices.delete({ index: 'clone_*' }); } catch (e) { /** ignore */ } }); - it('resolves right if cloning into a new target index', () => { - const task = cloneIndex(client, 'existing_index_with_write_block', 'yellow_then_green_index'); - expect(task()).resolves.toMatchInlineSnapshot(` + it('resolves right if cloning into a new target index', async () => { + expect.assertions(1); + const task = cloneIndex( + client, + 'existing_index_with_write_block', + 'clone_yellow_then_green_index_1' + ); + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", "right": Object { @@ -227,9 +241,10 @@ describe('migration actions', () => { `); }); it('resolves right after waiting for index status to be green if clone target already existed', async () => { + expect.assertions(2); // Create a yellow index await client.indices.create({ - index: 'yellow_then_green_index', + index: 'clone_yellow_then_green_index_2', body: { mappings: { properties: {} }, settings: { @@ -243,7 +258,7 @@ describe('migration actions', () => { const cloneIndexPromise = cloneIndex( client, 'existing_index_with_write_block', - 'yellow_then_green_index' + 'clone_yellow_then_green_index_2' )(); let indexGreen = false; @@ -258,7 +273,7 @@ describe('migration actions', () => { indexGreen = true; }, 10); - return cloneIndexPromise.then((res) => { + await cloneIndexPromise.then((res) => { // Assert that the promise didn't resolve before the index became green expect(indexGreen).toBe(true); expect(res).toMatchInlineSnapshot(` @@ -272,9 +287,10 @@ describe('migration actions', () => { `); }); }); - it('resolves left index_not_found_exception if the source index does not exist', () => { - const task = cloneIndex(client, 'no_such_index', 'yellow_then_green_index'); - expect(task()).resolves.toMatchInlineSnapshot(` + it('resolves left index_not_found_exception if the source index does not exist', async () => { + expect.assertions(1); + const task = cloneIndex(client, 'no_such_index', 'clone_yellow_then_green_index_3'); + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", "left": Object { @@ -289,6 +305,7 @@ describe('migration actions', () => { // Reindex doesn't return any errors on it's own, so we have to test // together with waitForReindexTask describe('reindex & waitForReindexTask', () => { + expect.assertions(2); it('resolves right when reindex succeeds without reindex script', async () => { const res = (await reindex( client, @@ -320,6 +337,7 @@ describe('migration actions', () => { `); }); it('resolves right when reindex succeeds with reindex script', async () => { + expect.assertions(2); const res = (await reindex( client, 'existing_index_with_docs', @@ -349,6 +367,7 @@ describe('migration actions', () => { `); }); it('resolves right, ignores version conflicts and does not update existing docs when reindex multiple times', async () => { + expect.assertions(3); // Reindex with a script let res = (await reindex( client, @@ -397,6 +416,7 @@ describe('migration actions', () => { `); }); it('resolves right and proceeds to add missing documents if there are some existing docs conflicts', async () => { + expect.assertions(2); // Simulate a reindex that only adds some of the documents from the // source index into the target index await createIndex(client, 'reindex_target_4', { properties: {} })(); @@ -444,6 +464,7 @@ describe('migration actions', () => { `); }); it('resolves left incompatible_mapping_exception if all reindex failures are due to a strict_dynamic_mapping_exception', async () => { + expect.assertions(1); // Simulates one instance having completed the UPDATE_TARGET_MAPPINGS // step which makes the mappings incompatible with outdated documents. // If another instance then tries a reindex it will get a @@ -479,6 +500,7 @@ describe('migration actions', () => { `); }); it('resolves left incompatible_mapping_exception if all reindex failures are due to a mapper_parsing_exception', async () => { + expect.assertions(1); // Simulates one instance having completed the UPDATE_TARGET_MAPPINGS // step which makes the mappings incompatible with outdated documents. // If another instance then tries a reindex it will get a @@ -512,6 +534,7 @@ describe('migration actions', () => { `); }); it('resolves left index_not_found_exception if source index does not exist', async () => { + expect.assertions(1); const res = (await reindex( client, 'no_such_index', @@ -520,7 +543,7 @@ describe('migration actions', () => { false )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", "left": Object { @@ -531,6 +554,7 @@ describe('migration actions', () => { `); }); it('resolves left target_index_had_write_block if all failures are due to a write block', async () => { + expect.assertions(1); const res = (await reindex( client, 'existing_index_with_docs', @@ -541,7 +565,7 @@ describe('migration actions', () => { const task = waitForReindexTask(client, res.right.taskId, '10s'); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", "left": Object { @@ -551,6 +575,7 @@ describe('migration actions', () => { `); }); it('resolves left if requireAlias=true and the target is not an alias', async () => { + expect.assertions(1); const res = (await reindex( client, 'existing_index_with_docs', @@ -561,7 +586,7 @@ describe('migration actions', () => { const task = waitForReindexTask(client, res.right.taskId, '10s'); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", "left": Object { @@ -575,6 +600,7 @@ describe('migration actions', () => { describe('verifyReindex', () => { it('resolves right if source and target indices have the same amount of documents', async () => { + expect.assertions(1); const res = (await reindex( client, 'existing_index_with_docs', @@ -592,9 +618,10 @@ describe('migration actions', () => { } `); }); - it('resolves left if source and target indices have different amount of documents', () => { + it('resolves left if source and target indices have different amount of documents', async () => { + expect.assertions(1); const task = verifyReindex(client, 'existing_index_with_docs', 'existing_index_2'); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", "left": Object { @@ -604,6 +631,7 @@ describe('migration actions', () => { `); }); it('rejects if source or target index does not exist', async () => { + expect.assertions(2); let task = verifyReindex(client, 'no_such_index', 'existing_index_2'); await expect(task()).rejects.toMatchInlineSnapshot( `[ResponseError: index_not_found_exception]` @@ -618,6 +646,7 @@ describe('migration actions', () => { describe('searchForOutdatedDocuments', () => { it('only returns documents that match the outdatedDocumentsQuery', async () => { + expect.assertions(2); const resultsWithQuery = ((await searchForOutdatedDocuments( client, 'existing_index_with_docs', @@ -635,6 +664,7 @@ describe('migration actions', () => { expect(resultsWithoutQuery.length).toBe(4); }); it('resolves with _id, _source, _seq_no and _primary_term', async () => { + expect.assertions(1); const results = ((await searchForOutdatedDocuments(client, 'existing_index_with_docs', { match: { title: { query: 'doc' } }, })()) as Either.Right).right.outdatedDocuments; @@ -655,6 +685,7 @@ describe('migration actions', () => { describe('waitForPickupUpdatedMappingsTask', () => { it('rejects if there are failures', async () => { + expect.assertions(1); const res = (await pickupUpdatedMappings( client, 'existing_index_with_write_block' @@ -664,11 +695,12 @@ describe('migration actions', () => { // We can't do a snapshot match because the response includes an index // id which ES assigns dynamically - return expect(task()).rejects.toMatchObject({ + await expect(task()).rejects.toMatchObject({ message: /pickupUpdatedMappings task failed with the following failures:\n\[\{\"index\":\"existing_index_with_write_block\"/, }); }); it('rejects if there is an error', async () => { + expect.assertions(1); const res = (await pickupUpdatedMappings( client, 'no_such_index' @@ -676,12 +708,13 @@ describe('migration actions', () => { const task = waitForPickupUpdatedMappingsTask(client, res.right.taskId, '10s'); - return expect(task()).rejects.toMatchInlineSnapshot(` + await expect(task()).rejects.toMatchInlineSnapshot(` [Error: pickupUpdatedMappings task failed with the following error: {"type":"index_not_found_exception","reason":"no such index [no_such_index]","resource.type":"index_or_alias","resource.id":"no_such_index","index_uuid":"_na_","index":"no_such_index"}] `); }); it('resolves right when successful', async () => { + expect.assertions(1); const res = (await pickupUpdatedMappings( client, 'existing_index_with_docs' @@ -689,7 +722,7 @@ describe('migration actions', () => { const task = waitForPickupUpdatedMappingsTask(client, res.right.taskId, '10s'); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", "right": "pickup_updated_mappings_succeeded", @@ -700,6 +733,7 @@ describe('migration actions', () => { describe('updateAndPickupMappings', () => { it('resolves right when mappings were updated and picked up', async () => { + expect.assertions(3); // Create an index without any mappings and insert documents into it await createIndex(client, 'existing_index_without_mappings', { dynamic: false as any, @@ -741,13 +775,14 @@ describe('migration actions', () => { 'existing_index_without_mappings', { match: { title: { query: 'doc' } } } )()) as Either.Right).right.outdatedDocuments; - return expect(pickedUpSearchResults.length).toBe(4); + expect(pickedUpSearchResults.length).toBe(4); }); }); describe('updateAliases', () => { describe('remove', () => { - it('resolves left index_not_found_exception when the index does not exist', () => { + it('resolves left index_not_found_exception when the index does not exist', async () => { + expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -757,7 +792,7 @@ describe('migration actions', () => { }, }, ]); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", "left": Object { @@ -769,6 +804,7 @@ describe('migration actions', () => { }); describe('with must_exist=false', () => { it('resolves left alias_not_found_exception when alias does not exist', async () => { + expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -778,7 +814,7 @@ describe('migration actions', () => { }, }, ]); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", "left": Object { @@ -790,6 +826,7 @@ describe('migration actions', () => { }); describe('with must_exist=true', () => { it('resolves left alias_not_found_exception when alias does not exist on specified index', async () => { + expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -799,7 +836,7 @@ describe('migration actions', () => { }, }, ]); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", "left": Object { @@ -809,6 +846,7 @@ describe('migration actions', () => { `); }); it('resolves left alias_not_found_exception when alias does not exist', async () => { + expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -818,7 +856,7 @@ describe('migration actions', () => { }, }, ]); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", "left": Object { @@ -830,7 +868,8 @@ describe('migration actions', () => { }); }); describe('remove_index', () => { - it('left index_not_found_exception if index does not exist', () => { + it('left index_not_found_exception if index does not exist', async () => { + expect.assertions(1); const task = updateAliases(client, [ { remove_index: { @@ -838,7 +877,7 @@ describe('migration actions', () => { }, }, ]); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", "left": Object { @@ -848,7 +887,8 @@ describe('migration actions', () => { } `); }); - it('left remove_index_not_a_concrete_index when remove_index targets an alias', () => { + it('left remove_index_not_a_concrete_index when remove_index targets an alias', async () => { + expect.assertions(1); const task = updateAliases(client, [ { remove_index: { @@ -856,7 +896,7 @@ describe('migration actions', () => { }, }, ]); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", "left": Object { @@ -873,6 +913,7 @@ describe('migration actions', () => { await client.indices.delete({ index: 'yellow_then_green_index' }); }); it('resolves right after waiting for an index status to be green if the index already existed', async () => { + expect.assertions(2); // Create a yellow index await client.indices.create( { @@ -903,7 +944,7 @@ describe('migration actions', () => { indexGreen = true; }, 10); - return createIndexPromise.then((res) => { + await createIndexPromise.then((res) => { // Assert that the promise didn't resolve before the index became green expect(indexGreen).toBe(true); expect(res).toMatchInlineSnapshot(` @@ -914,24 +955,26 @@ describe('migration actions', () => { `); }); }); - it('rejects when there is an unexpected error creating the index', () => { + it('rejects when there is an unexpected error creating the index', async () => { + expect.assertions(1); // Creating an index with the same name as an existing alias to induce // failure - expect( + await expect( createIndex(client, 'existing_index_2_alias', undefined as any)() ).rejects.toMatchInlineSnapshot(`[ResponseError: invalid_index_name_exception]`); }); }); describe('bulkOverwriteTransformedDocuments', () => { - it('resolves right when documents do not yet exist in the index', () => { + it('resolves right when documents do not yet exist in the index', async () => { + expect.assertions(1); const newDocs = ([ { _source: { title: 'doc 5' } }, { _source: { title: 'doc 6' } }, { _source: { title: 'doc 7' } }, ] as unknown) as SavedObjectsRawDoc[]; const task = bulkOverwriteTransformedDocuments(client, 'existing_index_with_docs', newDocs); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", "right": "bulk_index_succeeded", @@ -939,6 +982,7 @@ describe('migration actions', () => { `); }); it('resolves right even if there were some version_conflict_engine_exception', async () => { + expect.assertions(1); const existingDocs = ((await searchForOutdatedDocuments( client, 'existing_index_with_docs', @@ -949,20 +993,21 @@ describe('migration actions', () => { ...existingDocs, ({ _source: { title: 'doc 8' } } as unknown) as SavedObjectsRawDoc, ]); - return expect(task()).resolves.toMatchInlineSnapshot(` + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", "right": "bulk_index_succeeded", } `); }); - it('rejects if there are errors', () => { + it('rejects if there are errors', async () => { + expect.assertions(1); const newDocs = ([ { _source: { title: 'doc 5' } }, { _source: { title: 'doc 6' } }, { _source: { title: 'doc 7' } }, ] as unknown) as SavedObjectsRawDoc[]; - return expect( + await expect( bulkOverwriteTransformedDocuments(client, 'existing_index_with_write_block', newDocs)() ).rejects.toMatchObject(expect.anything()); }); diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts index 6dbb986e868ee..2baf27e94edb3 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts @@ -142,8 +142,8 @@ describe('migrationsStateActionMachine', () => { } `); }); - it('resolves when reaching the DONE state', () => { - return expect( + it('resolves when reaching the DONE state', async () => { + await expect( migrationStateActionMachine({ initialState, logger: mockLogger.get(), @@ -152,8 +152,8 @@ describe('migrationsStateActionMachine', () => { }) ).resolves.toEqual(expect.anything()); }); - it('resolves with migrated status if some sourceIndex in the DONE state', () => { - return expect( + it('resolves with migrated status if some sourceIndex in the DONE state', async () => { + await expect( migrationStateActionMachine({ initialState: { ...initialState, ...{ sourceIndex: Option.some('source-index') } }, logger: mockLogger.get(), @@ -162,8 +162,8 @@ describe('migrationsStateActionMachine', () => { }) ).resolves.toEqual(expect.objectContaining({ status: 'migrated' })); }); - it('resolves with patched status if none sourceIndex in the DONE state', () => { - return expect( + it('resolves with patched status if none sourceIndex in the DONE state', async () => { + await expect( migrationStateActionMachine({ initialState: { ...initialState, ...{ sourceIndex: Option.none } }, logger: mockLogger.get(), @@ -172,8 +172,8 @@ describe('migrationsStateActionMachine', () => { }) ).resolves.toEqual(expect.objectContaining({ status: 'patched' })); }); - it('rejects with error message when reaching the FATAL state', () => { - return expect( + it('rejects with error message when reaching the FATAL state', async () => { + await expect( migrationStateActionMachine({ initialState: { ...initialState, reason: 'the fatal reason' } as State, logger: mockLogger.get(), diff --git a/src/core/server/saved_objects/migrationsv2/state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/state_action_machine.test.ts index 15dde10eb21ec..3760b92591346 100644 --- a/src/core/server/saved_objects/migrationsv2/state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrationsv2/state_action_machine.test.ts @@ -86,14 +86,14 @@ describe('state action machine', () => { }); }); - test('rejects if an exception is throw from inside an action', () => { - return expect( + test('rejects if an exception is throw from inside an action', async () => { + await expect( stateActionMachine({ ...state, controlState: 'THROW' }, next, countUntilThree) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Invalid control state"`); }); - test('resolve with the final state once all steps are completed', () => { - return expect(finalStateP).resolves.toMatchInlineSnapshot(` + test('resolve with the final state once all steps are completed', async () => { + await expect(finalStateP).resolves.toMatchInlineSnapshot(` Object { "controlState": "DONE", "count": 3, @@ -101,8 +101,8 @@ describe('state action machine', () => { `); }); - test("rejects if control state doesn't change after 50 steps", () => { - return expect( + test("rejects if control state doesn't change after 50 steps", async () => { + await expect( stateActionMachine(state, next, countUntilModel(51)) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Control state didn't change after 50 steps aborting."` diff --git a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh index 68b4ca45ec911..bdc51d09a2aa5 100644 --- a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh +++ b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh @@ -77,7 +77,7 @@ case $1 in --gid "<%= group %>" \ --shell /sbin/nologin \ --comment "kibana service user" \ - "<%= user %>" \ + "<%= user %>" echo " OK" fi diff --git a/src/dev/code_coverage/nyc_config/nyc.functional.config.js b/src/dev/code_coverage/nyc_config/nyc.functional.config.js index 20d266ab9e2c3..9c025c6a98458 100644 --- a/src/dev/code_coverage/nyc_config/nyc.functional.config.js +++ b/src/dev/code_coverage/nyc_config/nyc.functional.config.js @@ -18,7 +18,7 @@ */ const defaultExclude = require('@istanbuljs/schema/default-exclude'); -const extraExclude = ['data/optimize/**', 'src/core/server/**', '**/test/**']; +const extraExclude = ['data/optimize/**', 'src/core/server/**', '**/{test, types}/**/*']; const path = require('path'); module.exports = { diff --git a/src/dev/code_coverage/shell_scripts/extract_archives.sh b/src/dev/code_coverage/shell_scripts/extract_archives.sh index 32b4ccd6abccb..376467f9f2e55 100644 --- a/src/dev/code_coverage/shell_scripts/extract_archives.sh +++ b/src/dev/code_coverage/shell_scripts/extract_archives.sh @@ -6,7 +6,7 @@ EXTRACT_DIR=/tmp/extracted_coverage mkdir -p $EXTRACT_DIR echo "### Extracting downloaded artifacts" -for x in kibana-intake kibana-oss-tests kibana-xpack-tests; do #x-pack-intake skipping due to failures +for x in kibana-intake x-pack-intake kibana-oss-tests kibana-xpack-tests; do tar -xzf $DOWNLOAD_DIR/coverage/${x}/kibana-coverage.tar.gz -C $EXTRACT_DIR || echo "### Error 'tarring': ${x}" done diff --git a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh index 5d983828394bf..62b81929ae79b 100644 --- a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh @@ -32,7 +32,7 @@ TEAM_ASSIGN_PATH=$5 # Build team assignments dat file node scripts/generate_team_assignments.js --verbose --src .github/CODEOWNERS --dest $TEAM_ASSIGN_PATH -for x in functional; do #jest skip due to failures +for x in jest functional; do echo "### Ingesting coverage for ${x}" COVERAGE_SUMMARY_FILE=target/kibana-coverage/${x}-combined/coverage-summary.json diff --git a/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh b/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh index a8952f987b419..707c6de3f88a0 100644 --- a/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh +++ b/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh @@ -4,6 +4,6 @@ COVERAGE_TEMP_DIR=/tmp/extracted_coverage/target/kibana-coverage/ export COVERAGE_TEMP_DIR echo "### Merge coverage reports" -for x in functional; do # jest skip due to failures +for x in jest functional; do yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.${x}.config.js done diff --git a/src/plugins/apm_oss/server/tutorial/instructions/apm_agent_instructions.ts b/src/plugins/apm_oss/server/tutorial/instructions/apm_agent_instructions.ts index a74223f28dd03..0694ed57a031a 100644 --- a/src/plugins/apm_oss/server/tutorial/instructions/apm_agent_instructions.ts +++ b/src/plugins/apm_oss/server/tutorial/instructions/apm_agent_instructions.ts @@ -46,10 +46,11 @@ This agent supports a variety of frameworks but can also be used with your custo } )} var apm = require('elastic-apm-node').start({curlyOpen} + // ${i18n.translate( 'apmOss.tutorial.nodeClient.configure.commands.setRequiredServiceNameComment', { - defaultMessage: 'Override service name from package.json', + defaultMessage: 'Override the service name from package.json', } )} // ${i18n.translate('apmOss.tutorial.nodeClient.configure.commands.allowedCharactersComment', { @@ -60,7 +61,7 @@ var apm = require('elastic-apm-node').start({curlyOpen} // ${i18n.translate( 'apmOss.tutorial.nodeClient.configure.commands.useIfApmRequiresTokenComment', { - defaultMessage: 'Use if APM Server requires a token', + defaultMessage: 'Use if APM Server requires a secret token', } )} secretToken: '${secretToken}', @@ -68,11 +69,19 @@ var apm = require('elastic-apm-node').start({curlyOpen} // ${i18n.translate( 'apmOss.tutorial.nodeClient.configure.commands.setCustomApmServerUrlComment', { - defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', + defaultMessage: 'Set the custom APM Server URL (default: {defaultApmServerUrl})', values: { defaultApmServerUrl: 'http://localhost:8200' }, } )} - serverUrl: '${apmServerUrl}' + serverUrl: '${apmServerUrl}', + + // ${i18n.translate( + 'apmOss.tutorial.nodeClient.configure.commands.setCustomServiceEnvironmentComment', + { + defaultMessage: 'Set the service environment', + } + )} + environment: 'production' {curlyClose})`.split('\n'), textPost: i18n.translate('apmOss.tutorial.nodeClient.configure.textPost', { defaultMessage: @@ -121,7 +130,7 @@ ELASTIC_APM = {curlyOpen} # ${i18n.translate( 'apmOss.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment', { - defaultMessage: 'Set required service name. Allowed characters:', + defaultMessage: 'Set the required service name. Allowed characters:', } )} # ${i18n.translate('apmOss.tutorial.djangoClient.configure.commands.allowedCharactersComment', { @@ -132,7 +141,7 @@ ELASTIC_APM = {curlyOpen} # ${i18n.translate( 'apmOss.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment', { - defaultMessage: 'Use if APM Server requires a token', + defaultMessage: 'Use if APM Server requires a secret token', } )} 'SECRET_TOKEN': '${secretToken}', @@ -140,11 +149,19 @@ ELASTIC_APM = {curlyOpen} # ${i18n.translate( 'apmOss.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment', { - defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', + defaultMessage: 'Set the custom APM Server URL (default: {defaultApmServerUrl})', values: { defaultApmServerUrl: 'http://localhost:8200' }, } )} 'SERVER_URL': '${apmServerUrl}', + + # ${i18n.translate( + 'apmOss.tutorial.djangoClient.configure.commands.setServiceEnvironmentComment', + { + defaultMessage: 'Set the service environment', + } + )} + 'ENVIRONMENT': 'production', {curlyClose} # ${i18n.translate('apmOss.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment', { @@ -201,7 +218,7 @@ app.config['ELASTIC_APM'] = {curlyOpen} # ${i18n.translate( 'apmOss.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment', { - defaultMessage: 'Set required service name. Allowed characters:', + defaultMessage: 'Set the required service name. Allowed characters:', } )} # ${i18n.translate('apmOss.tutorial.flaskClient.configure.commands.allowedCharactersComment', { @@ -212,7 +229,7 @@ app.config['ELASTIC_APM'] = {curlyOpen} # ${i18n.translate( 'apmOss.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment', { - defaultMessage: 'Use if APM Server requires a token', + defaultMessage: 'Use if APM Server requires a secret token', } )} 'SECRET_TOKEN': '${secretToken}', @@ -220,11 +237,19 @@ app.config['ELASTIC_APM'] = {curlyOpen} # ${i18n.translate( 'apmOss.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment', { - defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', + defaultMessage: 'Set the custom APM Server URL (default: {defaultApmServerUrl})', values: { defaultApmServerUrl: 'http://localhost:8200' }, } )} 'SERVER_URL': '${apmServerUrl}', + + # ${i18n.translate( + 'apmOss.tutorial.flaskClient.configure.commands.setServiceEnvironmentComment', + { + defaultMessage: 'Set the service environment', + } + )} + 'ENVIRONMENT': 'production', {curlyClose} apm = ElasticAPM(app)`.split('\n'), @@ -259,15 +284,18 @@ export const createRailsAgentInstructions = (apmServerUrl = '', secretToken = '' }), commands: `# config/elastic_apm.yml: -# Set service name - allowed characters: a-z, A-Z, 0-9, -, _ and space +# Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space # Defaults to the name of your Rails app -# service_name: 'my-service' +service_name: 'my-service' + +# Use if APM Server requires a secret token +secret_token: '${secretToken}' -# Use if APM Server requires a token -# secret_token: '${secretToken}' +# Set the custom APM Server URL (default: http://localhost:8200) +server_url: '${apmServerUrl || 'http://localhost:8200'}' -# Set custom APM Server URL (default: http://localhost:8200) -# server_url: '${apmServerUrl || 'http://localhost:8200'}'`.split('\n'), +# Set the service environment +environment: 'production'`.split('\n'), textPost: i18n.translate('apmOss.tutorial.railsClient.configure.textPost', { defaultMessage: 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', @@ -335,7 +363,7 @@ export const createRackAgentInstructions = (apmServerUrl = '', secretToken = '') commands: `# config/elastic_apm.yml: # ${i18n.translate('apmOss.tutorial.rackClient.createConfig.commands.setServiceNameComment', { - defaultMessage: 'Set service name - allowed characters: a-z, A-Z, 0-9, -, _ and space', + defaultMessage: 'Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space', })} # ${i18n.translate( 'apmOss.tutorial.rackClient.createConfig.commands.defaultsToTheNameOfRackAppClassComment', @@ -343,7 +371,7 @@ export const createRackAgentInstructions = (apmServerUrl = '', secretToken = '') defaultMessage: "Defaults to the name of your Rack app's class.", } )} -# service_name: 'my-service' +service_name: 'my-service' # ${i18n.translate( 'apmOss.tutorial.rackClient.createConfig.commands.useIfApmServerRequiresTokenComment', @@ -351,13 +379,18 @@ export const createRackAgentInstructions = (apmServerUrl = '', secretToken = '') defaultMessage: 'Use if APM Server requires a token', } )} -# secret_token: '${secretToken}' +secret_token: '${secretToken}' # ${i18n.translate('apmOss.tutorial.rackClient.createConfig.commands.setCustomApmServerComment', { defaultMessage: 'Set custom APM Server URL (default: {defaultServerUrl})', values: { defaultServerUrl: 'http://localhost:8200' }, })} -# server_url: '${apmServerUrl || 'http://localhost:8200'}'`.split('\n'), +server_url: '${apmServerUrl || 'http://localhost:8200'}', + +# ${i18n.translate('apmOss.tutorial.rackClient.createConfig.commands.setServiceEnvironment', { + defaultMessage: 'Set the service environment', + })} +environment: 'production'`.split('\n'), textPost: i18n.translate('apmOss.tutorial.rackClient.createConfig.textPost', { defaultMessage: 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', @@ -417,10 +450,18 @@ var apm = initApm({curlyOpen} // ${i18n.translate( 'apmOss.tutorial.jsClient.installDependency.commands.setServiceVersionComment', { - defaultMessage: 'Set service version (required for source map feature)', + defaultMessage: 'Set the service version (required for source map feature)', } )} - serviceVersion: '' + serviceVersion: '', + + // ${i18n.translate( + 'apmOss.tutorial.jsClient.installDependency.commands.setServiceEnvironmentComment', + { + defaultMessage: 'Set the service environment', + } + )} + environment: 'production' {curlyClose})`.split('\n'), textPost: i18n.translate('apmOss.tutorial.jsClient.installDependency.textPost', { defaultMessage: @@ -502,9 +543,14 @@ export ELASTIC_APM_SERVICE_NAME= export ELASTIC_APM_SERVER_URL=${apmServerUrl} # ${i18n.translate('apmOss.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment', { - defaultMessage: 'Use if APM Server requires a token', + defaultMessage: 'Use if APM Server requires a secret token', })} export ELASTIC_APM_SECRET_TOKEN=${secretToken} + +# ${i18n.translate('apmOss.tutorial.goClient.configure.commands.setServiceEnvironment', { + defaultMessage: 'Set the service environment', + })} +export ELASTIC_APM_ENVIRONMENT= `.split('\n'), textPost: i18n.translate('apmOss.tutorial.goClient.configure.textPost', { defaultMessage: 'See the [documentation]({documentationLink}) for advanced configuration.', @@ -568,8 +614,10 @@ Do **not** add the agent as a dependency to your application.', textPre: i18n.translate('apmOss.tutorial.javaClient.startApplication.textPre', { defaultMessage: 'Add the `-javaagent` flag and configure the agent with system properties.\n\n \ -* Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)\n \ -* Set custom APM Server URL (default: {customApmServerUrl})\n \ +* Set the required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)\n \ +* Set the custom APM Server URL (default: {customApmServerUrl})\n \ +* Set the APM Server secret token\n \ +* Set the service environment\n \ * Set the base package of your application', values: { customApmServerUrl: 'http://localhost:8200' }, }), @@ -577,6 +625,7 @@ Do **not** add the agent as a dependency to your application.', -Delastic.apm.service_name=my-application \\ -Delastic.apm.server_urls=${apmServerUrl || 'http://localhost:8200'} \\ -Delastic.apm.secret_token=${secretToken} \\ + -Delastic.apm.environment=production \\ -Delastic.apm.application_packages=org.example \\ -jar my-application.jar`.split('\n'), textPost: i18n.translate('apmOss.tutorial.javaClient.startApplication.textPost', { @@ -647,7 +696,8 @@ export const createDotNetAgentInstructions = (apmServerUrl = '', secretToken = ' "ServerUrls": "${ apmServerUrl || 'http://localhost:8200' }", //Set custom APM Server URL (default: http://localhost:8200) - "ServiceName" : "MyApp", //allowed characters: a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application + "ServiceName": "MyApp", //allowed characters: a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application + "Environment": "production", // Set the service environment {curlyClose} {curlyClose}`.split('\n'), textPost: i18n.translate('apmOss.tutorial.dotNetClient.configureAgent.textPost', { diff --git a/src/plugins/charts/public/services/legacy_colors/colors.test.ts b/src/plugins/charts/public/services/legacy_colors/colors.test.ts index 89cf7a4817377..b94918426c525 100644 --- a/src/plugins/charts/public/services/legacy_colors/colors.test.ts +++ b/src/plugins/charts/public/services/legacy_colors/colors.test.ts @@ -62,14 +62,17 @@ describe('Vislib Color Service', () => { it('should throw an error if input is not an array', () => { expect(() => { + // @ts-expect-error colors.createColorLookupFunction(200); }).toThrowError(); expect(() => { + // @ts-expect-error colors.createColorLookupFunction('help'); }).toThrowError(); expect(() => { + // @ts-expect-error colors.createColorLookupFunction(true); }).toThrowError(); @@ -78,10 +81,12 @@ describe('Vislib Color Service', () => { }).toThrowError(); expect(() => { + // @ts-expect-error colors.createColorLookupFunction(nullValue); }).toThrowError(); expect(() => { + // @ts-expect-error colors.createColorLookupFunction(emptyObject); }).toThrowError(); }); @@ -89,14 +94,17 @@ describe('Vislib Color Service', () => { describe('when array is not composed of numbers, strings, or undefined values', () => { it('should throw an error', () => { expect(() => { + // @ts-expect-error colors.createColorLookupFunction(arrayOfObjects); }).toThrowError(); expect(() => { + // @ts-expect-error colors.createColorLookupFunction(arrayOfBooleans); }).toThrowError(); expect(() => { + // @ts-expect-error colors.createColorLookupFunction(arrayOfNullValues); }).toThrowError(); }); @@ -113,6 +121,7 @@ describe('Vislib Color Service', () => { }).not.toThrowError(); expect(() => { + // @ts-expect-error colors.createColorLookupFunction(arrayOfUndefinedValues); }).not.toThrowError(); }); diff --git a/src/plugins/charts/public/services/legacy_colors/colors.ts b/src/plugins/charts/public/services/legacy_colors/colors.ts index e1342a114f8df..e367a780fb292 100644 --- a/src/plugins/charts/public/services/legacy_colors/colors.ts +++ b/src/plugins/charts/public/services/legacy_colors/colors.ts @@ -48,7 +48,7 @@ export class LegacyColorsService { } createColorLookupFunction( - arrayOfStringsOrNumbers?: any, + arrayOfStringsOrNumbers?: Array, colorMapping: Partial> = {} ) { if (!Array.isArray(arrayOfStringsOrNumbers)) { @@ -67,7 +67,7 @@ export class LegacyColorsService { this.mappedColors.mapKeys(arrayOfStringsOrNumbers); - return (value: string) => { + return (value: string | number) => { return colorMapping[value] || this.mappedColors.get(value); }; } diff --git a/src/plugins/charts/public/static/components/basic_options.tsx b/src/plugins/charts/public/static/components/basic_options.tsx index cac4c8d70d796..9c5a22543df99 100644 --- a/src/plugins/charts/public/static/components/basic_options.tsx +++ b/src/plugins/charts/public/static/components/basic_options.tsx @@ -18,9 +18,11 @@ */ import React from 'react'; + import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { VisOptionsProps } from '../../../../vis_default_editor/public'; + import { SwitchOption } from './switch'; import { SelectOption } from './select'; diff --git a/src/plugins/charts/public/static/components/collections.ts b/src/plugins/charts/public/static/components/collections.ts index 9dde50f2b44c9..16d875836a202 100644 --- a/src/plugins/charts/public/static/components/collections.ts +++ b/src/plugins/charts/public/static/components/collections.ts @@ -18,17 +18,22 @@ */ import { $Values } from '@kbn/utility-types'; +import { i18n } from '@kbn/i18n'; -export const ColorModes = Object.freeze({ - BACKGROUND: 'Background' as 'Background', - LABELS: 'Labels' as 'Labels', - NONE: 'None' as 'None', +export const ColorMode = Object.freeze({ + Background: 'Background' as 'Background', + Labels: 'Labels' as 'Labels', + None: 'None' as 'None', }); -export type ColorModes = $Values; +export type ColorMode = $Values; -export const Rotates = Object.freeze({ - HORIZONTAL: 0, - VERTICAL: 90, - ANGLED: 75, +export const LabelRotation = Object.freeze({ + Horizontal: 0, + Vertical: 90, + Angled: 75, +}); +export type LabelRotation = $Values; + +export const defaultCountLabel = i18n.translate('charts.countText', { + defaultMessage: 'Count', }); -export type Rotates = $Values; diff --git a/src/plugins/charts/public/static/components/color_picker.scss b/src/plugins/charts/public/static/components/color_picker.scss new file mode 100644 index 0000000000000..85bfefca41a09 --- /dev/null +++ b/src/plugins/charts/public/static/components/color_picker.scss @@ -0,0 +1,18 @@ +$visColorPickerWidth: $euiSizeL * 8; // 8 columns + +.visColorPicker__value { + width: $visColorPickerWidth; +} + +.visColorPicker__valueDot { + cursor: pointer; + + &:hover { + transform: scale(1.4); + } + + &-isSelected { + border: $euiSizeXS solid; + border-radius: 100%; + } +} diff --git a/src/plugins/charts/public/static/components/color_picker.tsx b/src/plugins/charts/public/static/components/color_picker.tsx new file mode 100644 index 0000000000000..d785a9c9ad4b7 --- /dev/null +++ b/src/plugins/charts/public/static/components/color_picker.tsx @@ -0,0 +1,138 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import classNames from 'classnames'; +import React, { BaseSyntheticEvent } from 'react'; + +import { EuiButtonEmpty, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import './color_picker.scss'; + +export const legendColors: string[] = [ + '#3F6833', + '#967302', + '#2F575E', + '#99440A', + '#58140C', + '#052B51', + '#511749', + '#3F2B5B', + '#508642', + '#CCA300', + '#447EBC', + '#C15C17', + '#890F02', + '#0A437C', + '#6D1F62', + '#584477', + '#629E51', + '#E5AC0E', + '#64B0C8', + '#E0752D', + '#BF1B00', + '#0A50A1', + '#962D82', + '#614D93', + '#7EB26D', + '#EAB839', + '#6ED0E0', + '#EF843C', + '#E24D42', + '#1F78C1', + '#BA43A9', + '#705DA0', + '#9AC48A', + '#F2C96D', + '#65C5DB', + '#F9934E', + '#EA6460', + '#5195CE', + '#D683CE', + '#806EB7', + '#B7DBAB', + '#F4D598', + '#70DBED', + '#F9BA8F', + '#F29191', + '#82B5D8', + '#E5A8E2', + '#AEA2E0', + '#E0F9D7', + '#FCEACA', + '#CFFAFF', + '#F9E2D2', + '#FCE2DE', + '#BADFF4', + '#F9D9F9', + '#DEDAF7', +]; + +interface ColorPickerProps { + id?: string; + label: string | number | null; + onChange: (color: string | null, event: BaseSyntheticEvent) => void; + color: string; +} + +export const ColorPicker = ({ onChange, color: selectedColor, id, label }: ColorPickerProps) => ( +
+ + + +
+ {legendColors.map((color) => ( + onChange(color, e)} + onKeyPress={(e) => onChange(color, e)} + className={classNames('visColorPicker__valueDot', { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'visColorPicker__valueDot-isSelected': color === selectedColor, + })} + style={{ color }} + data-test-subj={`visColorPickerColor-${color}`} + /> + ))} +
+ {legendColors.some((c) => c === selectedColor) && ( + + onChange(null, e)} + onKeyPress={(e: any) => onChange(null, e)} + > + + + + )} +
+); diff --git a/src/plugins/charts/public/static/components/current_time.tsx b/src/plugins/charts/public/static/components/current_time.tsx new file mode 100644 index 0000000000000..00c1a74a7bfd8 --- /dev/null +++ b/src/plugins/charts/public/static/components/current_time.tsx @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import moment, { Moment } from 'moment'; +import React, { FC } from 'react'; + +import { LineAnnotation, AnnotationDomainTypes, LineAnnotationStyle } from '@elastic/charts'; +import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; +import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; + +interface CurrentTimeProps { + isDarkMode: boolean; + domainEnd?: number | Moment; +} + +/** + * Render current time line annotation on @elastic/charts `Chart` + */ +export const CurrentTime: FC = ({ isDarkMode, domainEnd }) => { + const lineAnnotationStyle: Partial = { + line: { + strokeWidth: 2, + stroke: isDarkMode ? darkEuiTheme.euiColorDanger : lightEuiTheme.euiColorDanger, + opacity: 0.7, + }, + }; + + // Domain end of 'now' will be milliseconds behind current time, so we extend time by 1 minute and check if + // the annotation is within this range; if so, the line annotation uses the domainEnd as its value + const now = moment(); + const isAnnotationAtEdge = domainEnd + ? moment(domainEnd).add(1, 'm').isAfter(now) && now.isAfter(domainEnd) + : false; + const lineAnnotationData = [ + { + dataValue: isAnnotationAtEdge ? domainEnd : now.valueOf(), + }, + ]; + + return ( + + ); +}; diff --git a/src/plugins/charts/public/static/components/endzones.tsx b/src/plugins/charts/public/static/components/endzones.tsx new file mode 100644 index 0000000000000..db68f72c18c05 --- /dev/null +++ b/src/plugins/charts/public/static/components/endzones.tsx @@ -0,0 +1,197 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { FC } from 'react'; +import moment, { unitOfTime } from 'moment'; + +import { + TooltipValue, + RectAnnotation, + RectAnnotationDatum, + RectAnnotationStyle, +} from '@elastic/charts'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; +import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; +import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; + +interface EndzonesProps { + isDarkMode: boolean; + domainStart: number; + domainEnd: number; + interval: number; + domainMin: number; + domainMax: number; + hideTooltips?: boolean; + /** + * used to toggle full bin endzones for multiple non-stacked bars + */ + isFullBin?: boolean; +} + +export const Endzones: FC = ({ + isDarkMode, + domainStart, + domainEnd, + interval, + domainMin, + domainMax, + hideTooltips = true, + isFullBin = false, +}) => { + const rectAnnotationStyle: Partial = { + stroke: isDarkMode ? darkEuiTheme.euiColorLightShade : lightEuiTheme.euiColorDarkShade, + strokeWidth: 0, + opacity: isDarkMode ? 0.6 : 0.2, + fill: isDarkMode ? darkEuiTheme.euiColorLightShade : lightEuiTheme.euiColorDarkShade, + }; + + const rectAnnotations: RectAnnotationDatum[] = []; + + if (domainStart > domainMin) { + rectAnnotations.push({ + coordinates: { + x1: isFullBin ? domainMin : domainStart, + }, + }); + } + + if (domainEnd - interval < domainMax) { + rectAnnotations.push({ + coordinates: { + x0: isFullBin ? domainMax : domainEnd, + }, + }); + } + + return ( + + ); +}; + +const findIntervalFromDuration = ( + dateValue: number, + esValue: number, + esUnit: unitOfTime.Base, + timeZone: string +) => { + const date = moment.tz(dateValue, timeZone); + const startOfDate = moment.tz(date, timeZone).startOf(esUnit); + const endOfDate = moment.tz(date, timeZone).startOf(esUnit).add(esValue, esUnit); + return endOfDate.valueOf() - startOfDate.valueOf(); +}; + +const getIntervalInMs = ( + value: number, + esValue: number, + esUnit: unitOfTime.Base, + timeZone: string +): number => { + switch (esUnit) { + case 's': + return 1000 * esValue; + case 'ms': + return 1 * esValue; + default: + return findIntervalFromDuration(value, esValue, esUnit, timeZone); + } +}; + +/** + * Returns the adjusted interval based on the data + * + * @param xValues sorted and unquie x values + * @param esValue + * @param esUnit + * @param timeZone + */ +export const getAdjustedInterval = ( + xValues: number[], + esValue: number, + esUnit: unitOfTime.Base, + timeZone: string +): number => { + const newInterval = xValues.reduce((minInterval, currentXvalue, index) => { + let currentDiff = minInterval; + + if (index > 0) { + currentDiff = Math.abs(xValues[index - 1] - currentXvalue); + } + + const singleUnitInterval = getIntervalInMs(currentXvalue, esValue, esUnit, timeZone); + return Math.min(minInterval, singleUnitInterval, currentDiff); + }, Number.MAX_SAFE_INTEGER); + + return newInterval > 0 ? newInterval : moment.duration(esValue, esUnit).asMilliseconds(); +}; + +const partialDataText = i18n.translate('charts.partialData.bucketTooltipText', { + defaultMessage: + 'The selected time range does not include this entire bucket. It might contain partial data.', +}); + +const Prompt = () => ( + + + + + {partialDataText} + +); + +export const renderEndzoneTooltip = ( + xInterval?: number, + domainStart?: number, + domainEnd?: number, + formatter?: (v: any) => string, + renderValue = true +) => (headerData: TooltipValue): JSX.Element | string => { + const headerDataValue = headerData.value; + const formattedValue = formatter ? formatter(headerDataValue) : headerDataValue; + + if ( + (domainStart !== undefined && domainStart > headerDataValue) || + (domainEnd !== undefined && xInterval !== undefined && domainEnd - xInterval < headerDataValue) + ) { + return ( + <> + + {renderValue && ( + <> + +

{formattedValue}

+ + )} + + ); + } + + return renderValue ? formattedValue : null; +}; diff --git a/src/plugins/charts/public/static/components/index.ts b/src/plugins/charts/public/static/components/index.ts index 48c9e40145481..c044d361bed18 100644 --- a/src/plugins/charts/public/static/components/index.ts +++ b/src/plugins/charts/public/static/components/index.ts @@ -18,7 +18,7 @@ */ export { BasicOptions } from './basic_options'; -export { ColorModes, Rotates } from './collections'; +export { ColorMode, LabelRotation, defaultCountLabel } from './collections'; export { ColorRanges, SetColorRangeValue } from './color_ranges'; export { ColorSchemaOptions, SetColorSchemaOptionsValue } from './color_schema'; export { ColorSchemaParams, Labels, Style } from './types'; @@ -28,3 +28,7 @@ export { RequiredNumberInputOption } from './required_number_input'; export { SelectOption } from './select'; export { SwitchOption } from './switch'; export { TextInputOption } from './text_input'; +export { LegendToggle } from './legend_toggle'; +export { ColorPicker } from './color_picker'; +export { CurrentTime } from './current_time'; +export * from './endzones'; diff --git a/src/plugins/charts/public/static/components/legend_toggle.scss b/src/plugins/charts/public/static/components/legend_toggle.scss new file mode 100644 index 0000000000000..7eb85a5e08ec0 --- /dev/null +++ b/src/plugins/charts/public/static/components/legend_toggle.scss @@ -0,0 +1,20 @@ +.echLegend__toggle { + position: absolute; + bottom: 0; + left: 0; + z-index: 1; + margin: $euiSizeXS; + + &--isOpen { + background-color: $euiColorLightestShade; + } + + &--position-left, + &--position-bottom { + left: auto; + bottom: auto; + right: 0; + top: 0; + } +} + diff --git a/src/plugins/charts/public/static/components/legend_toggle.tsx b/src/plugins/charts/public/static/components/legend_toggle.tsx new file mode 100644 index 0000000000000..12742b6da6e6b --- /dev/null +++ b/src/plugins/charts/public/static/components/legend_toggle.tsx @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { memo, useMemo } from 'react'; +import classNames from 'classnames'; + +import { i18n } from '@kbn/i18n'; +import { htmlIdGenerator, EuiButtonIcon } from '@elastic/eui'; +import { Position } from '@elastic/charts'; + +import './legend_toggle.scss'; + +interface LegendToggleProps { + onClick: () => void; + showLegend: boolean; + legendPosition: Position; +} + +const LegendToggleComponent = ({ onClick, showLegend, legendPosition }: LegendToggleProps) => { + const legendId = useMemo(() => htmlIdGenerator()('legend'), []); + + return ( + + ); +}; + +export const LegendToggle = memo(LegendToggleComponent); diff --git a/src/plugins/charts/public/static/components/types.ts b/src/plugins/charts/public/static/components/types.ts index 196eb60b06aec..a4c384141dafb 100644 --- a/src/plugins/charts/public/static/components/types.ts +++ b/src/plugins/charts/public/static/components/types.ts @@ -18,7 +18,7 @@ */ import { ColorSchemas } from '../color_maps'; -import { Rotates } from './collections'; +import { LabelRotation } from './collections'; export interface ColorSchemaParams { colorSchema: ColorSchemas; @@ -29,8 +29,8 @@ export interface Labels { color?: string; filter?: boolean; overwriteColor?: boolean; - rotate?: Rotates; - show: boolean; + rotate?: LabelRotation; + show?: boolean; truncate?: number | null; } diff --git a/src/plugins/charts/public/static/index.ts b/src/plugins/charts/public/static/index.ts index b8a8406c375dd..638e119d8be49 100644 --- a/src/plugins/charts/public/static/index.ts +++ b/src/plugins/charts/public/static/index.ts @@ -20,3 +20,4 @@ export * from './color_maps'; export * from './colors'; export * from './components'; +export * from './utils'; diff --git a/src/plugins/inspector/common/adapters/data/index.ts b/src/plugins/charts/public/static/utils/index.ts similarity index 89% rename from src/plugins/inspector/common/adapters/data/index.ts rename to src/plugins/charts/public/static/utils/index.ts index a8b1abcd8cd7e..777deb326125e 100644 --- a/src/plugins/inspector/common/adapters/data/index.ts +++ b/src/plugins/charts/public/static/utils/index.ts @@ -17,6 +17,4 @@ * under the License. */ -export * from './data_adapter'; -export * from './formatted_data'; -export * from './types'; +export * from './transform_click_event'; diff --git a/src/plugins/charts/public/static/utils/transform_click_event.ts b/src/plugins/charts/public/static/utils/transform_click_event.ts new file mode 100644 index 0000000000000..21460eb51e3fb --- /dev/null +++ b/src/plugins/charts/public/static/utils/transform_click_event.ts @@ -0,0 +1,238 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + XYChartSeriesIdentifier, + GeometryValue, + XYBrushArea, + Accessor, + AccessorFn, + Datum, +} from '@elastic/charts'; + +import { RangeSelectContext, ValueClickContext } from '../../../../embeddable/public'; +import { Datatable } from '../../../../expressions/public'; + +export interface ClickTriggerEvent { + name: 'filterBucket'; + data: ValueClickContext['data']; +} + +export interface BrushTriggerEvent { + name: 'brush'; + data: RangeSelectContext['data']; +} + +type AllSeriesAccessors = Array<[accessor: Accessor | AccessorFn, value: string | number]>; + +/** + * returns accessor value from string or function accessor + * @param datum + * @param accessor + */ +function getAccessorValue(datum: Datum, accessor: Accessor | AccessorFn) { + if (typeof accessor === 'function') { + return accessor(datum); + } + + return datum[accessor]; +} + +/** + * This is a little unorthodox, but using functional accessors makes it + * difficult to match the correct column. This creates a test object to throw + * an error when the target id is accessed, thus matcing the target column. + */ +function validateAccessorId(id: string, accessor: Accessor | AccessorFn) { + if (typeof accessor !== 'function') { + return id === accessor; + } + + const matchedMessage = 'validateAccessorId matched'; + + try { + accessor({ + get [id]() { + throw new Error(matchedMessage); + }, + }); + return false; + } catch ({ message }) { + return message === matchedMessage; + } +} + +/** + * Groups split accessors by their accessor string or function and related value + * + * @param splitAccessors + * @param splitSeriesAccessorFnMap + */ +const getAllSplitAccessors = ( + splitAccessors: Map, + splitSeriesAccessorFnMap?: Map +): Array<[accessor: Accessor | AccessorFn, value: string | number]> => + [...splitAccessors.entries()].map(([key, value]) => [ + splitSeriesAccessorFnMap?.get?.(key) ?? key, + value, + ]); + +/** + * Reduces matching column indexes + * + * @param xAccessor + * @param yAccessor + * @param splitAccessors + */ +const columnReducer = ( + xAccessor: Accessor | AccessorFn | null, + yAccessor: Accessor | AccessorFn | null, + splitAccessors: AllSeriesAccessors +) => ( + acc: Array<[index: number, id: string]>, + { id }: Datatable['columns'][number], + index: number +): Array<[index: number, id: string]> => { + if ( + (xAccessor !== null && validateAccessorId(id, xAccessor)) || + (yAccessor !== null && validateAccessorId(id, yAccessor)) || + splitAccessors.some(([accessor]) => validateAccessorId(id, accessor)) + ) { + acc.push([index, id]); + } + + return acc; +}; + +/** + * Finds matching row index for given accessors and geometry values + * + * @param geometry + * @param xAccessor + * @param yAccessor + * @param splitAccessors + */ +const rowFindPredicate = ( + geometry: GeometryValue | null, + xAccessor: Accessor | AccessorFn | null, + yAccessor: Accessor | AccessorFn | null, + splitAccessors: AllSeriesAccessors +) => (row: Datatable['rows'][number]): boolean => + (geometry === null || + (xAccessor !== null && + getAccessorValue(row, xAccessor) === geometry.x && + yAccessor !== null && + getAccessorValue(row, yAccessor) === geometry.y)) && + [...splitAccessors].every(([accessor, value]) => getAccessorValue(row, accessor) === value); + +/** + * Helper function to transform `@elastic/charts` click event into filter action event + * + * @param table + * @param xAccessor + * @param splitSeriesAccessorFnMap needed when using `splitSeriesAccessors` as `AccessorFn` + * @param negate + */ +export const getFilterFromChartClickEventFn = ( + table: Datatable, + xAccessor: Accessor | AccessorFn, + splitSeriesAccessorFnMap?: Map, + negate: boolean = false +) => (points: Array<[GeometryValue, XYChartSeriesIdentifier]>): ClickTriggerEvent => { + const data: ValueClickContext['data']['data'] = []; + + points.forEach((point) => { + const [geometry, { yAccessor, splitAccessors }] = point; + const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap); + const columns = table.columns.reduce>( + columnReducer(xAccessor, yAccessor, allSplitAccessors), + [] + ); + const row = table.rows.findIndex( + rowFindPredicate(geometry, xAccessor, yAccessor, allSplitAccessors) + ); + const newData = columns.map(([column, id]) => ({ + table, + column, + row, + value: table.rows?.[row]?.[id] ?? null, + })); + + data.push(...newData); + }); + + return { + name: 'filterBucket', + data: { + negate, + data, + }, + }; +}; + +/** + * Helper function to get filter action event from series + */ +export const getFilterFromSeriesFn = (table: Datatable) => ( + { splitAccessors }: XYChartSeriesIdentifier, + splitSeriesAccessorFnMap?: Map, + negate = false +): ClickTriggerEvent => { + const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap); + const columns = table.columns.reduce>( + columnReducer(null, null, allSplitAccessors), + [] + ); + const row = table.rows.findIndex(rowFindPredicate(null, null, null, allSplitAccessors)); + const data: ValueClickContext['data']['data'] = columns.map(([column, id]) => ({ + table, + column, + row, + value: table.rows?.[row]?.[id] ?? null, + })); + + return { + name: 'filterBucket', + data: { + negate, + data, + }, + }; +}; + +/** + * Helper function to transform `@elastic/charts` brush event into brush action event + */ +export const getBrushFromChartBrushEventFn = ( + table: Datatable, + xAccessor: Accessor | AccessorFn +) => ({ x: selectedRange }: XYBrushArea): BrushTriggerEvent => { + const [start, end] = selectedRange ?? [0, 0]; + const range: [number, number] = [start, end]; + const column = table.columns.findIndex(({ id }) => validateAccessorId(id, xAccessor)); + + return { + data: { + table, + column, + range, + }, + name: 'brush', + }; +}; diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx index 4f78a738095d2..a31ecbea88bab 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -65,7 +65,7 @@ export class ExportCSVAction implements ActionByType { } private hasDatatableContent = (adapters: Adapters | undefined) => { - return Object.keys(adapters?.tables || {}).length > 0; + return Object.keys(adapters?.tables || {}).length > 0 && adapters!.tables.allowCsvExport; }; private getFormatter = (): FormatFactory | undefined => { @@ -76,7 +76,7 @@ export class ExportCSVAction implements ActionByType { private getDataTableContent = (adapters: Adapters | undefined) => { if (this.hasDatatableContent(adapters)) { - return adapters?.tables; + return adapters?.tables.tables; } return; }; diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index 4aa552893ab9b..9bede02c75b94 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -134,7 +134,7 @@ export const dashboardExportCsvAction = { }), getUntitledFilename: () => i18n.translate('dashboard.actions.downloadOptionsUnsavedFilename', { - defaultMessage: 'unsaved', + defaultMessage: 'untitled', }), }; diff --git a/src/plugins/data/common/es_query/kuery/index.ts b/src/plugins/data/common/es_query/kuery/index.ts index 4184dea62ef2c..5b6cfab030acb 100644 --- a/src/plugins/data/common/es_query/kuery/index.ts +++ b/src/plugins/data/common/es_query/kuery/index.ts @@ -18,7 +18,7 @@ */ export { KQLSyntaxError } from './kuery_syntax_error'; -export { nodeTypes } from './node_types'; +export { nodeTypes, nodeBuilder } from './node_types'; export * from './ast'; export * from './types'; diff --git a/src/plugins/data/common/es_query/kuery/node_types/index.ts b/src/plugins/data/common/es_query/kuery/node_types/index.ts index 22e73e791df9a..bec42f89c5b71 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/index.ts +++ b/src/plugins/data/common/es_query/kuery/node_types/index.ts @@ -24,6 +24,7 @@ import * as wildcard from './wildcard'; import { NodeTypes } from './types'; export { NodeTypes }; +export { nodeBuilder } from './node_builder'; export const nodeTypes: NodeTypes = { // This requires better typing of the different typings and their return types. diff --git a/src/plugins/data/common/es_query/kuery/node_types/node_builder.ts b/src/plugins/data/common/es_query/kuery/node_types/node_builder.ts new file mode 100644 index 0000000000000..4b77566dbc32b --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/node_builder.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { KueryNode, nodeTypes } from '../types'; + +export const nodeBuilder = { + is: (fieldName: string, value: string | KueryNode) => { + return nodeTypes.function.buildNodeWithArgumentNodes('is', [ + nodeTypes.literal.buildNode(fieldName), + typeof value === 'string' ? nodeTypes.literal.buildNode(value) : value, + nodeTypes.literal.buildNode(false), + ]); + }, + or: ([first, ...args]: KueryNode[]): KueryNode => { + return args.length ? nodeTypes.function.buildNode('or', [first, nodeBuilder.or(args)]) : first; + }, + and: ([first, ...args]: KueryNode[]): KueryNode => { + return args.length + ? nodeTypes.function.buildNode('and', [first, nodeBuilder.and(args)]) + : first; + }, +}; diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index 160860bcce591..eeaebecfe7f76 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -203,11 +203,12 @@ describe('Aggs service', () => { describe('start()', () => { test('exposes proper contract', () => { const start = service.start(startDeps); - expect(Object.keys(start).length).toBe(4); + expect(Object.keys(start).length).toBe(5); expect(start).toHaveProperty('calculateAutoTimeExpression'); expect(start).toHaveProperty('getDateMetaByDatatableColumn'); expect(start).toHaveProperty('createAggConfigs'); expect(start).toHaveProperty('types'); + expect(start).toHaveProperty('datatableUtilities'); }); test('types registry returns uninitialized type providers', () => { diff --git a/src/plugins/data/common/search/aggs/aggs_service.ts b/src/plugins/data/common/search/aggs/aggs_service.ts index b6afa708f9e6f..4b1756fabf1a7 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.ts @@ -18,7 +18,7 @@ */ import { ExpressionsServiceSetup } from 'src/plugins/expressions/common'; -import { IndexPattern, UI_SETTINGS } from '../../../common'; +import { CreateAggConfigParams, IndexPattern, UI_SETTINGS } from '../../../common'; import { GetConfigFn } from '../../types'; import { AggConfigs, @@ -29,6 +29,7 @@ import { } from './'; import { AggsCommonSetup, AggsCommonStart } from './types'; import { getDateMetaByDatatableColumn } from './utils/time_column_meta'; +import { getDatatableColumnUtilities } from './utils/datatable_column_meta'; /** @internal */ export const aggsRequiredUiSettings = [ @@ -88,6 +89,15 @@ export class AggsCommonService { const aggTypesStart = this.aggTypesRegistry.start(); const calculateAutoTimeExpression = getCalculateAutoTimeExpression(getConfig); + const createAggConfigs = ( + indexPattern: IndexPattern, + configStates?: CreateAggConfigParams[] + ) => { + return new AggConfigs(indexPattern, configStates, { + typesRegistry: aggTypesStart, + }); + }; + return { calculateAutoTimeExpression, getDateMetaByDatatableColumn: getDateMetaByDatatableColumn({ @@ -96,11 +106,12 @@ export class AggsCommonService { getConfig, isDefaultTimezone, }), - createAggConfigs: (indexPattern, configStates = [], schemas) => { - return new AggConfigs(indexPattern, configStates, { - typesRegistry: aggTypesStart, - }); - }, + datatableUtilities: getDatatableColumnUtilities({ + getIndexPattern, + createAggConfigs, + aggTypesStart, + }), + createAggConfigs, types: aggTypesStart, }; } diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index f3ae7d66dca96..1055777396f5f 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -94,6 +94,7 @@ import { CreateAggConfigParams, getCalculateAutoTimeExpression, METRIC_TYPES, + AggConfig, } from './'; export { IAggConfig, AggConfigSerialized } from './agg_config'; @@ -127,10 +128,14 @@ export interface AggsCommonStart { getDateMetaByDatatableColumn: ( column: DatatableColumn ) => Promise; + datatableUtilities: { + getIndexPattern: (column: DatatableColumn) => Promise; + getAggConfig: (column: DatatableColumn) => Promise; + isFilterable: (column: DatatableColumn) => boolean; + }; createAggConfigs: ( indexPattern: IndexPattern, - configStates?: CreateAggConfigParams[], - schemas?: Record + configStates?: CreateAggConfigParams[] ) => InstanceType; types: ReturnType; } diff --git a/src/plugins/data/common/search/aggs/utils/datatable_column_meta.ts b/src/plugins/data/common/search/aggs/utils/datatable_column_meta.ts new file mode 100644 index 0000000000000..38865f05727a9 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/datatable_column_meta.ts @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DatatableColumn } from 'src/plugins/expressions/common'; +import { IndexPattern } from '../../../index_patterns'; +import { AggConfigs, CreateAggConfigParams } from '../agg_configs'; +import { AggTypesRegistryStart } from '../agg_types_registry'; +import { IAggType } from '../agg_type'; + +export interface MetaByColumnDeps { + getIndexPattern: (id: string) => Promise; + createAggConfigs: ( + indexPattern: IndexPattern, + configStates?: CreateAggConfigParams[] + ) => InstanceType; + aggTypesStart: AggTypesRegistryStart; +} + +export const getDatatableColumnUtilities = (deps: MetaByColumnDeps) => { + const { getIndexPattern, createAggConfigs, aggTypesStart } = deps; + + const getIndexPatternFromDatatableColumn = async (column: DatatableColumn) => { + if (!column.meta.index) return; + + return await getIndexPattern(column.meta.index); + }; + + const getAggConfigFromDatatableColumn = async (column: DatatableColumn) => { + const indexPattern = await getIndexPatternFromDatatableColumn(column); + + if (!indexPattern) return; + + const aggConfigs = await createAggConfigs(indexPattern, [column.meta.sourceParams as any]); + return aggConfigs.aggs[0]; + }; + + const isFilterableAggDatatableColumn = (column: DatatableColumn) => { + if (column.meta.source !== 'esaggs') { + return false; + } + const aggType = (aggTypesStart.get(column.meta.sourceParams?.type as string) as any)( + {} + ) as IAggType; + return Boolean(aggType.createFilter); + }; + + return { + getIndexPattern: getIndexPatternFromDatatableColumn, + getAggConfig: getAggConfigFromDatatableColumn, + isFilterable: isFilterableAggDatatableColumn, + }; +}; diff --git a/src/plugins/data/common/search/expressions/esaggs/build_tabular_inspector_data.ts b/src/plugins/data/common/search/expressions/esaggs/build_tabular_inspector_data.ts deleted file mode 100644 index 2db3694884e2c..0000000000000 --- a/src/plugins/data/common/search/expressions/esaggs/build_tabular_inspector_data.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { set } from '@elastic/safer-lodash-set'; -import { - FormattedData, - TabularData, - TabularDataValue, -} from '../../../../../../plugins/inspector/common'; -import { Filter } from '../../../es_query'; -import { FormatFactory } from '../../../field_formats/utils'; -import { TabbedTable } from '../../tabify'; -import { createFilter } from './create_filter'; - -/** - * Type borrowed from the client-side FilterManager['addFilters']. - * - * We need to use a custom type to make this isomorphic since FilterManager - * doesn't exist on the server. - * - * @internal - */ -export type AddFilters = (filters: Filter[] | Filter, pinFilterStatus?: boolean) => void; - -/** - * This function builds tabular data from the response and attaches it to the - * inspector. It will only be called when the data view in the inspector is opened. - * - * @internal - */ -export async function buildTabularInspectorData( - table: TabbedTable, - { - addFilters, - deserializeFieldFormat, - }: { - addFilters?: AddFilters; - deserializeFieldFormat: FormatFactory; - } -): Promise { - const aggConfigs = table.columns.map((column) => column.aggConfig); - const rows = table.rows.map((row) => { - return table.columns.reduce>((prev, cur, colIndex) => { - const value = row[cur.id]; - - let format = cur.aggConfig.toSerializedFieldFormat(); - if (Object.keys(format).length < 1) { - // If no format exists, fall back to string as a default - format = { id: 'string' }; - } - const fieldFormatter = deserializeFieldFormat(format); - - prev[`col-${colIndex}-${cur.aggConfig.id}`] = new FormattedData( - value, - fieldFormatter.convert(value) - ); - return prev; - }, {}); - }); - - const columns = table.columns.map((col, colIndex) => { - const field = col.aggConfig.getField(); - const isCellContentFilterable = col.aggConfig.isFilterable() && (!field || field.filterable); - return { - name: col.name, - field: `col-${colIndex}-${col.aggConfig.id}`, - filter: - addFilters && - isCellContentFilterable && - ((value: TabularDataValue) => { - const rowIndex = rows.findIndex( - (row) => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw - ); - const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw); - - if (filter) { - addFilters(filter); - } - }), - filterOut: - addFilters && - isCellContentFilterable && - ((value: TabularDataValue) => { - const rowIndex = rows.findIndex( - (row) => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw - ); - const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw); - - if (filter) { - const notOther = value.raw !== '__other__'; - const notMissing = value.raw !== '__missing__'; - if (Array.isArray(filter)) { - filter.forEach((f) => set(f, 'meta.negate', notOther && notMissing)); - } else { - set(filter, 'meta.negate', notOther && notMissing); - } - addFilters(filter); - } - }), - }; - }); - - return { columns, rows }; -} diff --git a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts index 6c53a8a09274a..2274fcfd6b8d5 100644 --- a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts +++ b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts @@ -34,7 +34,6 @@ import { AggsStart, AggExpressionType } from '../../aggs'; import { ISearchStartSearchSource } from '../../search_source'; import { KibanaContext } from '../kibana_context_type'; -import { AddFilters } from './build_tabular_inspector_data'; import { handleRequest, RequestHandlerParams } from './request_handler'; const name = 'esaggs'; @@ -59,7 +58,6 @@ export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition< /** @internal */ export interface EsaggsStartDependencies { - addFilters?: AddFilters; aggs: AggsStart; deserializeFieldFormat: FormatFactory; indexPatterns: IndexPatternsContract; diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts index aba498f720ec1..78d169e8529c5 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts @@ -39,7 +39,6 @@ describe('esaggs expression function - public', () => { jest.clearAllMocks(); mockParams = { abortSignal: (jest.fn() as unknown) as jest.Mocked, - addFilters: jest.fn(), aggs: ({ aggs: [{ type: { name: 'terms', postFlightRequest: jest.fn().mockResolvedValue({}) } }], setTimeRange: jest.fn(), diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts index 3c1745409ebcd..e4385526ee6e8 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts @@ -36,13 +36,9 @@ import { ISearchStartSearchSource } from '../../search_source'; import { tabifyAggResponse } from '../../tabify'; import { getRequestInspectorStats, getResponseInspectorStats } from '../utils'; -import type { AddFilters } from './build_tabular_inspector_data'; -import { buildTabularInspectorData } from './build_tabular_inspector_data'; - /** @internal */ export interface RequestHandlerParams { abortSignal?: AbortSignal; - addFilters?: AddFilters; aggs: IAggConfigs; deserializeFieldFormat: FormatFactory; filters?: Filter[]; @@ -59,7 +55,6 @@ export interface RequestHandlerParams { export const handleRequest = async ({ abortSignal, - addFilters, aggs, deserializeFieldFormat, filters, @@ -199,16 +194,5 @@ export const handleRequest = async ({ const tabifiedResponse = tabifyAggResponse(aggs, response, tabifyParams); - if (inspectorAdapters.data) { - inspectorAdapters.data.setTabularLoader( - () => - buildTabularInspectorData(tabifiedResponse, { - addFilters, - deserializeFieldFormat, - }), - { returnsFormattedValues: true } - ); - } - return tabifiedResponse; }; diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index 06b083e0ff3aa..a09ab12f0c6f0 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -7,7 +7,8 @@ "bfetch", "expressions", "uiActions", - "share" + "share", + "inspector" ], "optionalPlugins": ["usageCollection"], "extraPublicDirs": ["common"], diff --git a/src/plugins/data/public/index.scss b/src/plugins/data/public/index.scss index a51fde079f10b..467efa98934ec 100644 --- a/src/plugins/data/public/index.scss +++ b/src/plugins/data/public/index.scss @@ -1 +1,2 @@ @import './ui/index'; +@import './utils/table_inspector_view/index'; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 458024151c585..eb3a053b78a2d 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -70,6 +70,7 @@ import { import { SavedObjectsClientPublicToCommon } from './index_patterns'; import { getIndexPatternLoad } from './index_patterns/expressions'; import { UsageCollectionSetup } from '../../usage_collection/public'; +import { getTableViewDescription } from './utils/table_inspector_view'; declare module '../../ui_actions/public' { export interface ActionContextMapping { @@ -104,7 +105,7 @@ export class DataPublicPlugin public setup( core: CoreSetup, - { bfetch, expressions, uiActions, usageCollection }: DataSetupDependencies + { bfetch, expressions, uiActions, usageCollection, inspector }: DataSetupDependencies ): DataPublicPluginSetup { const startServices = createStartServicesGetter(core.getStartServices); @@ -141,6 +142,15 @@ export class DataPublicPlugin expressions, }); + inspector.registerView( + getTableViewDescription(() => ({ + uiActions: startServices().plugins.uiActions, + uiSettings: startServices().core.uiSettings, + fieldFormats: startServices().self.fieldFormats, + isFilterable: startServices().self.search.aggs.datatableUtilities.isFilterable, + })) + ); + return { autocomplete: this.autocomplete.setup(core, { timefilter: queryService.timefilter }), search: searchService, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 373aa4dee53fd..e5df6d860b404 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -20,6 +20,7 @@ import { CoreSetup } from 'src/core/public'; import { CoreSetup as CoreSetup_2 } from 'kibana/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; +import * as CSS from 'csstype'; import { Datatable as Datatable_2 } from 'src/plugins/expressions'; import { Datatable as Datatable_3 } from 'src/plugins/expressions/common'; import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions'; @@ -66,11 +67,12 @@ import { Plugin as Plugin_2 } 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'; +import * as PropTypes from 'prop-types'; import { PublicContract } from '@kbn/utility-types'; import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams } from 'src/core/server/types'; import React from 'react'; -import * as React_2 from 'react'; +import * as React_3 from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Reporter } from '@kbn/analytics'; import { RequestAdapter } from 'src/plugins/inspector/common'; @@ -1888,7 +1890,7 @@ export class Plugin implements Plugin_2); // (undocumented) - setup(core: CoreSetup, { bfetch, expressions, uiActions, usageCollection }: DataSetupDependencies): DataPublicPluginSetup; + setup(core: CoreSetup, { bfetch, expressions, uiActions, usageCollection, inspector }: DataSetupDependencies): DataPublicPluginSetup; // (undocumented) start(core: CoreStart_2, { uiActions }: DataStartDependencies): DataPublicPluginStart; // (undocumented) @@ -2560,7 +2562,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:128:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/search/aggs/types.ts:145:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/search/aggs/types.ts:150:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/search_source/search_source.ts:197:7 - (ae-forgotten-export) The symbol "SearchFieldValue" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index de747d234b441..bc4992384b0c2 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -88,11 +88,12 @@ describe('AggsService - public', () => { describe('start()', () => { test('exposes proper contract', () => { const start = service.start(startDeps); - expect(Object.keys(start).length).toBe(4); + expect(Object.keys(start).length).toBe(5); expect(start).toHaveProperty('calculateAutoTimeExpression'); expect(start).toHaveProperty('getDateMetaByDatatableColumn'); expect(start).toHaveProperty('createAggConfigs'); expect(start).toHaveProperty('types'); + expect(start).toHaveProperty('datatableUtilities'); }); test('types registry returns initialized agg types', () => { diff --git a/src/plugins/data/public/search/aggs/aggs_service.ts b/src/plugins/data/public/search/aggs/aggs_service.ts index 85e0f604bb8b5..7b5edac0280d9 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.ts @@ -102,6 +102,7 @@ export class AggsService { const { calculateAutoTimeExpression, getDateMetaByDatatableColumn, + datatableUtilities, types, } = this.aggsCommonService.start({ getConfig: this.getConfig!, @@ -148,7 +149,8 @@ export class AggsService { return { calculateAutoTimeExpression, getDateMetaByDatatableColumn, - createAggConfigs: (indexPattern, configStates = [], schemas) => { + datatableUtilities, + createAggConfigs: (indexPattern, configStates = []) => { return new AggConfigs(indexPattern, configStates, { typesRegistry }); }, types: typesRegistry, diff --git a/src/plugins/data/public/search/aggs/mocks.ts b/src/plugins/data/public/search/aggs/mocks.ts index abc930f00b594..bc02b48d67f7b 100644 --- a/src/plugins/data/public/search/aggs/mocks.ts +++ b/src/plugins/data/public/search/aggs/mocks.ts @@ -68,6 +68,11 @@ export const searchAggsSetupMock = (): AggsSetup => ({ export const searchAggsStartMock = (): AggsStart => ({ calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), getDateMetaByDatatableColumn: jest.fn(), + datatableUtilities: { + isFilterable: jest.fn(), + getAggConfig: jest.fn(), + getIndexPattern: jest.fn(), + }, createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: mockAggTypesRegistry(), diff --git a/src/plugins/data/public/search/expressions/esaggs.test.ts b/src/plugins/data/public/search/expressions/esaggs.test.ts index 10ed22c861188..abb95ed05b12e 100644 --- a/src/plugins/data/public/search/expressions/esaggs.test.ts +++ b/src/plugins/data/public/search/expressions/esaggs.test.ts @@ -72,7 +72,6 @@ describe('esaggs expression function - public', () => { types: {}, }; startDependencies = { - addFilters: jest.fn(), aggs: ({ createAggConfigs: jest.fn().mockReturnValue({ foo: 'bar' }), } as unknown) as jest.Mocked, @@ -113,7 +112,6 @@ describe('esaggs expression function - public', () => { expect(handleEsaggsRequest).toHaveBeenCalledWith(null, args, { abortSignal: mockHandlers.abortSignal, - addFilters: startDependencies.addFilters, aggs: { foo: 'bar' }, deserializeFieldFormat: startDependencies.deserializeFieldFormat, filters: undefined, diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 4a078bf9b2e55..d8d90ea464a73 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -49,7 +49,6 @@ export function getFunctionDefinition({ ...getEsaggsMeta(), async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) { const { - addFilters, aggs, deserializeFieldFormat, indexPatterns, @@ -64,7 +63,6 @@ export function getFunctionDefinition({ return await handleEsaggsRequest(input, args, { abortSignal: (abortSignal as unknown) as AbortSignal, - addFilters, aggs: aggConfigs, deserializeFieldFormat, filters: get(input, 'filters', undefined), @@ -104,9 +102,8 @@ export function getEsaggs({ return getFunctionDefinition({ getStartDependencies: async () => { const [, , self] = await getStartServices(); - const { fieldFormats, indexPatterns, query, search } = self; + const { fieldFormats, indexPatterns, search } = self; return { - addFilters: query.filterManager.addFilters.bind(query.filterManager), aggs: search.aggs, deserializeFieldFormat: fieldFormats.deserialize.bind(fieldFormats), indexPatterns, diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 4082fbe55094c..c7b66acfc6c7a 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -31,6 +31,7 @@ import { QuerySetup, QueryStart } from './query'; import { IndexPatternsContract } from './index_patterns'; import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui'; import { UsageCollectionSetup } from '../../usage_collection/public'; +import { Setup as InspectorSetup } from '../../inspector/public'; export interface DataPublicPluginEnhancements { search: SearchEnhancements; @@ -40,6 +41,7 @@ export interface DataSetupDependencies { bfetch: BfetchPublicSetup; expressions: ExpressionsSetup; uiActions: UiActionsSetup; + inspector: InspectorSetup; usageCollection?: UsageCollectionSetup; } diff --git a/src/plugins/inspector/public/views/data/_data_table.scss b/src/plugins/data/public/utils/table_inspector_view/_data_table.scss similarity index 100% rename from src/plugins/inspector/public/views/data/_data_table.scss rename to src/plugins/data/public/utils/table_inspector_view/_data_table.scss diff --git a/src/plugins/inspector/public/views/data/_index.scss b/src/plugins/data/public/utils/table_inspector_view/_index.scss similarity index 100% rename from src/plugins/inspector/public/views/data/_index.scss rename to src/plugins/data/public/utils/table_inspector_view/_index.scss diff --git a/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap similarity index 68% rename from src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap rename to src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap index ec68b307734e3..4320fc186783b 100644 --- a/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap @@ -1,17 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Inspector Data View component should render empty state 1`] = ` -

@@ -272,7 +159,7 @@ exports[`Inspector Data View component should render empty state 1`] = `

@@ -295,7 +182,7 @@ exports[`Inspector Data View component should render empty state 1`] = ` > No data available @@ -316,7 +203,7 @@ exports[`Inspector Data View component should render empty state 1`] = `

The element did not provide any data. @@ -329,7 +216,7 @@ exports[`Inspector Data View component should render empty state 1`] = ` - + `; exports[`Inspector Data View component should render loading state 1`] = ` @@ -442,6 +329,20 @@ exports[`Inspector Data View component should render loading state 1`] = ` } } > +

loading
diff --git a/src/plugins/inspector/public/views/data/components/data_table.tsx b/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx similarity index 57% rename from src/plugins/inspector/public/views/data/components/data_table.tsx rename to src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx index 69be069272f79..f4d1a8988da78 100644 --- a/src/plugins/inspector/public/views/data/components/data_table.tsx +++ b/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx @@ -35,8 +35,10 @@ import { i18n } from '@kbn/i18n'; import { DataDownloadOptions } from './download_options'; import { DataViewRow, DataViewColumn } from '../types'; -import { TabularData } from '../../../../common/adapters/data/types'; import { IUiSettingsClient } from '../../../../../../core/public'; +import { Datatable, DatatableColumn } from '../../../../../expressions/public'; +import { FieldFormatsStart } from '../../../field_formats'; +import { UiActionsStart } from '../../../../../ui_actions/public'; interface DataTableFormatState { columns: DataViewColumn[]; @@ -44,10 +46,21 @@ interface DataTableFormatState { } interface DataTableFormatProps { - data: TabularData; + data: Datatable; exportTitle: string; uiSettings: IUiSettingsClient; - isFormatted?: boolean; + fieldFormats: FieldFormatsStart; + uiActions: UiActionsStart; + isFilterable: (column: DatatableColumn) => boolean; +} + +interface RenderCellArguments { + table: Datatable; + columnIndex: number; + rowIndex: number; + formattedValue: string; + uiActions: UiActionsStart; + isFilterable: boolean; } export class DataTableFormat extends Component { @@ -55,25 +68,35 @@ export class DataTableFormat extends Component - {isFormatted ? value.formatted : value} + {formattedValue} - {dataColumn.filter && ( + {isFilterable && ( } @@ -81,23 +104,29 @@ export class DataTableFormat extends Component dataColumn.filter(value)} + onClick={() => { + const value = table.rows[rowIndex][column.id]; + const eventData = { table, column: columnIndex, row: rowIndex, value }; + uiActions.executeTriggerActions('VALUE_CLICK_TRIGGER', { + data: { data: [eventData] }, + }); + }} /> )} - {dataColumn.filterOut && ( + {isFilterable && ( } @@ -105,12 +134,21 @@ export class DataTableFormat extends Component dataColumn.filterOut(value)} + onClick={() => { + const value = table.rows[rowIndex][column.id]; + const eventData = { table, column: columnIndex, row: rowIndex, value }; + uiActions.executeTriggerActions('VALUE_CLICK_TRIGGER', { + data: { data: [eventData], negate: true }, + }); + }} /> @@ -121,7 +159,12 @@ export class DataTableFormat extends Component ({ - name: dataColumn.name, - field: dataColumn.field, - sortable: isFormatted ? (row: DataViewRow) => row[dataColumn.field].raw : true, - render: (value: any) => DataTableFormat.renderCell(dataColumn, value, isFormatted), - })); + const columns = data.columns.map((dataColumn: any, index: number) => { + const formatParams = { id: 'string', ...dataColumn.meta.params }; + const fieldFormatter = fieldFormats.deserialize(formatParams); + const filterable = isFilterable(dataColumn); + return { + originalColumn: () => dataColumn, + name: dataColumn.name, + field: dataColumn.id, + sortable: true, + render: (value: any) => { + const formattedValue = fieldFormatter.convert(value); + const rowIndex = data.rows.findIndex((row) => row[dataColumn.id] === value) || 0; + + return DataTableFormat.renderCell({ + table: data, + columnIndex: index, + rowIndex, + formattedValue, + uiActions, + isFilterable: filterable, + }); + }, + }; + }); return { columns, rows: data.rows }; } @@ -152,12 +213,12 @@ export class DataTableFormat extends Component diff --git a/src/plugins/inspector/public/views/data/components/data_view.test.tsx b/src/plugins/data/public/utils/table_inspector_view/components/data_view.test.tsx similarity index 77% rename from src/plugins/inspector/public/views/data/components/data_view.test.tsx rename to src/plugins/data/public/utils/table_inspector_view/components/data_view.test.tsx index 82bec5ee3fe8c..975a91548d799 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.test.tsx +++ b/src/plugins/data/public/utils/table_inspector_view/components/data_view.test.tsx @@ -18,11 +18,11 @@ */ import React, { Suspense } from 'react'; -import { getDataViewDescription } from '../index'; -import { DataAdapter } from '../../../../common/adapters/data'; +import { getTableViewDescription } from '../index'; import { mountWithIntl } from '@kbn/test/jest'; +import { TablesAdapter } from '../../../../../expressions/common'; -jest.mock('../lib/export_csv', () => ({ +jest.mock('./export_csv', () => ({ exportAsCsv: jest.fn(), })); @@ -30,13 +30,18 @@ describe('Inspector Data View', () => { let DataView: any; beforeEach(() => { - DataView = getDataViewDescription(); + DataView = getTableViewDescription(() => ({ + uiActions: {} as any, + uiSettings: {} as any, + fieldFormats: {} as any, + isFilterable: jest.fn(), + })); }); it('should only show if data adapter is present', () => { - const adapter = new DataAdapter(); + const adapter = new TablesAdapter(); - expect(DataView.shouldShow({ data: adapter })).toBe(true); + expect(DataView.shouldShow({ tables: adapter })).toBe(true); expect(DataView.shouldShow({})).toBe(false); }); @@ -44,7 +49,7 @@ describe('Inspector Data View', () => { let adapters: any; beforeEach(() => { - adapters = { data: new DataAdapter() }; + adapters = { tables: new TablesAdapter() }; }); it('should render loading state', () => { @@ -60,9 +65,7 @@ describe('Inspector Data View', () => { it('should render empty state', async () => { const component = mountWithIntl(); // eslint-disable-line react/jsx-pascal-case - const tabularLoader = Promise.resolve(null); - adapters.data.setTabularLoader(() => tabularLoader); - await tabularLoader; + adapters.tables.logDatatable({ columns: [{ id: '1' }], rows: [{ '1': 123 }] }); // After the loader has resolved we'll still need one update, to "flush" the state changes component.update(); expect(component).toMatchSnapshot(); diff --git a/src/plugins/data/public/utils/table_inspector_view/components/data_view.tsx b/src/plugins/data/public/utils/table_inspector_view/components/data_view.tsx new file mode 100644 index 0000000000000..97dca45d742c9 --- /dev/null +++ b/src/plugins/data/public/utils/table_inspector_view/components/data_view.tsx @@ -0,0 +1,137 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiEmptyPrompt } from '@elastic/eui'; + +import { DataTableFormat } from './data_table'; +import { IUiSettingsClient } from '../../../../../../core/public'; +import { InspectorViewProps, Adapters } from '../../../../../inspector/public'; +import { UiActionsStart } from '../../../../../ui_actions/public'; +import { FieldFormatsStart } from '../../../field_formats'; +import { TablesAdapter, Datatable, DatatableColumn } from '../../../../../expressions/public'; + +interface DataViewComponentState { + datatable: Datatable; + adapters: Adapters; +} + +interface DataViewComponentProps extends InspectorViewProps { + uiSettings: IUiSettingsClient; + uiActions: UiActionsStart; + fieldFormats: FieldFormatsStart; + isFilterable: (column: DatatableColumn) => boolean; +} + +class DataViewComponent extends Component { + static propTypes = { + adapters: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + uiSettings: PropTypes.object, + uiActions: PropTypes.object.isRequired, + fieldFormats: PropTypes.object.isRequired, + isFilterable: PropTypes.func.isRequired, + }; + + state = {} as DataViewComponentState; + + static getDerivedStateFromProps( + nextProps: Readonly, + state: DataViewComponentState + ) { + if (state && nextProps.adapters === state.adapters) { + return null; + } + + const { tables } = nextProps.adapters.tables; + const keys = Object.keys(tables); + const datatable = keys.length ? tables[keys[0]] : undefined; + + return { + adapters: nextProps.adapters, + datatable, + }; + } + + onUpdateData = (tables: TablesAdapter['tables']) => { + const keys = Object.keys(tables); + const datatable = keys.length ? tables[keys[0]] : undefined; + + if (datatable) { + this.setState({ + datatable, + }); + } + }; + + componentDidMount() { + this.props.adapters.tables!.on('change', this.onUpdateData); + } + + componentWillUnmount() { + this.props.adapters.tables!.removeListener('change', this.onUpdateData); + } + + static renderNoData() { + return ( + + + + } + body={ + +

+ +

+
+ } + /> + ); + } + + render() { + if (!this.state.datatable) { + return DataViewComponent.renderNoData(); + } + + return ( + + ); + } +} + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export default DataViewComponent; diff --git a/src/plugins/data/public/utils/table_inspector_view/components/data_view_wrapper.tsx b/src/plugins/data/public/utils/table_inspector_view/components/data_view_wrapper.tsx new file mode 100644 index 0000000000000..d8b96da36628c --- /dev/null +++ b/src/plugins/data/public/utils/table_inspector_view/components/data_view_wrapper.tsx @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { lazy } from 'react'; +import { IUiSettingsClient } from 'kibana/public'; +import { UiActionsStart } from '../../../../../ui_actions/public'; +import { FieldFormatsStart } from '../../../field_formats'; +import { DatatableColumn } from '../../../../../expressions/common/expression_types/specs'; + +const DataViewComponent = lazy(() => import('./data_view')); + +export const getDataViewComponentWrapper = ( + getStartServices: () => { + uiActions: UiActionsStart; + fieldFormats: FieldFormatsStart; + uiSettings: IUiSettingsClient; + isFilterable: (column: DatatableColumn) => boolean; + } +) => { + return (props: any) => { + return ( + + ); + }; +}; diff --git a/src/plugins/inspector/public/views/data/components/download_options.tsx b/src/plugins/data/public/utils/table_inspector_view/components/download_options.tsx similarity index 77% rename from src/plugins/inspector/public/views/data/components/download_options.tsx rename to src/plugins/data/public/utils/table_inspector_view/components/download_options.tsx index cedb723091638..f849f598e9c69 100644 --- a/src/plugins/inspector/public/views/data/components/download_options.tsx +++ b/src/plugins/data/public/utils/table_inspector_view/components/download_options.tsx @@ -24,8 +24,8 @@ import { i18n } from '@kbn/i18n'; import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { DataViewColumn, DataViewRow } from '../types'; - -import { exportAsCsv } from '../lib/export_csv'; +import { exportAsCsv } from './export_csv'; +import { FieldFormatsStart } from '../../../field_formats'; interface DataDownloadOptionsState { isPopoverOpen: boolean; @@ -38,6 +38,7 @@ interface DataDownloadOptionsProps { csvSeparator: string; quoteValues: boolean; isFormatted?: boolean; + fieldFormats: FieldFormatsStart; } class DataDownloadOptions extends Component { @@ -45,9 +46,9 @@ class DataDownloadOptions extends Component { + exportCsv = (isFormatted: boolean = true) => { let filename = this.props.title; if (!filename || filename.length === 0) { - filename = i18n.translate('inspector.data.downloadOptionsUnsavedFilename', { + filename = i18n.translate('data.inspector.table.downloadOptionsUnsavedFilename', { defaultMessage: 'unsaved', }); } @@ -79,38 +80,24 @@ class DataDownloadOptions extends Component { - this.exportCsv({ - valueFormatter: (item: any) => item.formatted, - }); + this.exportCsv(true); }; exportFormattedAsRawCsv = () => { - this.exportCsv({ - valueFormatter: (item: any) => item.raw, - }); + this.exportCsv(false); }; - renderUnformattedDownload() { - return ( - - - - ); - } - renderFormattedDownloads() { const button = ( @@ -121,14 +108,14 @@ class DataDownloadOptions extends Component } toolTipPosition="left" > , @@ -137,13 +124,13 @@ class DataDownloadOptions extends Component } toolTipPosition="left" > - + , ]; @@ -162,9 +149,7 @@ class DataDownloadOptions extends Component escape(col.name, quoteValues)); + const formatters = columns.map((column) => { + return fieldFormats.deserialize(column.originalColumn().meta.params); + }); + // Convert the array of row objects to an array of row arrays - const orderedFieldNames = columns.map((col) => col.field); const csvRows = rows.map((row) => { - return orderedFieldNames.map((field) => - escape(valueFormatter ? valueFormatter(row[field]) : row[field], quoteValues) - ); + return columns.map((column, i) => { + return escape( + isFormatted ? formatters[i].convert(row[column.field]) : row[column.field], + quoteValues + ); + }); }); return ( @@ -69,14 +77,18 @@ export function exportAsCsv({ filename, columns, rows, - valueFormatter, + isFormatted, csvSeparator, quoteValues, + fieldFormats, }: any) { const type = 'text/plain;charset=utf-8'; - const csv = new Blob([buildCsv(columns, rows, csvSeparator, quoteValues, valueFormatter)], { - type, - }); + const csv = new Blob( + [buildCsv(columns, rows, csvSeparator, quoteValues, isFormatted, fieldFormats)], + { + type, + } + ); saveAs(csv, filename); } diff --git a/src/plugins/data/public/utils/table_inspector_view/index.ts b/src/plugins/data/public/utils/table_inspector_view/index.ts new file mode 100644 index 0000000000000..3769298af05f3 --- /dev/null +++ b/src/plugins/data/public/utils/table_inspector_view/index.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { i18n } from '@kbn/i18n'; +import { IUiSettingsClient } from 'kibana/public'; +import { Adapters, InspectorViewDescription } from '../../../../inspector/public'; +import { getDataViewComponentWrapper } from './components/data_view_wrapper'; +import { UiActionsStart } from '../../../../ui_actions/public'; +import { FieldFormatsStart } from '../../field_formats'; +import { DatatableColumn } from '../../../../expressions/common/expression_types/specs'; + +export const getTableViewDescription = ( + getStartServices: () => { + uiActions: UiActionsStart; + fieldFormats: FieldFormatsStart; + isFilterable: (column: DatatableColumn) => boolean; + uiSettings: IUiSettingsClient; + } +): InspectorViewDescription => ({ + title: i18n.translate('data.inspector.table.dataTitle', { + defaultMessage: 'Data', + }), + order: 10, + help: i18n.translate('data.inspector.table..dataDescriptionTooltip', { + defaultMessage: 'View the data behind the visualization', + }), + shouldShow(adapters: Adapters) { + return Boolean(adapters.tables); + }, + component: getDataViewComponentWrapper(getStartServices), +}); diff --git a/src/plugins/inspector/public/views/data/types.ts b/src/plugins/data/public/utils/table_inspector_view/types.ts similarity index 70% rename from src/plugins/inspector/public/views/data/types.ts rename to src/plugins/data/public/utils/table_inspector_view/types.ts index 31de9eb3a152e..dc85c3c2e3135 100644 --- a/src/plugins/inspector/public/views/data/types.ts +++ b/src/plugins/data/public/utils/table_inspector_view/types.ts @@ -17,15 +17,20 @@ * under the License. */ -import { TabularDataRow } from '../../../common/adapters'; +import { Datatable, DatatableColumn, DatatableRow } from '../../../../expressions/common'; -type DataViewColumnRender = (value: string, _item: TabularDataRow) => string; +type DataViewColumnRender = (value: string, _item: DatatableRow) => string; export interface DataViewColumn { + originalColumn: () => DatatableColumn; name: string; field: string; - sortable: (item: TabularDataRow) => string | number; + sortable: (item: DatatableRow) => string | number; render: DataViewColumnRender; } -export type DataViewRow = TabularDataRow; +export type DataViewRow = DatatableRow; + +export interface TableInspectorAdapter { + [key: string]: Datatable; +} diff --git a/src/plugins/data/server/search/aggs/aggs_service.ts b/src/plugins/data/server/search/aggs/aggs_service.ts index c23f748b1eeb5..ae1cf3054ec3f 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.ts @@ -86,6 +86,7 @@ export class AggsService { const { calculateAutoTimeExpression, getDateMetaByDatatableColumn, + datatableUtilities, types, } = this.aggsCommonService.start({ getConfig, @@ -130,7 +131,8 @@ export class AggsService { return { calculateAutoTimeExpression, getDateMetaByDatatableColumn, - createAggConfigs: (indexPattern, configStates = [], schemas) => { + datatableUtilities, + createAggConfigs: (indexPattern, configStates = []) => { return new AggConfigs(indexPattern, configStates, { typesRegistry }); }, types: typesRegistry, diff --git a/src/plugins/data/server/search/aggs/mocks.ts b/src/plugins/data/server/search/aggs/mocks.ts index 7b7f3d3c40652..66a6aa2c7d803 100644 --- a/src/plugins/data/server/search/aggs/mocks.ts +++ b/src/plugins/data/server/search/aggs/mocks.ts @@ -70,6 +70,11 @@ export const searchAggsSetupMock = (): AggsSetup => ({ const commonStartMock = (): AggsCommonStart => ({ calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), getDateMetaByDatatableColumn: jest.fn(), + datatableUtilities: { + getIndexPattern: jest.fn(), + getAggConfig: jest.fn(), + isFilterable: jest.fn(), + }, createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: mockAggTypesRegistry(), diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 620df9c8edcb0..983e4a44370d9 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -25,6 +25,7 @@ import type { SearchUsage } from '../collectors'; import { getDefaultSearchParams, getShardTimeout, shimAbortSignal } from './request_utils'; import { toKibanaSearchResponse } from './response_utils'; import { searchUsageObserver } from '../collectors/usage'; +import { KbnServerError } from '../../../../kibana_utils/server'; export const esSearchStrategyProvider = ( config$: Observable, @@ -35,7 +36,7 @@ export const esSearchStrategyProvider = ( // Only default index pattern type is supported here. // See data_enhanced for other type support. if (request.indexType) { - throw new Error(`Unsupported index pattern type ${request.indexType}`); + throw new KbnServerError(`Unsupported index pattern type ${request.indexType}`, 400); } const search = async () => { diff --git a/src/plugins/data/server/search/routes/msearch.ts b/src/plugins/data/server/search/routes/msearch.ts index 7b44aa18bf8fc..ae6362fac1b21 100644 --- a/src/plugins/data/server/search/routes/msearch.ts +++ b/src/plugins/data/server/search/routes/msearch.ts @@ -23,6 +23,7 @@ import { IRouter } from 'src/core/server'; import { SearchRouteDependencies } from '../search_service'; import { getCallMsearch } from './call_msearch'; +import { reportServerError } from '../../../../kibana_utils/server'; /** * The msearch route takes in an array of searches, each consisting of header @@ -69,15 +70,7 @@ export function registerMsearchRoute(router: IRouter, deps: SearchRouteDependenc const response = await callMsearch({ body: request.body }); return res.ok(response); } catch (err) { - return res.customError({ - statusCode: err.statusCode || 500, - body: { - message: err.message, - attributes: { - error: err.body?.error || err.message, - }, - }, - }); + return reportServerError(res, err); } } ); diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts index ed519164c8e43..6c27b7e2c43d3 100644 --- a/src/plugins/data/server/search/routes/search.ts +++ b/src/plugins/data/server/search/routes/search.ts @@ -22,6 +22,7 @@ import { schema } from '@kbn/config-schema'; import type { IRouter } from 'src/core/server'; import { getRequestAbortedSignal } from '../../lib'; import { shimHitsTotal } from './shim_hits_total'; +import { reportServerError } from '../../../../kibana_utils/server'; export function registerSearchRoute(router: IRouter): void { router.post( @@ -74,15 +75,7 @@ export function registerSearchRoute(router: IRouter): void { }, }); } catch (err) { - return res.customError({ - statusCode: err.statusCode || 500, - body: { - message: err.message, - attributes: { - error: err.body?.error || err.message, - }, - }, - }); + return reportServerError(res, err); } } ); @@ -106,15 +99,7 @@ export function registerSearchRoute(router: IRouter): void { await context.search!.cancel(id, { strategy }); return res.ok(); } catch (err) { - return res.customError({ - statusCode: err.statusCode, - body: { - message: err.message, - attributes: { - error: err.body.error, - }, - }, - }); + return reportServerError(res, err); } } ); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 6c8683220bc4c..5de019cd1b83e 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -73,6 +73,7 @@ import { import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { ConfigSchema } from '../../config'; import { SessionService, IScopedSessionService, ISessionService } from './session'; +import { KbnServerError } from '../../../kibana_utils/server'; declare module 'src/core/server' { interface RequestHandlerContext { @@ -305,7 +306,13 @@ export class SearchService implements Plugin { private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => { const strategy = this.getSearchStrategy(options.strategy); - return strategy.cancel ? strategy.cancel(id, options, deps) : Promise.resolve(); + if (!strategy.cancel) { + throw new KbnServerError( + `Search strategy ${options.strategy} doesn't support cancellations`, + 400 + ); + } + return strategy.cancel(id, options, deps); }; private getSearchStrategy = < @@ -317,7 +324,7 @@ export class SearchService implements Plugin { this.logger.debug(`Get strategy ${name}`); const strategy = this.searchStrategies[name]; if (!strategy) { - throw new Error(`Search strategy ${name} not found`); + throw new KbnServerError(`Search strategy ${name} not found`, 404); } return strategy; }; diff --git a/src/plugins/discover/public/application/angular/directives/histogram.tsx b/src/plugins/discover/public/application/angular/directives/histogram.tsx index 4c39c8bb25542..5a0bd5cca6109 100644 --- a/src/plugins/discover/public/application/angular/directives/histogram.tsx +++ b/src/plugins/discover/public/application/angular/directives/histogram.tsx @@ -17,25 +17,17 @@ * under the License. */ -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; -import moment from 'moment-timezone'; -import { unitOfTime } from 'moment'; +import moment, { unitOfTime } from 'moment-timezone'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; -import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; import { - AnnotationDomainTypes, Axis, Chart, HistogramBarSeries, - LineAnnotation, Position, ScaleType, Settings, - RectAnnotation, - TooltipValue, TooltipType, ElementClickListener, XYChartElementEvent, @@ -43,12 +35,17 @@ import { Theme, } from '@elastic/charts'; -import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'kibana/public'; import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme'; import { Subscription, combineLatest } from 'rxjs'; import { getServices } from '../../../kibana_services'; import { Chart as IChart } from '../helpers/point_series'; +import { + CurrentTime, + Endzones, + getAdjustedInterval, + renderEndzoneTooltip, +} from '../../../../../charts/public'; export interface DiscoverHistogramProps { chartData: IChart; @@ -60,34 +57,6 @@ interface DiscoverHistogramState { chartsBaseTheme: Theme; } -function findIntervalFromDuration( - dateValue: number, - esValue: number, - esUnit: unitOfTime.Base, - timeZone: string -) { - const date = moment.tz(dateValue, timeZone); - const startOfDate = moment.tz(date, timeZone).startOf(esUnit); - const endOfDate = moment.tz(date, timeZone).startOf(esUnit).add(esValue, esUnit); - return endOfDate.valueOf() - startOfDate.valueOf(); -} - -function getIntervalInMs( - value: number, - esValue: number, - esUnit: unitOfTime.Base, - timeZone: string -): number { - switch (esUnit) { - case 's': - return 1000 * esValue; - case 'ms': - return 1 * esValue; - default: - return findIntervalFromDuration(value, esValue, esUnit, timeZone); - } -} - function getTimezone(uiSettings: IUiSettingsClient) { if (uiSettings.isDefault('dateFormat:tz')) { const detectedTimezone = moment.tz.guess(); @@ -98,27 +67,6 @@ function getTimezone(uiSettings: IUiSettingsClient) { } } -export function findMinInterval( - xValues: number[], - esValue: number, - esUnit: string, - timeZone: string -): number { - return xValues.reduce((minInterval, currentXvalue, index) => { - let currentDiff = minInterval; - if (index > 0) { - currentDiff = Math.abs(xValues[index - 1] - currentXvalue); - } - const singleUnitInterval = getIntervalInMs( - currentXvalue, - esValue, - esUnit as unitOfTime.Base, - timeZone - ); - return Math.min(minInterval, singleUnitInterval, currentDiff); - }, Number.MAX_SAFE_INTEGER); -} - export class DiscoverHistogram extends Component { public static propTypes = { chartData: PropTypes.object, @@ -132,10 +80,10 @@ export class DiscoverHistogram extends Component + getServices().theme.chartsBaseTheme$, + ]).subscribe(([chartsTheme, chartsBaseTheme]) => this.setState({ chartsTheme, chartsBaseTheme }) ); } @@ -171,40 +119,6 @@ export class DiscoverHistogram extends Component ( - headerData: TooltipValue - ): JSX.Element | string => { - const headerDataValue = headerData.value; - const formattedValue = this.formatXValue(headerDataValue); - - const partialDataText = i18n.translate('discover.histogram.partialData.bucketTooltipText', { - defaultMessage: - 'The selected time range does not include this entire bucket, it may contain partial data.', - }); - - if (headerDataValue < domainStart || headerDataValue + xInterval > domainEnd) { - return ( - - - - - - {partialDataText} - - -

{formattedValue}

-
- ); - } - - return formattedValue; - }; - public render() { const uiSettings = getServices().uiSettings; const timeZone = getTimezone(uiSettings); @@ -216,8 +130,9 @@ export class DiscoverHistogram extends Component domainStart ? domainStart : data[0]?.x; - const domainMax = domainEnd - xInterval > lastXValue ? domainEnd - xInterval : lastXValue; + const domainMin = Math.min(data[0]?.x, domainStart); + const domainMax = Math.max(domainEnd - xInterval, lastXValue); const xDomain = { min: domainMin, max: domainMax, - minInterval: findMinInterval(xValues, intervalESValue, intervalESUnit, timeZone), - }; - - // Domain end of 'now' will be milliseconds behind current time, so we extend time by 1 minute and check if - // the annotation is within this range; if so, the line annotation uses the domainEnd as its value - const now = moment(); - const isAnnotationAtEdge = moment(domainEnd).add(60000).isAfter(now) && now.isAfter(domainEnd); - const lineAnnotationValue = isAnnotationAtEdge ? domainEnd : now; - - const lineAnnotationData = [ - { - dataValue: lineAnnotationValue, - }, - ]; - const isDarkMode = uiSettings.get('theme:darkMode'); - - const lineAnnotationStyle = { - line: { - strokeWidth: 2, - stroke: isDarkMode ? darkEuiTheme.euiColorDanger : lightEuiTheme.euiColorDanger, - opacity: 0.7, - }, + minInterval: getAdjustedInterval( + xValues, + intervalESValue, + intervalESUnit as unitOfTime.Base, + timeZone + ), }; - - const rectAnnotations = []; - if (domainStart !== domainMin) { - rectAnnotations.push({ - coordinates: { - x1: domainStart, - }, - }); - } - if (domainEnd !== domainMax) { - rectAnnotations.push({ - coordinates: { - x0: domainEnd, - }, - }); - } - - const rectAnnotationStyle = { - stroke: isDarkMode ? darkEuiTheme.euiColorLightShade : lightEuiTheme.euiColorDarkShade, - strokeWidth: 0, - opacity: isDarkMode ? 0.6 : 0.2, - fill: isDarkMode ? darkEuiTheme.euiColorLightShade : lightEuiTheme.euiColorDarkShade, - }; - const tooltipProps = { - headerFormatter: this.renderBarTooltip(xInterval, domainStart, domainEnd), + headerFormatter: renderEndzoneTooltip(xInterval, domainStart, domainEnd, this.formatXValue), type: TooltipType.VerticalCursor, }; @@ -313,19 +188,14 @@ export class DiscoverHistogram extends Component - - + { return { tables: { - layer1: { - type: 'datatable', - columns: [ - { id: 'firstName', name: 'First Name' }, - { id: 'originalLastName', name: 'Last Name' }, - ], - rows: [ - { - firstName: this.getInput().firstName, - orignialLastName: this.getInput().lastName, - }, - ], + allowCsvExport: true, + tables: { + layer1: { + type: 'datatable', + columns: [ + { id: 'firstName', name: 'First Name' }, + { id: 'originalLastName', name: 'Last Name' }, + ], + rows: [ + { + firstName: this.getInput().firstName, + orignialLastName: this.getInput().lastName, + }, + ], + }, }, }, }; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 60b694d628b78..03818fccda0bc 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -109,10 +109,6 @@ export const ACTION_EDIT_PANEL = "editPanel"; export interface Adapters { // (undocumented) [key: string]: any; - // Warning: (ae-forgotten-export) The symbol "DataAdapter" needs to be exported by the entry point index.d.ts - // - // (undocumented) - data?: DataAdapter; // Warning: (ae-forgotten-export) The symbol "RequestAdapter" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 10a18d0cbf435..9819c721d7275 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -220,10 +220,10 @@ describe('Execution', () => { }); describe('inspector adapters', () => { - test('by default, "data" and "requests" inspector adapters are available', async () => { + test('by default, "tables" and "requests" inspector adapters are available', async () => { const { result } = (await run('introspectContext key="inspectorAdapters"')) as any; expect(result).toMatchObject({ - data: expect.any(Object), + tables: expect.any(Object), requests: expect.any(Object), }); }); diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index c5c7d82e223b0..609022f8a55c0 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -23,7 +23,7 @@ import { Executor } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; import { abortSignalToPromise, Defer, now } from '../../../kibana_utils/common'; -import { RequestAdapter, DataAdapter, Adapters } from '../../../inspector/common'; +import { RequestAdapter, Adapters } from '../../../inspector/common'; import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { ExpressionAstExpression, @@ -34,11 +34,12 @@ import { ExpressionAstNode, } from '../ast'; import { ExecutionContext, DefaultInspectorAdapters } from './types'; -import { getType, ExpressionValue } from '../expression_types'; +import { getType, ExpressionValue, Datatable } from '../expression_types'; import { ArgumentType, ExpressionFunction } from '../expression_functions'; import { getByAlias } from '../util/get_by_alias'; import { ExecutionContract } from './execution_contract'; import { ExpressionExecutionParams } from '../service'; +import { TablesAdapter } from '../util/tables_adapter'; /** * AbortController is not available in Node until v15, so we @@ -72,7 +73,7 @@ export interface ExecutionParams { const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({ requests: new RequestAdapter(), - data: new DataAdapter(), + tables: new TablesAdapter(), }); export class Execution< @@ -166,6 +167,9 @@ export class Execution< ast, }); + const inspectorAdapters = + execution.params.inspectorAdapters || createDefaultInspectorAdapters(); + this.context = { getSearchContext: () => this.execution.params.searchContext || {}, getSearchSessionId: () => execution.params.searchSessionId, @@ -175,7 +179,10 @@ export class Execution< variables: execution.params.variables || {}, types: executor.getTypes(), abortSignal: this.abortController.signal, - inspectorAdapters: execution.params.inspectorAdapters || createDefaultInspectorAdapters(), + inspectorAdapters, + logDatatable: (name: string, datatable: Datatable) => { + inspectorAdapters.tables[name] = datatable; + }, ...(execution.params as any).extraContext, }; } diff --git a/src/plugins/expressions/common/execution/execution_contract.test.ts b/src/plugins/expressions/common/execution/execution_contract.test.ts index eaf7e6ea862eb..0a6704a8cb2f6 100644 --- a/src/plugins/expressions/common/execution/execution_contract.test.ts +++ b/src/plugins/expressions/common/execution/execution_contract.test.ts @@ -71,7 +71,7 @@ describe('ExecutionContract', () => { const execution = createExecution('foo bar=123'); const contract = new ExecutionContract(execution); expect(contract.inspect()).toMatchObject({ - data: expect.any(Object), + tables: expect.any(Object), requests: expect.any(Object), }); }); diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index a41f97118c4b2..a1b25c3802f4b 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -21,8 +21,9 @@ import type { KibanaRequest } from 'src/core/server'; import { ExpressionType, SerializableState } from '../expression_types'; -import { Adapters, DataAdapter, RequestAdapter } from '../../../inspector/common'; +import { Adapters, RequestAdapter } from '../../../inspector/common'; import { SavedObject, SavedObjectAttributes } from '../../../../core/public'; +import { TablesAdapter } from '../util/tables_adapter'; /** * `ExecutionContext` is an object available to all functions during a single execution; @@ -89,5 +90,5 @@ export interface ExecutionContext< */ export interface DefaultInspectorAdapters extends Adapters { requests: RequestAdapter; - data: DataAdapter; + tables: TablesAdapter; } diff --git a/src/plugins/expressions/common/util/index.ts b/src/plugins/expressions/common/util/index.ts index ee677d54ce968..ea900687650f8 100644 --- a/src/plugins/expressions/common/util/index.ts +++ b/src/plugins/expressions/common/util/index.ts @@ -19,3 +19,4 @@ export * from './create_error'; export * from './get_by_alias'; +export * from './tables_adapter'; diff --git a/test/functional/apps/getting_started/index.js b/src/plugins/expressions/common/util/tables_adapter.ts similarity index 66% rename from test/functional/apps/getting_started/index.js rename to src/plugins/expressions/common/util/tables_adapter.ts index 399d3fe87484b..30b869818f999 100644 --- a/test/functional/apps/getting_started/index.js +++ b/src/plugins/expressions/common/util/tables_adapter.ts @@ -17,16 +17,18 @@ * under the License. */ -export default function ({ getService, loadTestFile }) { - const browser = getService('browser'); +import { EventEmitter } from 'events'; +import { Datatable } from '../expression_types/specs'; - describe('Getting Started ', function () { - this.tags(['ciGroup6']); +export class TablesAdapter extends EventEmitter { + private _tables: { [key: string]: Datatable } = {}; - before(async function () { - await browser.setWindowSize(1200, 800); - }); - // https://www.elastic.co/guide/en/kibana/current/tutorial-load-dataset.html - loadTestFile(require.resolve('./_shakespeare')); - }); + public logDatatable(name: string, datatable: Datatable): void { + this._tables[name] = datatable; + this.emit('change', this.tables); + } + + public get tables() { + return this._tables; + } } diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 385055bc2fdc2..cd43c90d5ff45 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -117,4 +117,5 @@ export { ExpressionsService, ExpressionsServiceSetup, ExpressionsServiceStart, + TablesAdapter, } from '../common'; diff --git a/src/plugins/expressions/public/loader.test.ts b/src/plugins/expressions/public/loader.test.ts index 598b614a326a9..0508b36fad385 100644 --- a/src/plugins/expressions/public/loader.test.ts +++ b/src/plugins/expressions/public/loader.test.ts @@ -166,7 +166,7 @@ describe('ExpressionLoader', () => { it('inspect() returns correct inspector adapters', () => { const expressionDataHandler = new ExpressionLoader(element, expressionString, {}); - expect(expressionDataHandler.inspect()).toHaveProperty('data'); + expect(expressionDataHandler.inspect()).toHaveProperty('tables'); expect(expressionDataHandler.inspect()).toHaveProperty('requests'); }); }); diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 6eb0e71c58e3f..bb1f5dd9270d5 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -1096,6 +1096,18 @@ export interface SerializedFieldFormat> { // @public (undocumented) export type Style = ExpressionTypeStyle; +// Warning: (ae-missing-release-tag) "TablesAdapter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class TablesAdapter extends EventEmitter { + // (undocumented) + logDatatable(name: string, datatable: Datatable): void; + // (undocumented) + get tables(): { + [key: string]: Datatable; + }; + } + // Warning: (ae-missing-release-tag) "TextAlignment" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts index 60d05890028d1..5cb837f816ff2 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts @@ -34,7 +34,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[eCommerce] Sales by Category', }), visState: - '{"title":"[eCommerce] Sales by Category","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Sum of total_quantity"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Sum of total_quantity","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"top","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"sum","schema":"metric","params":{"field":"total_quantity"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"order_date","interval":"auto","time_zone":"America/New_York","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"category.keyword","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[eCommerce] Sales by Category","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Sum of total_quantity"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Sum of total_quantity","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"top","times":[],"addTimeMarker":false,"detailedTooltip":true},"aggs":[{"id":"1","enabled":true,"type":"sum","schema":"metric","params":{"field":"total_quantity"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"order_date","interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"category.keyword","size":5,"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 e65b6ad40651b..6b06a526a1239 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 @@ -56,7 +56,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Flight Count and Average Ticket Price', }), visState: - '{"title":"[Flights] Flight Count and Average Ticket Price","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Average Ticket Price"}},{"id":"ValueAxis-2","name":"RightAxis-1","type":"value","position":"right","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Flight Count"}}],"seriesParams":[{"show":true,"mode":"stacked","type":"area","drawLinesBetweenPoints":true,"showCircles":false,"interpolate":"linear","lineWidth":2,"data":{"id":"5","label":"Flight Count"},"valueAxis":"ValueAxis-2"},{"show":true,"mode":"stacked","type":"line","drawLinesBetweenPoints":false,"showCircles":true,"interpolate":"linear","data":{"id":"4","label":"Average Ticket Price"},"valueAxis":"ValueAxis-1","lineWidth":2}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"radiusRatio":13},"aggs":[{"id":"3","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"5","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Flight Count"}},{"id":"4","enabled":true,"type":"avg","schema":"metric","params":{"field":"AvgTicketPrice","customLabel":"Average Ticket Price"}},{"id":"2","enabled":true,"type":"avg","schema":"radius","params":{"field":"AvgTicketPrice"}}]}', + '{"title":"[Flights] Flight Count and Average Ticket Price","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Average Ticket Price"}},{"id":"ValueAxis-2","name":"RightAxis-1","type":"value","position":"right","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Flight Count"}}],"seriesParams":[{"show":true,"mode":"stacked","type":"area","drawLinesBetweenPoints":true,"showCircles":false,"interpolate":"linear","lineWidth":2,"data":{"id":"5","label":"Flight Count"},"valueAxis":"ValueAxis-2"},{"show":true,"mode":"stacked","type":"line","drawLinesBetweenPoints":false,"showCircles":true,"interpolate":"linear","data":{"id":"4","label":"Average Ticket Price"},"valueAxis":"ValueAxis-1","lineWidth":2}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"radiusRatio":13,"detailedTooltip":true},"aggs":[{"id":"3","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","min_doc_count":1,"extended_bounds":{}}},{"id":"5","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Flight Count"}},{"id":"4","enabled":true,"type":"avg","schema":"metric","params":{"field":"AvgTicketPrice","customLabel":"Average Ticket Price"}},{"id":"2","enabled":true,"type":"avg","schema":"radius","params":{"field":"AvgTicketPrice"}}]}', uiStateJSON: '{"vis":{"legendOpen":true,"colors":{"Average Ticket Price":"#629E51","Flight Count":"#AEA2E0"}}}', description: '', @@ -133,7 +133,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Delay Type', }), visState: - '{"title":"[Flights] Delay Type","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"FlightDelayType","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[Flights] Delay Type","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"FlightDelayType","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', uiStateJSON: '{}', description: '', version: 1, @@ -176,7 +176,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Delay Buckets', }), visState: - '{"title":"[Flights] Delay Buckets","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"histogram","schema":"segment","params":{"field":"FlightDelayMin","interval":30,"extended_bounds":{},"customLabel":"Flight Delay Minutes"}}]}', + '{"title":"[Flights] Delay Buckets","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"histogram","schema":"segment","params":{"field":"FlightDelayMin","interval":30,"extended_bounds":{},"customLabel":"Flight Delay Minutes"}}]}', uiStateJSON: '{"vis":{"legendOpen":false}}', description: '', version: 1, @@ -198,7 +198,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Flight Delays', }), visState: - '{"title":"[Flights] Flight Delays","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"left","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"BottomAxis-1","type":"value","position":"bottom","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"FlightDelay","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Flight Delays"}}]}', + '{"title":"[Flights] Flight Delays","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"left","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"BottomAxis-1","type":"value","position":"bottom","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"FlightDelay","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Flight Delays"}}]}', uiStateJSON: '{}', description: '', version: 1, @@ -220,7 +220,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Flight Cancellations', }), visState: - '{"title":"[Flights] Flight Cancellations","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"left","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"BottomAxis-1","type":"value","position":"bottom","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"Cancelled","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Flight Cancellations"}}]}', + '{"title":"[Flights] Flight Cancellations","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"left","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"BottomAxis-1","type":"value","position":"bottom","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"Cancelled","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Flight Cancellations"}}]}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts index 068ba66c4b0de..7b891107cdfb0 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts @@ -33,7 +33,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Logs] Unique Visitors vs. Average Bytes', }), visState: - '{"title":"[Logs] Unique Visitors vs. Average Bytes","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Avg. Bytes"}},{"id":"ValueAxis-2","name":"RightAxis-1","type":"value","position":"right","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Unique Visitors"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Avg. Bytes","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"},{"show":true,"mode":"stacked","type":"line","drawLinesBetweenPoints":false,"showCircles":true,"interpolate":"linear","data":{"id":"2","label":"Unique Visitors"},"valueAxis":"ValueAxis-2"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"radiusRatio":17},"aggs":[{"id":"1","enabled":true,"type":"avg","schema":"metric","params":{"field":"bytes","customLabel":"Avg. Bytes"}},{"id":"2","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"clientip","customLabel":"Unique Visitors"}},{"id":"3","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","time_zone":"America/Los_Angeles","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"4","enabled":true,"type":"count","schema":"radius","params":{}}]}', + '{"title":"[Logs] Unique Visitors vs. Average Bytes","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Avg. Bytes"}},{"id":"ValueAxis-2","name":"RightAxis-1","type":"value","position":"right","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Unique Visitors"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Avg. Bytes","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"},{"show":true,"mode":"stacked","type":"line","drawLinesBetweenPoints":false,"showCircles":true,"interpolate":"linear","data":{"id":"2","label":"Unique Visitors"},"valueAxis":"ValueAxis-2"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"radiusRatio":17,"detailedTooltip":true},"aggs":[{"id":"1","enabled":true,"type":"avg","schema":"metric","params":{"field":"bytes","customLabel":"Avg. Bytes"}},{"id":"2","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"clientip","customLabel":"Unique Visitors"}},{"id":"3","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","min_doc_count":1,"extended_bounds":{}}},{"id":"4","enabled":true,"type":"count","schema":"radius","params":{}}]}', uiStateJSON: '{"vis":{"colors":{"Avg. Bytes":"#70DBED","Unique Visitors":"#0A437C"}}}', description: '', version: 1, diff --git a/src/plugins/inspector/common/adapters/data/data_adapters.test.ts b/src/plugins/inspector/common/adapters/data/data_adapters.test.ts deleted file mode 100644 index 7cc52807548f0..0000000000000 --- a/src/plugins/inspector/common/adapters/data/data_adapters.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { DataAdapter } from './data_adapter'; - -describe('DataAdapter', () => { - let adapter: DataAdapter; - - beforeEach(() => { - adapter = new DataAdapter(); - }); - - describe('getTabular()', () => { - it('should return a null promise when called before initialized', () => { - expect(adapter.getTabular()).resolves.toEqual({ - data: null, - options: {}, - }); - }); - - it('should call the provided callback and resolve with its value', async () => { - const data = { columns: [], rows: [] }; - const spy = jest.fn(() => data); - adapter.setTabularLoader(spy); - expect(spy).not.toBeCalled(); - const result = await adapter.getTabular(); - expect(spy).toBeCalled(); - expect(result.data).toBe(data); - }); - - it('should pass through options specified via setTabularLoader', async () => { - const data = { columns: [], rows: [] }; - adapter.setTabularLoader(() => data, { returnsFormattedValues: true }); - const result = await adapter.getTabular(); - expect(result.options).toEqual({ returnsFormattedValues: true }); - }); - - it('should return options set when starting loading data', async () => { - const data = { columns: [], rows: [] }; - adapter.setTabularLoader(() => data, { returnsFormattedValues: true }); - const waitForResult = adapter.getTabular(); - adapter.setTabularLoader(() => data, { returnsFormattedValues: false }); - const result = await waitForResult; - expect(result.options).toEqual({ returnsFormattedValues: true }); - }); - }); - - it('should emit a "tabular" event when a new tabular loader is specified', () => { - const data = { columns: [], rows: [] }; - const spy = jest.fn(); - adapter.once('change', spy); - adapter.setTabularLoader(() => data); - expect(spy).toBeCalled(); - }); -}); diff --git a/src/plugins/inspector/common/adapters/index.ts b/src/plugins/inspector/common/adapters/index.ts index 0c6319a2905a8..152d7c54d7d84 100644 --- a/src/plugins/inspector/common/adapters/index.ts +++ b/src/plugins/inspector/common/adapters/index.ts @@ -17,6 +17,5 @@ * under the License. */ -export * from './data'; export * from './request'; export * from './types'; diff --git a/src/plugins/inspector/common/adapters/types.ts b/src/plugins/inspector/common/adapters/types.ts index b51c3e56c749f..ee56c994be469 100644 --- a/src/plugins/inspector/common/adapters/types.ts +++ b/src/plugins/inspector/common/adapters/types.ts @@ -17,14 +17,12 @@ * under the License. */ -import type { DataAdapter } from './data'; import type { RequestAdapter } from './request'; /** * The interface that the adapters used to open an inspector have to fullfill. */ export interface Adapters { - data?: DataAdapter; requests?: RequestAdapter; [key: string]: any; } diff --git a/src/plugins/inspector/common/index.ts b/src/plugins/inspector/common/index.ts index c5755b22095dc..f9f486521a76b 100644 --- a/src/plugins/inspector/common/index.ts +++ b/src/plugins/inspector/common/index.ts @@ -19,15 +19,9 @@ export { Adapters, - DataAdapter, - FormattedData, RequestAdapter, RequestStatistic, RequestStatistics, RequestStatus, RequestResponder, - TabularData, - TabularDataColumn, - TabularDataRow, - TabularDataValue, } from './adapters'; diff --git a/src/plugins/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx index 07ef7c8fbab0d..d3d867344a2a8 100644 --- a/src/plugins/inspector/public/plugin.tsx +++ b/src/plugins/inspector/public/plugin.tsx @@ -26,7 +26,7 @@ import { InspectorOptions, InspectorSession } from './types'; import { InspectorPanel } from './ui/inspector_panel'; import { Adapters } from '../common'; -import { getRequestsViewDescription, getDataViewDescription } from './views'; +import { getRequestsViewDescription } from './views'; export interface Setup { registerView: InspectorViewRegistry['register']; @@ -70,7 +70,6 @@ export class InspectorPublicPlugin implements Plugin { public async setup(core: CoreSetup) { this.views = new InspectorViewRegistry(); - this.views.register(getDataViewDescription()); this.views.register(getRequestsViewDescription()); return { diff --git a/src/plugins/inspector/public/test/is_available.test.ts b/src/plugins/inspector/public/test/is_available.test.ts index c38d9d7a3f825..1f5220fc07a63 100644 --- a/src/plugins/inspector/public/test/is_available.test.ts +++ b/src/plugins/inspector/public/test/is_available.test.ts @@ -18,19 +18,12 @@ */ import { inspectorPluginMock } from '../mocks'; -import { DataAdapter, RequestAdapter } from '../../common/adapters'; +import { RequestAdapter } from '../../common/adapters'; -const adapter1 = new DataAdapter(); const adapter2 = new RequestAdapter(); describe('inspector', () => { describe('isAvailable()', () => { - it('should return false if no view would be available', async () => { - const { doStart } = await inspectorPluginMock.createPlugin(); - const start = await doStart(); - expect(start.isAvailable({ adapter1 })).toBe(false); - }); - it('should return true if views would be available, false otherwise', async () => { const { setup, doStart } = await inspectorPluginMock.createPlugin(); @@ -44,7 +37,6 @@ describe('inspector', () => { const start = await doStart(); - expect(start.isAvailable({ adapter1 })).toBe(true); expect(start.isAvailable({ adapter2 })).toBe(false); }); }); diff --git a/src/plugins/inspector/public/views/_index.scss b/src/plugins/inspector/public/views/_index.scss index 620a33e965840..43fbc09e921cc 100644 --- a/src/plugins/inspector/public/views/_index.scss +++ b/src/plugins/inspector/public/views/_index.scss @@ -1,2 +1 @@ -@import './data/index'; @import './requests/index'; diff --git a/src/plugins/inspector/public/views/data/components/data_view.tsx b/src/plugins/inspector/public/views/data/components/data_view.tsx deleted file mode 100644 index 324094d8f93d0..0000000000000 --- a/src/plugins/inspector/public/views/data/components/data_view.tsx +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingChart, - EuiPanel, - EuiSpacer, - EuiText, -} from '@elastic/eui'; - -import { DataTableFormat } from './data_table'; -import { InspectorViewProps } from '../../../types'; -import { Adapters } from '../../../../common'; -import { - TabularLoaderOptions, - TabularData, - TabularHolder, -} from '../../../../common/adapters/data/types'; -import { IUiSettingsClient } from '../../../../../../core/public'; -import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public'; - -interface DataViewComponentState { - tabularData: TabularData | null; - tabularOptions: TabularLoaderOptions; - adapters: Adapters; - tabularPromise: Promise | null; -} - -interface DataViewComponentProps extends InspectorViewProps { - kibana: KibanaReactContextValue<{ uiSettings: IUiSettingsClient }>; -} - -class DataViewComponent extends Component { - static propTypes = { - adapters: PropTypes.object.isRequired, - title: PropTypes.string.isRequired, - kibana: PropTypes.object, - }; - - state = {} as DataViewComponentState; - _isMounted = false; - - static getDerivedStateFromProps( - nextProps: DataViewComponentProps, - state: DataViewComponentState - ) { - if (state && nextProps.adapters === state.adapters) { - return null; - } - - return { - adapters: nextProps.adapters, - tabularData: null, - tabularOptions: {}, - tabularPromise: nextProps.adapters.data!.getTabular(), - }; - } - - onUpdateData = (type: string) => { - if (type === 'tabular') { - this.setState({ - tabularData: null, - tabularOptions: {}, - tabularPromise: this.props.adapters.data!.getTabular(), - }); - } - }; - - async finishLoadingData() { - const { tabularPromise } = this.state; - - if (tabularPromise) { - const tabularData: TabularHolder = await tabularPromise; - - if (this._isMounted) { - this.setState({ - tabularData: tabularData.data, - tabularOptions: tabularData.options, - tabularPromise: null, - }); - } - } - } - - componentDidMount() { - this._isMounted = true; - this.props.adapters.data!.on('change', this.onUpdateData); - this.finishLoadingData(); - } - - componentWillUnmount() { - this._isMounted = false; - this.props.adapters.data!.removeListener('change', this.onUpdateData); - } - - componentDidUpdate() { - this.finishLoadingData(); - } - - static renderNoData() { - return ( - - - - } - body={ - -

- -

-
- } - /> - ); - } - - static renderLoading() { - return ( - - - - - - -

- -

-
-
-
-
- ); - } - - render() { - if (this.state.tabularPromise) { - return DataViewComponent.renderLoading(); - } else if (!this.state.tabularData) { - return DataViewComponent.renderNoData(); - } - - return ( - - ); - } -} - -// default export required for React.Lazy -// eslint-disable-next-line import/no-default-export -export default withKibana(DataViewComponent); diff --git a/src/plugins/inspector/public/views/index.ts b/src/plugins/inspector/public/views/index.ts index c75ecfbd3e998..8aef30a68a327 100644 --- a/src/plugins/inspector/public/views/index.ts +++ b/src/plugins/inspector/public/views/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export { getDataViewDescription } from './data'; export { getRequestsViewDescription } from './requests'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.test.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.test.ts index a7681e1766427..64f1088dc3392 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.test.ts @@ -16,14 +16,25 @@ * specific language governing permissions and limitations * under the License. */ - +import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; import { getSavedObjectsCounts } from './get_saved_object_counts'; +export function mockGetSavedObjectsCounts(params: any) { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + esClient.search.mockResolvedValue( + // @ts-ignore we only care about the response body + { + body: { ...params }, + } + ); + return esClient; +} + describe('getSavedObjectsCounts', () => { test('Get all the saved objects equal to 0 because no results were found', async () => { - const callCluster = jest.fn(() => ({})); + const esClient = mockGetSavedObjectsCounts({}); - const results = await getSavedObjectsCounts(callCluster as any, '.kibana'); + const results = await getSavedObjectsCounts(esClient, '.kibana'); expect(results).toStrictEqual({ dashboard: { total: 0 }, visualization: { total: 0 }, @@ -35,7 +46,7 @@ describe('getSavedObjectsCounts', () => { }); test('Merge the zeros with the results', async () => { - const callCluster = jest.fn(() => ({ + const esClient = mockGetSavedObjectsCounts({ aggregations: { types: { buckets: [ @@ -46,9 +57,9 @@ describe('getSavedObjectsCounts', () => { ], }, }, - })); + }); - const results = await getSavedObjectsCounts(callCluster as any, '.kibana'); + const results = await getSavedObjectsCounts(esClient, '.kibana'); expect(results).toStrictEqual({ dashboard: { total: 1 }, visualization: { total: 0 }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.ts index e88d90fe5b24b..65cc3643a88cb 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.ts @@ -27,7 +27,7 @@ */ import { snakeCase } from 'lodash'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'src/core/server'; const TYPES = [ 'dashboard', @@ -48,7 +48,7 @@ export interface KibanaSavedObjectCounts { } export async function getSavedObjectsCounts( - callCluster: LegacyAPICaller, + esClient: ElasticsearchClient, kibanaIndex: string // Typically '.kibana'. We might need a way to obtain it from the SavedObjects client (or the SavedObjects client to provide a way to run aggregations?) ): Promise { const savedObjectCountSearchParams = { @@ -67,9 +67,9 @@ export async function getSavedObjectsCounts( }, }, }; - const resp = await callCluster('search', savedObjectCountSearchParams); + const { body } = await esClient.search(savedObjectCountSearchParams); const buckets: Array<{ key: string; doc_count: number }> = - resp.aggregations?.types?.buckets || []; + body.aggregations?.types?.buckets || []; // Initialise the object with all zeros for all the types const allZeros: KibanaSavedObjectCounts = TYPES.reduce( diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts index 83cac1d456a3a..dee9ca4d32c5f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts @@ -20,12 +20,13 @@ import { loggingSystemMock, pluginInitializerContextConfigMock, + elasticsearchServiceMock, } from '../../../../../core/server/mocks'; import { Collector, + createCollectorFetchContextMock, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; -import { createCollectorFetchContextMock } from '../../../../usage_collection/server/mocks'; import { registerKibanaUsageCollector } from './'; const logger = loggingSystemMock.createLogger(); @@ -43,7 +44,9 @@ describe('telemetry_kibana', () => { const getMockFetchClients = (hits?: unknown[]) => { const fetchParamsMock = createCollectorFetchContextMock(); - fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + esClient.search.mockResolvedValue({ body: { hits: { hits } } } as any); + fetchParamsMock.esClient = esClient; return fetchParamsMock; }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts index 6c2e0a2c926ad..5dd39d172e1c2 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts @@ -43,13 +43,13 @@ export function getKibanaUsageCollector( graph_workspace: { total: { type: 'long' } }, timelion_sheet: { total: { type: 'long' } }, }, - async fetch({ callCluster }) { + async fetch({ esClient }) { const { kibana: { index }, } = await legacyConfig$.pipe(take(1)).toPromise(); return { index, - ...(await getSavedObjectsCounts(callCluster, index)), + ...(await getSavedObjectsCounts(esClient, index)), }; }, }); diff --git a/src/plugins/kibana_utils/server/index.ts b/src/plugins/kibana_utils/server/index.ts index d994a4940bdfd..a0e933f997dd5 100644 --- a/src/plugins/kibana_utils/server/index.ts +++ b/src/plugins/kibana_utils/server/index.ts @@ -28,3 +28,5 @@ export { Set, url, } from '../common'; + +export { KbnServerError, reportServerError } from './report_server_error'; diff --git a/src/plugins/kibana_utils/server/report_server_error.ts b/src/plugins/kibana_utils/server/report_server_error.ts new file mode 100644 index 0000000000000..43b4dba3d039b --- /dev/null +++ b/src/plugins/kibana_utils/server/report_server_error.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { KibanaResponseFactory } from 'kibana/server'; +import { KbnError } from '../common'; + +export class KbnServerError extends KbnError { + constructor(message: string, public readonly statusCode: number) { + super(message); + } +} + +export function reportServerError(res: KibanaResponseFactory, err: any) { + return res.customError({ + statusCode: err.statusCode ?? 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); +} diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index bf03c649fa6b4..81026927380e0 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -58,11 +58,12 @@ export class ManagementPlugin implements Plugin ({ default: '"{}"', }, }, - fn(context, args) { + fn(context, args, handlers) { const visConfig = JSON.parse(args.visConfig); + if (handlers?.inspectorAdapters?.tables) { + handlers.inspectorAdapters.tables.logDatatable('default', context); + } return { type: 'render', as: 'visualization', diff --git a/src/plugins/region_map/public/region_map_fn.test.js b/src/plugins/region_map/public/region_map_fn.test.js index 32467541dee02..d83d04be6d38c 100644 --- a/src/plugins/region_map/public/region_map_fn.test.js +++ b/src/plugins/region_map/public/region_map_fn.test.js @@ -57,7 +57,11 @@ describe('interpreter/functions#regionmap', () => { }; it('returns an object with the correct structure', () => { - const actual = fn(context, { visConfig: JSON.stringify(visConfig) }); + const actual = fn( + context, + { visConfig: JSON.stringify(visConfig) }, + { logDatatable: jest.fn() } + ); expect(actual).toMatchSnapshot(); }); }); diff --git a/src/plugins/tile_map/public/tile_map_fn.js b/src/plugins/tile_map/public/tile_map_fn.js index 3253598d98d94..7a5f36be1eb9d 100644 --- a/src/plugins/tile_map/public/tile_map_fn.js +++ b/src/plugins/tile_map/public/tile_map_fn.js @@ -34,7 +34,7 @@ export const createTileMapFn = () => ({ default: '"{}"', }, }, - fn(context, args) { + fn(context, args, handlers) { const visConfig = JSON.parse(args.visConfig); const { geohash, metric, geocentroid } = visConfig.dimensions; const convertedData = convertToGeoJson(context, { @@ -47,6 +47,9 @@ export const createTileMapFn = () => ({ convertedData.meta.geohash = context.columns[geohash.accessor].meta; } + if (handlers?.inspectorAdapters?.tables) { + handlers.inspectorAdapters.tables.logDatatable('default', context); + } return { type: 'render', as: 'visualization', diff --git a/src/plugins/tile_map/public/tilemap_fn.test.js b/src/plugins/tile_map/public/tilemap_fn.test.js index df9fc10a7303c..895842ea1e8f4 100644 --- a/src/plugins/tile_map/public/tilemap_fn.test.js +++ b/src/plugins/tile_map/public/tilemap_fn.test.js @@ -80,13 +80,17 @@ describe('interpreter/functions#tilemap', () => { }); it('returns an object with the correct structure', () => { - const actual = fn(context, { visConfig: JSON.stringify(visConfig) }); + const actual = fn( + context, + { visConfig: JSON.stringify(visConfig) }, + { logDatatable: jest.fn() } + ); expect(actual).toMatchSnapshot(); }); it('calls response handler with correct values', () => { const { geohash, metric, geocentroid } = visConfig.dimensions; - fn(context, { visConfig: JSON.stringify(visConfig) }); + fn(context, { visConfig: JSON.stringify(visConfig) }, { logDatatable: jest.fn() }); expect(convertToGeoJson).toHaveBeenCalledTimes(1); expect(convertToGeoJson).toHaveBeenCalledWith(context, { geohash, diff --git a/src/plugins/vis_default_editor/public/components/agg_add.tsx b/src/plugins/vis_default_editor/public/components/agg_add.tsx index e78f2fcc4453c..2da7b33139a8e 100644 --- a/src/plugins/vis_default_editor/public/components/agg_add.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_add.tsx @@ -76,10 +76,17 @@ function DefaultEditorAggAdd({ ); - const isSchemaDisabled = (schema: Schema): boolean => { + const isMaxedCount = (schema: Schema): boolean => { const count = group.filter((agg) => agg.schema === schema.name).length; return count >= schema.max; }; + const isSchemaDisabled = (schema: Schema, maxedCount: boolean): boolean => { + return schema.disabled ?? maxedCount; + }; + const maxTooltipText = i18n.translate('visDefaultEditor.aggAdd.maxBuckets', { + defaultMessage: 'Max {groupNameLabel} count reached', + values: { groupNameLabel }, + }); return ( @@ -109,16 +116,21 @@ function DefaultEditorAggAdd({ )} ( - onSelectSchema(schema)} - > - {schema.title} - - ))} + items={schemas.map((schema) => { + const maxedCount = isMaxedCount(schema); + + return ( + onSelectSchema(schema)} + toolTipContent={schema.tooltip ?? (maxedCount ? maxTooltipText : undefined)} + > + {schema.title} + + ); + })} />
diff --git a/src/plugins/vis_default_editor/public/components/sidebar/use_option_tabs.ts b/src/plugins/vis_default_editor/public/components/sidebar/use_option_tabs.ts index 337533df50fad..90e2d792d3597 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/use_option_tabs.ts +++ b/src/plugins/vis_default_editor/public/components/sidebar/use_option_tabs.ts @@ -20,9 +20,10 @@ import { useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { Vis } from 'src/plugins/visualizations/public'; -import { DefaultEditorDataTab, DefaultEditorDataTabProps } from './data_tab'; +import { Vis } from '../../../../visualizations/public'; + import { VisOptionsProps } from '../../vis_options_props'; +import { DefaultEditorDataTab, DefaultEditorDataTabProps } from './data_tab'; export interface OptionTab { editor: React.ComponentType; diff --git a/src/plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx index 707b14c23ea75..f44ea3e203b05 100644 --- a/src/plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -25,6 +25,7 @@ import { EuiErrorBoundary, EuiLoadingChart } from '@elastic/eui'; import { EditorRenderProps, IEditorController } from 'src/plugins/visualize/public'; import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; +// @ts-ignore const DefaultEditor = lazy(() => import('./default_editor')); class DefaultEditorController implements IEditorController { diff --git a/src/plugins/vis_default_editor/public/schemas.ts b/src/plugins/vis_default_editor/public/schemas.ts index d95a6252331bf..7ecb4e54726b8 100644 --- a/src/plugins/vis_default_editor/public/schemas.ts +++ b/src/plugins/vis_default_editor/public/schemas.ts @@ -17,6 +17,7 @@ * under the License. */ +import { ReactNode } from 'react'; import _, { defaults } from 'lodash'; import { Optional } from '@kbn/utility-types'; @@ -42,6 +43,8 @@ export interface Schema { hideCustomLabel?: boolean; mustBeFirst?: boolean; aggSettings?: any; + disabled?: boolean; + tooltip?: ReactNode; } export class Schemas implements ISchemas { diff --git a/src/plugins/vis_type_markdown/public/to_ast.test.ts b/src/plugins/vis_type_markdown/public/to_ast.test.ts index 1ad1fa0ee2517..4c7b570aef05d 100644 --- a/src/plugins/vis_type_markdown/public/to_ast.test.ts +++ b/src/plugins/vis_type_markdown/public/to_ast.test.ts @@ -42,12 +42,14 @@ describe('markdown vis toExpressionAst function', () => { it('without params', () => { vis.params = {}; + // @ts-expect-error const actual = toExpressionAst(vis); expect(actual).toMatchSnapshot(); }); it('with params', () => { vis.params = { markdown: '### my markdown', fontSize: 15, openLinksInNewTab: true }; + // @ts-expect-error const actual = toExpressionAst(vis); expect(actual).toMatchSnapshot(); }); diff --git a/src/plugins/vis_type_markdown/public/to_ast.ts b/src/plugins/vis_type_markdown/public/to_ast.ts index 9b481218b42ea..dc61acf722ad4 100644 --- a/src/plugins/vis_type_markdown/public/to_ast.ts +++ b/src/plugins/vis_type_markdown/public/to_ast.ts @@ -17,11 +17,11 @@ * under the License. */ -import { Vis } from '../../visualizations/public'; +import { VisToExpressionAst } from '../../visualizations/public'; import { buildExpression, buildExpressionFunction } from '../../expressions/public'; import { MarkdownVisExpressionFunctionDefinition } from './markdown_fn'; -export const toExpressionAst = (vis: Vis) => { +export const toExpressionAst: VisToExpressionAst = (vis) => { const { markdown, fontSize, openLinksInNewTab } = vis.params; const markdownVis = buildExpressionFunction( diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx index d0a7412238871..d87a0da740d75 100644 --- a/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx @@ -31,7 +31,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { - ColorModes, + ColorMode, ColorRanges, ColorSchemaOptions, SwitchOption, @@ -86,7 +86,7 @@ function MetricVisOptions({ ); const setColorMode: EuiButtonGroupProps['onChange'] = useCallback( - (id: string) => setMetricValue('metricColorMode', id as ColorModes), + (id: string) => setMetricValue('metricColorMode', id as ColorMode), [setMetricValue] ); @@ -158,7 +158,7 @@ function MetricVisOptions({ colorSchemas={vis.type.editorConfig.collections.colorSchemas} disabled={ stateParams.metric.colorsRange.length === 1 || - stateParams.metric.metricColorMode === ColorModes.NONE + stateParams.metric.metricColorMode === ColorMode.None } invertColors={stateParams.metric.invertColors} setValue={setMetricValue as SetColorSchemaOptionsValue} diff --git a/src/plugins/vis_type_metric/public/metric_vis_fn.ts b/src/plugins/vis_type_metric/public/metric_vis_fn.ts index 20de22f50e63a..783ca3d3906ea 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_fn.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_fn.ts @@ -27,14 +27,14 @@ import { Style, } from '../../expressions/public'; import { visType, DimensionsVisParam, VisParams } from './types'; -import { ColorSchemas, vislibColorMaps, ColorModes } from '../../charts/public'; +import { ColorSchemas, vislibColorMaps, ColorMode } from '../../charts/public'; export type Input = Datatable; interface Arguments { percentageMode: boolean; colorSchema: ColorSchemas; - colorMode: ColorModes; + colorMode: ColorMode; useRanges: boolean; invertColors: boolean; showLabels: boolean; @@ -86,7 +86,7 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({ colorMode: { types: ['string'], default: '"None"', - options: [ColorModes.NONE, ColorModes.LABELS, ColorModes.BACKGROUND], + options: [ColorMode.None, ColorMode.Labels, ColorMode.Background], help: i18n.translate('visTypeMetric.function.colorMode.help', { defaultMessage: 'Which part of metric to color', }), @@ -160,7 +160,7 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({ }), }, }, - fn(input, args) { + fn(input, args, handlers) { const dimensions: DimensionsVisParam = { metrics: args.metric, }; @@ -175,6 +175,9 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({ const fontSize = Number.parseInt(args.font.spec.fontSize || '', 10); + if (handlers?.inspectorAdapters?.tables) { + handlers.inspectorAdapters.tables.logDatatable('default', input); + } return { type: 'render', as: 'metric_vis', @@ -194,8 +197,8 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({ invertColors: args.invertColors, style: { bgFill: args.bgFill, - bgColor: args.colorMode === ColorModes.BACKGROUND, - labelColor: args.colorMode === ColorModes.LABELS, + bgColor: args.colorMode === ColorMode.Background, + labelColor: args.colorMode === ColorMode.Labels, subText: args.subText, fontSize, }, diff --git a/src/plugins/vis_type_metric/public/metric_vis_type.ts b/src/plugins/vis_type_metric/public/metric_vis_type.ts index f7c74e324053e..ba8f27b9412a2 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_type.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { BaseVisTypeOptions } from 'src/plugins/visualizations/public'; import { MetricVisOptions } from './components/metric_vis_options'; -import { ColorSchemas, colorSchemas, ColorModes } from '../../charts/public'; +import { ColorSchemas, colorSchemas, ColorMode } from '../../charts/public'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; import { toExpressionAst } from './to_ast'; @@ -42,7 +42,7 @@ export const createMetricVisTypeDefinition = (): BaseVisTypeOptions => ({ percentageMode: false, useRanges: false, colorSchema: ColorSchemas.GreenToRed, - metricColorMode: ColorModes.NONE, + metricColorMode: ColorMode.None, colorsRange: [{ from: 0, to: 10000 }], labels: { show: true, @@ -62,19 +62,19 @@ export const createMetricVisTypeDefinition = (): BaseVisTypeOptions => ({ collections: { metricColorMode: [ { - id: ColorModes.NONE, + id: ColorMode.None, label: i18n.translate('visTypeMetric.colorModes.noneOptionLabel', { defaultMessage: 'None', }), }, { - id: ColorModes.LABELS, + id: ColorMode.Labels, label: i18n.translate('visTypeMetric.colorModes.labelsOptionLabel', { defaultMessage: 'Labels', }), }, { - id: ColorModes.BACKGROUND, + id: ColorMode.Background, label: i18n.translate('visTypeMetric.colorModes.backgroundOptionLabel', { defaultMessage: 'Background', }), diff --git a/src/plugins/vis_type_metric/public/types.ts b/src/plugins/vis_type_metric/public/types.ts index e1f2c7721a426..e0ebfb36fa37d 100644 --- a/src/plugins/vis_type_metric/public/types.ts +++ b/src/plugins/vis_type_metric/public/types.ts @@ -19,7 +19,7 @@ import { Range } from '../../expressions/public'; import { SchemaConfig } from '../../visualizations/public'; -import { ColorModes, Labels, Style, ColorSchemas } from '../../charts/public'; +import { ColorMode, Labels, Style, ColorSchemas } from '../../charts/public'; export const visType = 'metric'; @@ -32,7 +32,7 @@ export interface MetricVisParam { percentageMode: boolean; useRanges: boolean; colorSchema: ColorSchemas; - metricColorMode: ColorModes; + metricColorMode: ColorMode; colorsRange: Range[]; labels: Labels; invertColors: boolean; diff --git a/src/plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts index 28990f28caf31..530f50ffd89b6 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -55,10 +55,13 @@ export const createTableVisFn = (): TableExpressionFunctionDefinition => ({ help: '', }, }, - fn(input, args) { + fn(input, args, handlers) { const visConfig = args.visConfig && JSON.parse(args.visConfig); const convertedData = tableVisResponseHandler(input, visConfig.dimensions); + if (handlers?.inspectorAdapters?.tables) { + handlers.inspectorAdapters.tables.logDatatable('default', input); + } return { type: 'render', as: 'table_vis', diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts index ff59572e0817d..3ed203dd22ef4 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts @@ -95,7 +95,7 @@ export const createTagCloudFn = (): TagcloudExpressionFunctionDefinition => ({ }), }, }, - fn(input, args) { + fn(input, args, handlers) { const visParams = { scale: args.scale, orientation: args.orientation, @@ -109,6 +109,9 @@ export const createTagCloudFn = (): TagcloudExpressionFunctionDefinition => ({ visParams.bucket = args.bucket; } + if (handlers?.inspectorAdapters?.tables) { + handlers.inspectorAdapters.tables.logDatatable('default', input); + } return { type: 'render', as: 'tagloud_vis', diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap index 7ded8e2254aa9..fceb9c3fdb819 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/area_decorator.js should render and match a snapshot 1`] = ` - should render and match a snapshot 1`] = ` - = { - name: 'area', - title: i18n.translate('visTypeVislib.area.areaTitle', { defaultMessage: 'Area' }), - icon: 'visArea', - description: i18n.translate('visTypeVislib.area.areaDescription', { - defaultMessage: 'Emphasize the data between an axis and a line.', - }), - getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + ...(xyVisTypes.area() as BaseVisTypeOptions), toExpressionAst, - visConfig: { - defaults: { - type: 'area', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.BOTTOM, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - }, - labels: { - show: true, - filter: true, - truncate: 100, - }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: AxisTypes.VALUE, - position: Positions.LEFT, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - mode: AxisModes.NORMAL, - }, - labels: { - show: true, - rotate: Rotates.HORIZONTAL, - filter: false, - truncate: 100, - }, - title: { - text: countLabel, - }, - }, - ], - seriesParams: [ - { - show: true, - type: ChartTypes.AREA, - mode: ChartModes.STACKED, - data: { - label: countLabel, - id: '1', - }, - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true, - interpolate: InterpolationModes.LINEAR, - valueAxis: 'ValueAxis-1', - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - times: [], - addTimeMarker: false, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: ThresholdLineStyles.FULL, - color: euiPaletteColorBlind()[9], - }, - labels: {}, - }, - }, - editorConfig: { - collections: getConfigCollections(), - optionTabs: getAreaOptionTabs(), - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeVislib.area.metricsTitle', { - defaultMessage: 'Y-axis', - }), - aggFilter: ['!geo_centroid', '!geo_bounds'], - min: 1, - defaults: [{ schema: 'metric', type: 'count' }], - }, - { - group: AggGroupNames.Metrics, - name: 'radius', - title: i18n.translate('visTypeVislib.area.radiusTitle', { - defaultMessage: 'Dot size', - }), - min: 0, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'], - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('visTypeVislib.area.segmentTitle', { - defaultMessage: 'X-axis', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('visTypeVislib.area.groupTitle', { - defaultMessage: 'Split series', - }), - min: 0, - max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeVislib.area.splitTitle', { - defaultMessage: 'Split chart', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - ]), - }, + visualization: undefined, }; diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx deleted file mode 100644 index 63881fea1ad88..0000000000000 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { mount, shallow } from 'enzyme'; - -import { IAggConfig, IAggType } from 'src/plugins/data/public'; -import MetricsAxisOptions from './index'; -import { BasicVislibParams, SeriesParam, ValueAxis } from '../../../types'; -import { ValidationVisOptionsProps } from '../../common'; -import { Positions } from '../../../utils/collections'; -import { ValueAxesPanel } from './value_axes_panel'; -import { CategoryAxisPanel } from './category_axis_panel'; -import { ChartTypes } from '../../../utils/collections'; -import { defaultValueAxisId, valueAxis, seriesParam, categoryAxis } from './mocks'; - -jest.mock('./series_panel', () => ({ - SeriesPanel: () => 'SeriesPanel', -})); -jest.mock('./category_axis_panel', () => ({ - CategoryAxisPanel: () => 'CategoryAxisPanel', -})); -jest.mock('./value_axes_panel', () => ({ - ValueAxesPanel: () => 'ValueAxesPanel', -})); - -const SERIES_PARAMS = 'seriesParams'; -const VALUE_AXES = 'valueAxes'; - -const aggCount: IAggConfig = { - id: '1', - type: { name: 'count' }, - makeLabel: () => 'Count', -} as IAggConfig; - -const aggAverage: IAggConfig = { - id: '2', - type: { name: 'average' } as IAggType, - makeLabel: () => 'Average', -} as IAggConfig; - -const createAggs = (aggs: any[]) => ({ - aggs, - bySchemaName: () => aggs, -}); - -describe('MetricsAxisOptions component', () => { - let setValue: jest.Mock; - let defaultProps: ValidationVisOptionsProps; - let axis: ValueAxis; - let axisRight: ValueAxis; - let chart: SeriesParam; - - beforeEach(() => { - setValue = jest.fn(); - - axis = { - ...valueAxis, - name: 'LeftAxis-1', - position: Positions.LEFT, - }; - axisRight = { - ...valueAxis, - id: 'ValueAxis-2', - name: 'RightAxis-1', - position: Positions.RIGHT, - }; - chart = { - ...seriesParam, - type: ChartTypes.AREA, - }; - - defaultProps = { - aggs: createAggs([aggCount]), - isTabSelected: true, - vis: { - type: { - type: ChartTypes.AREA, - schemas: { metrics: [{ name: 'metric' }] }, - }, - setState: jest.fn(), - serialize: jest.fn(), - }, - stateParams: { - valueAxes: [axis], - seriesParams: [chart], - categoryAxes: [categoryAxis], - grid: { valueAxis: defaultValueAxisId }, - }, - setValue, - } as any; - }); - - it('should init with the default set of props', () => { - const comp = shallow(); - - expect(comp).toMatchSnapshot(); - }); - - describe('useEffect', () => { - it('should update series when new agg is added', () => { - const comp = mount(); - comp.setProps({ - aggs: createAggs([aggCount, aggAverage]), - }); - - const updatedSeries = [chart, { ...chart, data: { id: '2', label: aggAverage.makeLabel() } }]; - expect(setValue).toHaveBeenLastCalledWith(SERIES_PARAMS, updatedSeries); - }); - - it('should update series when new agg label is changed', () => { - const comp = mount(); - const agg = { id: aggCount.id, makeLabel: () => 'New label' }; - comp.setProps({ - aggs: createAggs([agg]), - }); - - const updatedSeries = [{ ...chart, data: { id: agg.id, label: agg.makeLabel() } }]; - expect(setValue).toHaveBeenCalledWith(SERIES_PARAMS, updatedSeries); - }); - }); - - describe('updateAxisTitle', () => { - it('should not update the value axis title if custom title was set', () => { - defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; - const comp = mount(); - const newAgg = { - ...aggCount, - makeLabel: () => 'Custom label', - }; - comp.setProps({ - aggs: createAggs([newAgg]), - }); - const updatedValues = [{ ...axis, title: { text: newAgg.makeLabel() } }]; - expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES, updatedValues); - }); - - it('should set the custom title to match the value axis label when only one agg exists for that axis', () => { - const comp = mount(); - const agg = { - id: aggCount.id, - params: { customLabel: 'Custom label' }, - makeLabel: () => 'Custom label', - }; - comp.setProps({ - aggs: createAggs([agg]), - }); - - const updatedSeriesParams = [{ ...chart, data: { ...chart.data, label: agg.makeLabel() } }]; - const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; - - expect(setValue).toHaveBeenCalledTimes(5); - expect(setValue).toHaveBeenNthCalledWith(3, SERIES_PARAMS, updatedSeriesParams); - expect(setValue).toHaveBeenNthCalledWith(5, SERIES_PARAMS, updatedSeriesParams); - expect(setValue).toHaveBeenNthCalledWith(4, VALUE_AXES, updatedValues); - }); - - it('should not set the custom title to match the value axis label when more than one agg exists for that axis', () => { - const comp = mount(); - const agg = { id: aggCount.id, makeLabel: () => 'Custom label' }; - comp.setProps({ - aggs: createAggs([agg, aggAverage]), - stateParams: { - ...defaultProps.stateParams, - seriesParams: [chart, chart], - }, - }); - - expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); - }); - - it('should not overwrite the custom title with the value axis label if the custom title has been changed', () => { - defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; - const comp = mount(); - const agg = { - id: aggCount.id, - params: { customLabel: 'Custom label' }, - makeLabel: () => 'Custom label', - }; - comp.setProps({ - aggs: createAggs([agg]), - }); - - expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); - }); - }); - - it('should add value axis', () => { - const comp = shallow(); - comp.find(ValueAxesPanel).prop('addValueAxis')(); - - expect(setValue).toHaveBeenCalledWith(VALUE_AXES, [axis, axisRight]); - }); - - describe('removeValueAxis', () => { - beforeEach(() => { - defaultProps.stateParams.valueAxes = [axis, axisRight]; - }); - - it('should remove value axis', () => { - const comp = shallow(); - comp.find(ValueAxesPanel).prop('removeValueAxis')(axis); - - expect(setValue).toHaveBeenCalledWith(VALUE_AXES, [axisRight]); - }); - - it('should update seriesParams "valueAxis" prop', () => { - const updatedSeriesParam = { ...chart, valueAxis: 'ValueAxis-2' }; - const comp = shallow(); - comp.find(ValueAxesPanel).prop('removeValueAxis')(axis); - - expect(setValue).toHaveBeenCalledWith(SERIES_PARAMS, [updatedSeriesParam]); - }); - - it('should reset grid "valueAxis" prop', () => { - const updatedGrid = { valueAxis: undefined }; - defaultProps.stateParams.seriesParams[0].valueAxis = 'ValueAxis-2'; - const comp = shallow(); - comp.find(ValueAxesPanel).prop('removeValueAxis')(axis); - - expect(setValue).toHaveBeenCalledWith('grid', updatedGrid); - }); - }); - - it('should update axis value when when category position chnaged', () => { - const comp = shallow(); - comp.find(CategoryAxisPanel).prop('onPositionChanged')(Positions.LEFT); - - const updatedValues = [{ ...axis, name: 'BottomAxis-1', position: Positions.BOTTOM }]; - expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); - }); -}); diff --git a/src/plugins/vis_type_vislib/public/editor/collections.ts b/src/plugins/vis_type_vislib/public/editor/collections.ts new file mode 100644 index 0000000000000..f1caa0754b0b3 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/editor/collections.ts @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; + +import { colorSchemas } from '../../../charts/public'; +import { getPositions, getScaleTypes } from '../../../vis_type_xy/public'; + +import { Alignment, GaugeType } from '../types'; + +export const getGaugeTypes = () => [ + { + text: i18n.translate('visTypeVislib.gauge.gaugeTypes.arcText', { + defaultMessage: 'Arc', + }), + value: GaugeType.Arc, + }, + { + text: i18n.translate('visTypeVislib.gauge.gaugeTypes.circleText', { + defaultMessage: 'Circle', + }), + value: GaugeType.Circle, + }, +]; + +export const getAlignments = () => [ + { + text: i18n.translate('visTypeVislib.gauge.alignmentAutomaticTitle', { + defaultMessage: 'Automatic', + }), + value: Alignment.Automatic, + }, + { + text: i18n.translate('visTypeVislib.gauge.alignmentHorizontalTitle', { + defaultMessage: 'Horizontal', + }), + value: Alignment.Horizontal, + }, + { + text: i18n.translate('visTypeVislib.gauge.alignmentVerticalTitle', { + defaultMessage: 'Vertical', + }), + value: Alignment.Vertical, + }, +]; + +export const getGaugeCollections = () => ({ + gaugeTypes: getGaugeTypes(), + alignments: getAlignments(), + colorSchemas, +}); + +export const getHeatmapCollections = () => ({ + legendPositions: getPositions(), + scales: getScaleTypes(), + colorSchemas, +}); diff --git a/src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx b/src/plugins/vis_type_vislib/public/editor/components/gauge/index.tsx similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx rename to src/plugins/vis_type_vislib/public/editor/components/gauge/index.tsx diff --git a/src/plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx b/src/plugins/vis_type_vislib/public/editor/components/gauge/labels_panel.tsx similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx rename to src/plugins/vis_type_vislib/public/editor/components/gauge/labels_panel.tsx diff --git a/src/plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx b/src/plugins/vis_type_vislib/public/editor/components/gauge/ranges_panel.tsx similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx rename to src/plugins/vis_type_vislib/public/editor/components/gauge/ranges_panel.tsx diff --git a/src/plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx b/src/plugins/vis_type_vislib/public/editor/components/gauge/style_panel.tsx similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx rename to src/plugins/vis_type_vislib/public/editor/components/gauge/style_panel.tsx diff --git a/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx b/src/plugins/vis_type_vislib/public/editor/components/heatmap/index.tsx similarity index 97% rename from src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx rename to src/plugins/vis_type_vislib/public/editor/components/heatmap/index.tsx index 312cf60fda6b0..f5b853accb08e 100644 --- a/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/heatmap/index.tsx @@ -23,7 +23,8 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { VisOptionsProps } from '../../../../../vis_default_editor/public'; +import { ValueAxis } from '../../../../../vis_type_xy/public'; import { BasicOptions, ColorRanges, @@ -34,8 +35,8 @@ import { SetColorSchemaOptionsValue, SetColorRangeValue, } from '../../../../../charts/public'; + import { HeatmapVisParams } from '../../../heatmap'; -import { ValueAxis } from '../../../types'; import { LabelsPanel } from './labels_panel'; function HeatmapOptions(props: VisOptionsProps) { diff --git a/src/plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx b/src/plugins/vis_type_vislib/public/editor/components/heatmap/labels_panel.tsx similarity index 96% rename from src/plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx rename to src/plugins/vis_type_vislib/public/editor/components/heatmap/labels_panel.tsx index 4998a8fd02521..8ec06ea50ec12 100644 --- a/src/plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/heatmap/labels_panel.tsx @@ -23,10 +23,11 @@ import { EuiColorPicker, EuiFormRow, EuiPanel, EuiSpacer, EuiTitle } from '@elas import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { ValueAxis } from '../../../types'; -import { HeatmapVisParams } from '../../../heatmap'; +import { VisOptionsProps } from '../../../../../vis_default_editor/public'; import { SwitchOption } from '../../../../../charts/public'; +import { ValueAxis } from '../../../../../vis_type_xy/public'; + +import { HeatmapVisParams } from '../../../heatmap'; const VERTICAL_ROTATION = 270; diff --git a/src/plugins/vis_type_vislib/public/components/options/index.tsx b/src/plugins/vis_type_vislib/public/editor/components/index.tsx similarity index 71% rename from src/plugins/vis_type_vislib/public/components/options/index.tsx rename to src/plugins/vis_type_vislib/public/editor/components/index.tsx index 18c41bf289b11..ed8c8239a07b6 100644 --- a/src/plugins/vis_type_vislib/public/components/options/index.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/index.tsx @@ -19,18 +19,15 @@ import React, { lazy } from 'react'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { ValidationVisOptionsProps } from '../common'; +import { VisOptionsProps } from '../../../../vis_default_editor/public'; + import { GaugeVisParams } from '../../gauge'; import { PieVisParams } from '../../pie'; -import { BasicVislibParams } from '../../types'; import { HeatmapVisParams } from '../../heatmap'; const GaugeOptionsLazy = lazy(() => import('./gauge')); const PieOptionsLazy = lazy(() => import('./pie')); -const PointSeriesOptionsLazy = lazy(() => import('./point_series')); const HeatmapOptionsLazy = lazy(() => import('./heatmap')); -const MetricsAxisOptionsLazy = lazy(() => import('./metrics_axes')); export const GaugeOptions = (props: VisOptionsProps) => ( @@ -38,14 +35,6 @@ export const GaugeOptions = (props: VisOptionsProps) => ( export const PieOptions = (props: VisOptionsProps) => ; -export const PointSeriesOptions = (props: ValidationVisOptionsProps) => ( - -); - export const HeatmapOptions = (props: VisOptionsProps) => ( ); - -export const MetricsAxisOptions = (props: ValidationVisOptionsProps) => ( - -); diff --git a/src/plugins/vis_type_vislib/public/components/options/pie.tsx b/src/plugins/vis_type_vislib/public/editor/components/pie.tsx similarity index 96% rename from src/plugins/vis_type_vislib/public/components/options/pie.tsx rename to src/plugins/vis_type_vislib/public/editor/components/pie.tsx index 30828bfc6a3ea..1c3aa501b4d00 100644 --- a/src/plugins/vis_type_vislib/public/components/options/pie.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/pie.tsx @@ -22,9 +22,10 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { TruncateLabelsOption } from '../common'; +import { VisOptionsProps } from '../../../../vis_default_editor/public'; import { BasicOptions, SwitchOption } from '../../../../charts/public'; +import { TruncateLabelsOption } from '../../../../vis_type_xy/public'; + import { PieVisParams } from '../../pie'; function PieOptions(props: VisOptionsProps) { diff --git a/src/plugins/inspector/common/adapters/data/formatted_data.ts b/src/plugins/vis_type_vislib/public/editor/index.ts similarity index 88% rename from src/plugins/inspector/common/adapters/data/formatted_data.ts rename to src/plugins/vis_type_vislib/public/editor/index.ts index 08c956f27d011..2a73b1ad8fa68 100644 --- a/src/plugins/inspector/common/adapters/data/formatted_data.ts +++ b/src/plugins/vis_type_vislib/public/editor/index.ts @@ -17,6 +17,5 @@ * under the License. */ -export class FormattedData { - constructor(public readonly raw: any, public readonly formatted: any) {} -} +export * from './collections'; +export * from './components'; diff --git a/src/plugins/vis_type_vislib/public/gauge.ts b/src/plugins/vis_type_vislib/public/gauge.ts index 7cc8e03c9e4c2..de32ee17a21bf 100644 --- a/src/plugins/vis_type_vislib/public/gauge.ts +++ b/src/plugins/vis_type_vislib/public/gauge.ts @@ -19,24 +19,25 @@ import { i18n } from '@kbn/i18n'; +import { ColorMode, ColorSchemas, ColorSchemaParams, Labels, Style } from '../../charts/public'; import { RangeValues, Schemas } from '../../vis_default_editor/public'; import { AggGroupNames } from '../../data/public'; -import { GaugeOptions } from './components/options'; -import { getGaugeCollections, Alignments, GaugeTypes } from './utils/collections'; -import { ColorModes, ColorSchemas, ColorSchemaParams, Labels, Style } from '../../charts/public'; -import { toExpressionAst } from './to_ast'; import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; -import { BasicVislibParams } from './types'; + +import { Alignment, GaugeType, BasicVislibParams, VislibChartType } from './types'; +import { getGaugeCollections } from './editor'; +import { toExpressionAst } from './to_ast'; +import { GaugeOptions } from './editor/components'; export interface Gauge extends ColorSchemaParams { backStyle: 'Full'; gaugeStyle: 'Full'; orientation: 'vertical'; type: 'meter'; - alignment: Alignments; + alignment: Alignment; colorsRange: RangeValues[]; extendRange: boolean; - gaugeType: GaugeTypes; + gaugeType: GaugeType; labels: Labels; percentageMode: boolean; outline?: boolean; @@ -67,20 +68,20 @@ export const gaugeVisTypeDefinition: BaseVisTypeOptions = { toExpressionAst, visConfig: { defaults: { - type: 'gauge', + type: VislibChartType.Gauge, addTooltip: true, addLegend: true, isDisplayWarning: false, gauge: { - alignment: Alignments.AUTOMATIC, + alignment: Alignment.Automatic, extendRange: true, percentageMode: false, - gaugeType: GaugeTypes.ARC, + gaugeType: GaugeType.Arc, gaugeStyle: 'Full', backStyle: 'Full', orientation: 'vertical', colorSchema: ColorSchemas.GreenToRed, - gaugeColorMode: ColorModes.LABELS, + gaugeColorMode: ColorMode.Labels, colorsRange: [ { from: 0, to: 50 }, { from: 50, to: 75 }, diff --git a/src/plugins/vis_type_vislib/public/goal.ts b/src/plugins/vis_type_vislib/public/goal.ts index 46878ca82e45a..56361421261fc 100644 --- a/src/plugins/vis_type_vislib/public/goal.ts +++ b/src/plugins/vis_type_vislib/public/goal.ts @@ -19,14 +19,14 @@ import { i18n } from '@kbn/i18n'; -import { GaugeOptions } from './components/options'; -import { getGaugeCollections, GaugeTypes } from './utils/collections'; -import { ColorModes, ColorSchemas } from '../../charts/public'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; -import { toExpressionAst } from './to_ast'; +import { ColorMode, ColorSchemas } from '../../charts/public'; import { BaseVisTypeOptions } from '../../visualizations/public'; -import { BasicVislibParams } from './types'; + +import { getGaugeCollections, GaugeOptions } from './editor'; +import { toExpressionAst } from './to_ast'; +import { GaugeType, BasicVislibParams } from './types'; export const goalVisTypeDefinition: BaseVisTypeOptions = { name: 'goal', @@ -46,13 +46,13 @@ export const goalVisTypeDefinition: BaseVisTypeOptions = { verticalSplit: false, autoExtend: false, percentageMode: true, - gaugeType: GaugeTypes.ARC, + gaugeType: GaugeType.Arc, gaugeStyle: 'Full', backStyle: 'Full', orientation: 'vertical', useRanges: false, colorSchema: ColorSchemas.GreenToRed, - gaugeColorMode: ColorModes.NONE, + gaugeColorMode: ColorMode.None, colorsRange: [{ from: 0, to: 10000 }], invertColors: false, labels: { diff --git a/src/plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts index c408ac140dd46..4a815fd8b2c73 100644 --- a/src/plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -18,15 +18,17 @@ */ import { i18n } from '@kbn/i18n'; +import { Position } from '@elastic/charts'; import { RangeValues, Schemas } from '../../vis_default_editor/public'; import { AggGroupNames } from '../../data/public'; -import { AxisTypes, getHeatmapCollections, Positions, ScaleTypes } from './utils/collections'; -import { HeatmapOptions } from './components/options'; -import { TimeMarker } from './vislib/visualizations/time_marker'; -import { BasicVislibParams, CommonVislibParams, ValueAxis } from './types'; import { ColorSchemas, ColorSchemaParams } from '../../charts/public'; -import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { VIS_EVENT_TO_TRIGGER, BaseVisTypeOptions } from '../../visualizations/public'; +import { ValueAxis, ScaleType, AxisType } from '../../vis_type_xy/public'; + +import { HeatmapOptions, getHeatmapCollections } from './editor'; +import { TimeMarker } from './vislib/visualizations/time_marker'; +import { CommonVislibParams, BasicVislibParams, VislibChartType } from './types'; import { toExpressionAst } from './to_ast'; export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaParams { @@ -48,15 +50,15 @@ export const heatmapVisTypeDefinition: BaseVisTypeOptions = { description: i18n.translate('visTypeVislib.heatmap.heatmapDescription', { defaultMessage: 'Shade data in cells in a matrix.', }), - getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter], toExpressionAst, + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter], visConfig: { defaults: { - type: 'heatmap', + type: VislibChartType.Heatmap, addTooltip: true, addLegend: true, enableHover: false, - legendPosition: Positions.RIGHT, + legendPosition: Position.Right, times: [], colorsNumber: 4, colorSchema: ColorSchemas.Greens, @@ -68,9 +70,9 @@ export const heatmapVisTypeDefinition: BaseVisTypeOptions = { { show: false, id: 'ValueAxis-1', - type: AxisTypes.VALUE, + type: AxisType.Value, scale: { - type: ScaleTypes.LINEAR, + type: ScaleType.Linear, defaultYExtents: false, }, labels: { diff --git a/src/plugins/vis_type_vislib/public/histogram.ts b/src/plugins/vis_type_vislib/public/histogram.ts index de4855ba9aa2b..7424ef5c4df27 100644 --- a/src/plugins/vis_type_vislib/public/histogram.ts +++ b/src/plugins/vis_type_vislib/public/histogram.ts @@ -17,174 +17,14 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { palettes } from '@elastic/eui/lib/services'; -// @ts-ignore -import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { xyVisTypes } from '../../vis_type_xy/public'; +import { BaseVisTypeOptions } from '../../visualizations/public'; -import { AggGroupNames } from '../../data/public'; -import { Schemas } from '../../vis_default_editor/public'; -import { - Positions, - ChartTypes, - ChartModes, - AxisTypes, - ScaleTypes, - AxisModes, - ThresholdLineStyles, - getConfigCollections, -} from './utils/collections'; -import { getAreaOptionTabs, countLabel } from './utils/common_config'; -import { Rotates } from '../../charts/public'; -import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; -import { BasicVislibParams } from './types'; import { toExpressionAst } from './to_ast'; +import { BasicVislibParams } from './types'; export const histogramVisTypeDefinition: BaseVisTypeOptions = { - name: 'histogram', - title: i18n.translate('visTypeVislib.histogram.histogramTitle', { - defaultMessage: 'Vertical bar', - }), - icon: 'visBarVertical', - description: i18n.translate('visTypeVislib.histogram.histogramDescription', { - defaultMessage: 'Present data in vertical bars on an axis.', - }), - getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + ...(xyVisTypes.histogram() as BaseVisTypeOptions), toExpressionAst, - visConfig: { - defaults: { - type: 'histogram', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.BOTTOM, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - }, - labels: { - show: true, - filter: true, - truncate: 100, - }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: AxisTypes.VALUE, - position: Positions.LEFT, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - mode: AxisModes.NORMAL, - }, - labels: { - show: true, - rotate: Rotates.HORIZONTAL, - filter: false, - truncate: 100, - }, - title: { - text: countLabel, - }, - }, - ], - seriesParams: [ - { - show: true, - type: ChartTypes.HISTOGRAM, - mode: ChartModes.STACKED, - data: { - label: countLabel, - id: '1', - }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true, - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - times: [], - addTimeMarker: false, - labels: { - show: false, - }, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: ThresholdLineStyles.FULL, - color: euiPaletteColorBlind()[9], - }, - }, - }, - editorConfig: { - collections: getConfigCollections(), - optionTabs: getAreaOptionTabs(), - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeVislib.histogram.metricTitle', { - defaultMessage: 'Y-axis', - }), - min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds'], - defaults: [{ schema: 'metric', type: 'count' }], - }, - { - group: AggGroupNames.Metrics, - name: 'radius', - title: i18n.translate('visTypeVislib.histogram.radiusTitle', { - defaultMessage: 'Dot size', - }), - min: 0, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'], - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('visTypeVislib.histogram.segmentTitle', { - defaultMessage: 'X-axis', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('visTypeVislib.histogram.groupTitle', { - defaultMessage: 'Split series', - }), - min: 0, - max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeVislib.histogram.splitTitle', { - defaultMessage: 'Split chart', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - ]), - }, + visualization: undefined, }; diff --git a/src/plugins/vis_type_vislib/public/horizontal_bar.ts b/src/plugins/vis_type_vislib/public/horizontal_bar.ts index 144e63224533b..9e919c66cb365 100644 --- a/src/plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/plugins/vis_type_vislib/public/horizontal_bar.ts @@ -17,171 +17,14 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { palettes, euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { xyVisTypes } from '../../vis_type_xy/public'; +import { BaseVisTypeOptions } from '../../visualizations/public'; -import { AggGroupNames } from '../../data/public'; -import { Schemas } from '../../vis_default_editor/public'; -import { - Positions, - ChartTypes, - ChartModes, - AxisTypes, - ScaleTypes, - AxisModes, - ThresholdLineStyles, - getConfigCollections, -} from './utils/collections'; -import { getAreaOptionTabs, countLabel } from './utils/common_config'; -import { Rotates } from '../../charts/public'; -import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; -import { BasicVislibParams } from './types'; import { toExpressionAst } from './to_ast'; +import { BasicVislibParams } from './types'; export const horizontalBarVisTypeDefinition: BaseVisTypeOptions = { - name: 'horizontal_bar', - title: i18n.translate('visTypeVislib.horizontalBar.horizontalBarTitle', { - defaultMessage: 'Horizontal bar', - }), - icon: 'visBarHorizontal', - description: i18n.translate('visTypeVislib.horizontalBar.horizontalBarDescription', { - defaultMessage: 'Present data in horizontal bars on an axis.', - }), - getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + ...(xyVisTypes.horizontalBar() as BaseVisTypeOptions), toExpressionAst, - visConfig: { - defaults: { - type: 'histogram', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.LEFT, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - }, - labels: { - show: true, - rotate: Rotates.HORIZONTAL, - filter: false, - truncate: 200, - }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: AxisTypes.VALUE, - position: Positions.BOTTOM, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - mode: AxisModes.NORMAL, - }, - labels: { - show: true, - rotate: Rotates.ANGLED, - filter: true, - truncate: 100, - }, - title: { - text: countLabel, - }, - }, - ], - seriesParams: [ - { - show: true, - type: ChartTypes.HISTOGRAM, - mode: ChartModes.NORMAL, - data: { - label: countLabel, - id: '1', - }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true, - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - times: [], - addTimeMarker: false, - labels: {}, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: ThresholdLineStyles.FULL, - color: euiPaletteColorBlind()[9], - }, - }, - }, - editorConfig: { - collections: getConfigCollections(), - optionTabs: getAreaOptionTabs(), - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeVislib.horizontalBar.metricTitle', { - defaultMessage: 'Y-axis', - }), - min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds'], - defaults: [{ schema: 'metric', type: 'count' }], - }, - { - group: AggGroupNames.Metrics, - name: 'radius', - title: i18n.translate('visTypeVislib.horizontalBar.radiusTitle', { - defaultMessage: 'Dot size', - }), - min: 0, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'], - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('visTypeVislib.horizontalBar.segmentTitle', { - defaultMessage: 'X-axis', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('visTypeVislib.horizontalBar.groupTitle', { - defaultMessage: 'Split series', - }), - min: 0, - max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeVislib.horizontalBar.splitTitle', { - defaultMessage: 'Split chart', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - ]), - }, + visualization: undefined, }; diff --git a/src/plugins/vis_type_vislib/public/line.ts b/src/plugins/vis_type_vislib/public/line.ts index ffa40c8c29980..15f20edd359dd 100644 --- a/src/plugins/vis_type_vislib/public/line.ts +++ b/src/plugins/vis_type_vislib/public/line.ts @@ -17,164 +17,14 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { palettes, euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { xyVisTypes } from '../../vis_type_xy/public'; +import { BaseVisTypeOptions } from '../../visualizations/public'; -import { AggGroupNames } from '../../data/public'; -import { Schemas } from '../../vis_default_editor/public'; -import { - Positions, - ChartTypes, - ChartModes, - AxisTypes, - ScaleTypes, - AxisModes, - ThresholdLineStyles, - InterpolationModes, - getConfigCollections, -} from './utils/collections'; -import { getAreaOptionTabs, countLabel } from './utils/common_config'; -import { Rotates } from '../../charts/public'; -import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; import { toExpressionAst } from './to_ast'; import { BasicVislibParams } from './types'; export const lineVisTypeDefinition: BaseVisTypeOptions = { - name: 'line', - title: i18n.translate('visTypeVislib.line.lineTitle', { defaultMessage: 'Line' }), - icon: 'visLine', - description: i18n.translate('visTypeVislib.line.lineDescription', { - defaultMessage: 'Display data as a series of points.', - }), - getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + ...(xyVisTypes.line() as BaseVisTypeOptions), toExpressionAst, - visConfig: { - defaults: { - type: 'line', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.BOTTOM, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - }, - labels: { - show: true, - filter: true, - truncate: 100, - }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: AxisTypes.VALUE, - position: Positions.LEFT, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - mode: AxisModes.NORMAL, - }, - labels: { - show: true, - rotate: Rotates.HORIZONTAL, - filter: false, - truncate: 100, - }, - title: { - text: countLabel, - }, - }, - ], - seriesParams: [ - { - show: true, - type: ChartTypes.LINE, - mode: ChartModes.NORMAL, - data: { - label: countLabel, - id: '1', - }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - lineWidth: 2, - interpolate: InterpolationModes.LINEAR, - showCircles: true, - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - times: [], - addTimeMarker: false, - labels: {}, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: ThresholdLineStyles.FULL, - color: euiPaletteColorBlind()[9], - }, - }, - }, - editorConfig: { - collections: getConfigCollections(), - optionTabs: getAreaOptionTabs(), - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeVislib.line.metricTitle', { defaultMessage: 'Y-axis' }), - min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds'], - defaults: [{ schema: 'metric', type: 'count' }], - }, - { - group: AggGroupNames.Metrics, - name: 'radius', - title: i18n.translate('visTypeVislib.line.radiusTitle', { defaultMessage: 'Dot size' }), - min: 0, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits'], - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('visTypeVislib.line.segmentTitle', { defaultMessage: 'X-axis' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('visTypeVislib.line.groupTitle', { - defaultMessage: 'Split series', - }), - min: 0, - max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeVislib.line.splitTitle', { - defaultMessage: 'Split chart', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - ]), - }, + visualization: undefined, }; diff --git a/src/plugins/vis_type_vislib/public/pie.ts b/src/plugins/vis_type_vislib/public/pie.ts index 41b271054d59f..aa5a3ceaaba98 100644 --- a/src/plugins/vis_type_vislib/public/pie.ts +++ b/src/plugins/vis_type_vislib/public/pie.ts @@ -18,13 +18,15 @@ */ import { i18n } from '@kbn/i18n'; +import { Position } from '@elastic/charts'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; -import { PieOptions } from './components/options'; -import { getPositions, Positions } from './utils/collections'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; +import { getPositions } from '../../vis_type_xy/public'; + import { CommonVislibParams } from './types'; -import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { PieOptions } from './editor'; import { toExpressionAst } from './to_ast_pie'; export interface PieVisParams extends CommonVislibParams { @@ -52,7 +54,7 @@ export const pieVisTypeDefinition: BaseVisTypeOptions = { type: 'pie', addTooltip: true, addLegend: true, - legendPosition: Positions.RIGHT, + legendPosition: Position.Right, isDonut: true, labels: { show: false, diff --git a/src/plugins/vis_type_vislib/public/pie_fn.ts b/src/plugins/vis_type_vislib/public/pie_fn.ts index c9da9e9bd9fab..8b16d8eb0982f 100644 --- a/src/plugins/vis_type_vislib/public/pie_fn.ts +++ b/src/plugins/vis_type_vislib/public/pie_fn.ts @@ -24,6 +24,7 @@ import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressio // @ts-ignore import { vislibSlicesResponseHandler } from './vislib/response_handler'; import { PieVisParams } from './pie'; +import { VislibChartType } from './types'; import { vislibVisName } from './vis_type_vislib_vis_fn'; export const vislibPieName = 'vislib_pie_vis'; @@ -32,9 +33,9 @@ interface Arguments { visConfig: string; } -interface RenderValue { +export interface PieRenderValue { + visType: Extract; visData: unknown; - visType: string; visConfig: PieVisParams; } @@ -42,7 +43,7 @@ export type VisTypeVislibPieExpressionFunctionDefinition = ExpressionFunctionDef typeof vislibPieName, Datatable, Arguments, - Render + Render >; export const createPieVisFn = (): VisTypeVislibPieExpressionFunctionDefinition => ({ @@ -59,17 +60,21 @@ export const createPieVisFn = (): VisTypeVislibPieExpressionFunctionDefinition = help: 'vislib pie vis config', }, }, - fn(input, args) { + fn(input, args, handlers) { const visConfig = JSON.parse(args.visConfig) as PieVisParams; const visData = vislibSlicesResponseHandler(input, visConfig.dimensions); + if (handlers?.inspectorAdapters?.tables) { + handlers.inspectorAdapters.tables.logDatatable('default', input); + } + return { type: 'render', as: vislibVisName, value: { visData, visConfig, - visType: 'pie', + visType: VislibChartType.Pie, }, }; }, diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts index f183042fd5201..bef8ad26fb12c 100644 --- a/src/plugins/vis_type_vislib/public/plugin.ts +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -19,25 +19,28 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; -import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; -import { BaseVisTypeOptions, VisualizationsSetup } from '../../visualizations/public'; -import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; -import { createPieVisFn } from './pie_fn'; -import { visLibVisTypeDefinitions, pieVisTypeDefinition } from './vis_type_vislib_vis_types'; +import { VisualizationsSetup } from '../../visualizations/public'; import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; +import { CHARTS_LIBRARY } from '../../vis_type_xy/public'; + +import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; +import { createPieVisFn } from './pie_fn'; +import { + convertedTypeDefinitions, + pieVisTypeDefinition, + visLibVisTypeDefinitions, +} from './vis_type_vislib_vis_types'; import { setFormatService, setDataActions } from './services'; import { getVislibVisRenderer } from './vis_renderer'; -import { BasicVislibParams } from './types'; /** @internal */ export interface VisTypeVislibPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; - visTypeXy?: VisTypeXyPluginSetup; } /** @internal */ @@ -56,23 +59,21 @@ export class VisTypeVislibPlugin public async setup( core: VisTypeVislibCoreSetup, - { expressions, visualizations, charts, visTypeXy }: VisTypeVislibPluginSetupDependencies + { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies ) { - // if visTypeXy plugin is disabled it's config will be undefined - if (!visTypeXy) { - const convertedTypes: Array> = []; - const convertedFns: any[] = []; - - // Register legacy vislib types that have been converted - convertedFns.forEach(expressions.registerFunction); - convertedTypes.forEach(visualizations.createBaseVisualization); + if (core.uiSettings.get(CHARTS_LIBRARY)) { + // Register only non-replaced vis types + convertedTypeDefinitions.forEach(visualizations.createBaseVisualization); + visualizations.createBaseVisualization(pieVisTypeDefinition); + expressions.registerRenderer(getVislibVisRenderer(core, charts)); + [createVisTypeVislibVisFn(), createPieVisFn()].forEach(expressions.registerFunction); + } else { + // Register all vis types + visLibVisTypeDefinitions.forEach(visualizations.createBaseVisualization); + visualizations.createBaseVisualization(pieVisTypeDefinition); expressions.registerRenderer(getVislibVisRenderer(core, charts)); + [createVisTypeVislibVisFn(), createPieVisFn()].forEach(expressions.registerFunction); } - // Register non-converted types - visLibVisTypeDefinitions.forEach(visualizations.createBaseVisualization); - visualizations.createBaseVisualization(pieVisTypeDefinition); - expressions.registerRenderer(getVislibVisRenderer(core, charts)); - [createVisTypeVislibVisFn(), createPieVisFn()].forEach(expressions.registerFunction); } public start(core: CoreStart, { data }: VisTypeVislibPluginStartDependencies) { diff --git a/src/plugins/vis_type_vislib/public/to_ast.ts b/src/plugins/vis_type_vislib/public/to_ast.ts index 3a05410ff006b..30ed2e4ca1bb4 100644 --- a/src/plugins/vis_type_vislib/public/to_ast.ts +++ b/src/plugins/vis_type_vislib/public/to_ast.ts @@ -21,14 +21,11 @@ import moment from 'moment'; import { VisToExpressionAst, getVisSchemas } from '../../visualizations/public'; import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import type { Dimensions, DateHistogramParams, HistogramParams } from '../../vis_type_xy/public'; +import { BUCKET_TYPES } from '../../data/public'; import { vislibVisName, VisTypeVislibExpressionFunctionDefinition } from './vis_type_vislib_vis_fn'; -import { BasicVislibParams } from './types'; -import { - DateHistogramParams, - Dimensions, - HistogramParams, -} from './vislib/helpers/point_series/point_series'; +import { BasicVislibParams, VislibChartType } from './types'; import { getEsaggsFn } from './to_ast_esaggs'; export const toExpressionAst: VisToExpressionAst = async (vis, params) => { @@ -47,7 +44,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis if (dimensions.x) { const xAgg = responseAggs[dimensions.x.accessor] as any; - if (xAgg.type.name === 'date_histogram') { + if (xAgg.type.name === BUCKET_TYPES.DATE_HISTOGRAM) { (dimensions.x.params as DateHistogramParams).date = true; const { esUnit, esValue } = xAgg.buckets.getInterval(); (dimensions.x.params as DateHistogramParams).intervalESUnit = esUnit; @@ -57,7 +54,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis .asMilliseconds(); (dimensions.x.params as DateHistogramParams).format = xAgg.buckets.getScaledDateFormat(); (dimensions.x.params as DateHistogramParams).bounds = xAgg.buckets.getBounds(); - } else if (xAgg.type.name === 'histogram') { + } else if (xAgg.type.name === BUCKET_TYPES.HISTOGRAM) { const intervalParam = xAgg.type.paramByName('interval'); const output = { params: {} as any }; await intervalParam.modifyAggConfigOnSearchRequestStart(xAgg, vis.data.searchSource, { @@ -88,15 +85,15 @@ export const toExpressionAst: VisToExpressionAst = async (vis visConfig.dimensions = dimensions; - const visTypeXy = buildExpressionFunction( + const visTypeVislib = buildExpressionFunction( vislibVisName, { - type: vis.type.name, + type: vis.type.name as Exclude, visConfig: JSON.stringify(visConfig), } ); - const ast = buildExpression([getEsaggsFn(vis), visTypeXy]); + const ast = buildExpression([getEsaggsFn(vis), visTypeVislib]); return ast.toAst(); }; diff --git a/src/plugins/vis_type_vislib/public/to_ast_esaggs.ts b/src/plugins/vis_type_vislib/public/to_ast_esaggs.ts index 2835e5cc1c255..d23f1ab3626d3 100644 --- a/src/plugins/vis_type_vislib/public/to_ast_esaggs.ts +++ b/src/plugins/vis_type_vislib/public/to_ast_esaggs.ts @@ -29,6 +29,8 @@ import { BasicVislibParams } from './types'; /** * Get esaggs expressions function + * TODO: replace this with vis.data.aggs!.toExpressionAst(); + * https://github.com/elastic/kibana/issues/61768 * @param vis */ export function getEsaggsFn(vis: Vis | Vis) { diff --git a/src/plugins/vis_type_vislib/public/types.ts b/src/plugins/vis_type_vislib/public/types.ts index c0311edf76154..4f6c427669324 100644 --- a/src/plugins/vis_type_vislib/public/types.ts +++ b/src/plugins/vis_type_vislib/public/types.ts @@ -17,87 +17,71 @@ * under the License. */ -import { TimeMarker } from './vislib/visualizations/time_marker'; +import { $Values } from '@kbn/utility-types'; +import { Position } from '@elastic/charts'; + +import { Labels } from '../../charts/public'; import { - Positions, - ChartModes, - ChartTypes, - AxisModes, - AxisTypes, - InterpolationModes, - ScaleTypes, - ThresholdLineStyles, -} from './utils/collections'; -import { Labels, Style } from '../../charts/public'; -import { Dimensions } from './vislib/helpers/point_series/point_series'; + CategoryAxis, + Dimensions, + Grid, + SeriesParam, + ThresholdLine, + ValueAxis, +} from '../../vis_type_xy/public'; +import { TimeMarker } from './vislib/visualizations/time_marker'; + +/** + * Gauge title alignment + */ +export const Alignment = Object.freeze({ + Automatic: 'automatic' as const, + Horizontal: 'horizontal' as const, + Vertical: 'vertical' as const, +}); +export type Alignment = $Values; + +export const GaugeType = Object.freeze({ + Arc: 'Arc' as const, + Circle: 'Circle' as const, +}); +export type GaugeType = $Values; + +export const VislibChartType = Object.freeze({ + Histogram: 'histogram' as const, + HorizontalBar: 'horizontal_bar' as const, + Line: 'line' as const, + Pie: 'pie' as const, + Area: 'area' as const, + PointSeries: 'point_series' as const, + Heatmap: 'heatmap' as const, + Gauge: 'gauge' as const, + Goal: 'goal' as const, + Metric: 'metric' as const, +}); +export type VislibChartType = $Values; export interface CommonVislibParams { addTooltip: boolean; addLegend: boolean; - legendPosition: Positions; + legendPosition: Position; dimensions: Dimensions; } -export interface Scale { - boundsMargin?: number | ''; - defaultYExtents?: boolean; - max?: number | null; - min?: number | null; - mode?: AxisModes; - setYExtents?: boolean; - type: ScaleTypes; -} - -interface ThresholdLine { - show: boolean; - value: number | null; - width: number | null; - style: ThresholdLineStyles; - color: string; -} - -export interface Axis { - id: string; - labels: Labels; - position: Positions; - scale: Scale; - show: boolean; - style: Style; - title: { text: string }; - type: AxisTypes; -} - -export interface ValueAxis extends Axis { - name: string; -} - -export interface SeriesParam { - data: { label: string; id: string }; - drawLinesBetweenPoints: boolean; - interpolate: InterpolationModes; - lineWidth?: number; - mode: ChartModes; - show: boolean; - showCircles: boolean; - type: ChartTypes; - valueAxis: string; -} - export interface BasicVislibParams extends CommonVislibParams { + type: VislibChartType; + addLegend: boolean; addTimeMarker: boolean; - categoryAxes: Axis[]; + categoryAxes: CategoryAxis[]; orderBucketsBySum?: boolean; labels: Labels; thresholdLine: ThresholdLine; valueAxes: ValueAxis[]; + grid: Grid; gauge?: { percentageMode: boolean; }; - grid: { - categoryLines: boolean; - valueAxis?: string; - }; seriesParams: SeriesParam[]; times: TimeMarker[]; - type: string; + radiusRatio: number; } diff --git a/src/plugins/vis_type_vislib/public/utils/collections.ts b/src/plugins/vis_type_vislib/public/utils/collections.ts deleted file mode 100644 index 44df4864bfd68..0000000000000 --- a/src/plugins/vis_type_vislib/public/utils/collections.ts +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { i18n } from '@kbn/i18n'; -import { $Values } from '@kbn/utility-types'; - -import { colorSchemas, Rotates } from '../../../charts/public'; - -export const Positions = Object.freeze({ - RIGHT: 'right' as 'right', - LEFT: 'left' as 'left', - TOP: 'top' as 'top', - BOTTOM: 'bottom' as 'bottom', -}); -export type Positions = $Values; - -const getPositions = () => [ - { - text: i18n.translate('visTypeVislib.legendPositions.topText', { - defaultMessage: 'Top', - }), - value: Positions.TOP, - }, - { - text: i18n.translate('visTypeVislib.legendPositions.leftText', { - defaultMessage: 'Left', - }), - value: Positions.LEFT, - }, - { - text: i18n.translate('visTypeVislib.legendPositions.rightText', { - defaultMessage: 'Right', - }), - value: Positions.RIGHT, - }, - { - text: i18n.translate('visTypeVislib.legendPositions.bottomText', { - defaultMessage: 'Bottom', - }), - value: Positions.BOTTOM, - }, -]; - -export const ChartTypes = Object.freeze({ - LINE: 'line' as 'line', - AREA: 'area' as 'area', - HISTOGRAM: 'histogram' as 'histogram', -}); -export type ChartTypes = $Values; - -const getChartTypes = () => [ - { - text: i18n.translate('visTypeVislib.chartTypes.lineText', { - defaultMessage: 'Line', - }), - value: ChartTypes.LINE, - }, - { - text: i18n.translate('visTypeVislib.chartTypes.areaText', { - defaultMessage: 'Area', - }), - value: ChartTypes.AREA, - }, - { - text: i18n.translate('visTypeVislib.chartTypes.barText', { - defaultMessage: 'Bar', - }), - value: ChartTypes.HISTOGRAM, - }, -]; - -export const ChartModes = Object.freeze({ - NORMAL: 'normal' as 'normal', - STACKED: 'stacked' as 'stacked', -}); -export type ChartModes = $Values; - -const getChartModes = () => [ - { - text: i18n.translate('visTypeVislib.chartModes.normalText', { - defaultMessage: 'Normal', - }), - value: ChartModes.NORMAL, - }, - { - text: i18n.translate('visTypeVislib.chartModes.stackedText', { - defaultMessage: 'Stacked', - }), - value: ChartModes.STACKED, - }, -]; - -export const InterpolationModes = Object.freeze({ - LINEAR: 'linear' as 'linear', - CARDINAL: 'cardinal' as 'cardinal', - STEP_AFTER: 'step-after' as 'step-after', -}); -export type InterpolationModes = $Values; - -const getInterpolationModes = () => [ - { - text: i18n.translate('visTypeVislib.interpolationModes.straightText', { - defaultMessage: 'Straight', - }), - value: InterpolationModes.LINEAR, - }, - { - text: i18n.translate('visTypeVislib.interpolationModes.smoothedText', { - defaultMessage: 'Smoothed', - }), - value: InterpolationModes.CARDINAL, - }, - { - text: i18n.translate('visTypeVislib.interpolationModes.steppedText', { - defaultMessage: 'Stepped', - }), - value: InterpolationModes.STEP_AFTER, - }, -]; - -export const AxisTypes = Object.freeze({ - CATEGORY: 'category' as 'category', - VALUE: 'value' as 'value', -}); -export type AxisTypes = $Values; - -export const ScaleTypes = Object.freeze({ - LINEAR: 'linear' as 'linear', - LOG: 'log' as 'log', - SQUARE_ROOT: 'square root' as 'square root', -}); -export type ScaleTypes = $Values; - -const getScaleTypes = () => [ - { - text: i18n.translate('visTypeVislib.scaleTypes.linearText', { - defaultMessage: 'Linear', - }), - value: ScaleTypes.LINEAR, - }, - { - text: i18n.translate('visTypeVislib.scaleTypes.logText', { - defaultMessage: 'Log', - }), - value: ScaleTypes.LOG, - }, - { - text: i18n.translate('visTypeVislib.scaleTypes.squareRootText', { - defaultMessage: 'Square root', - }), - value: ScaleTypes.SQUARE_ROOT, - }, -]; - -export const AxisModes = Object.freeze({ - NORMAL: 'normal' as 'normal', - PERCENTAGE: 'percentage' as 'percentage', - WIGGLE: 'wiggle' as 'wiggle', - SILHOUETTE: 'silhouette' as 'silhouette', -}); -export type AxisModes = $Values; - -const getAxisModes = () => [ - { - text: i18n.translate('visTypeVislib.axisModes.normalText', { - defaultMessage: 'Normal', - }), - value: AxisModes.NORMAL, - }, - { - text: i18n.translate('visTypeVislib.axisModes.percentageText', { - defaultMessage: 'Percentage', - }), - value: AxisModes.PERCENTAGE, - }, - { - text: i18n.translate('visTypeVislib.axisModes.wiggleText', { - defaultMessage: 'Wiggle', - }), - value: AxisModes.WIGGLE, - }, - { - text: i18n.translate('visTypeVislib.axisModes.silhouetteText', { - defaultMessage: 'Silhouette', - }), - value: AxisModes.SILHOUETTE, - }, -]; - -export const ThresholdLineStyles = Object.freeze({ - FULL: 'full' as 'full', - DASHED: 'dashed' as 'dashed', - DOT_DASHED: 'dot-dashed' as 'dot-dashed', -}); -export type ThresholdLineStyles = $Values; - -const getThresholdLineStyles = () => [ - { - value: ThresholdLineStyles.FULL, - text: i18n.translate('visTypeVislib.thresholdLine.style.fullText', { - defaultMessage: 'Full', - }), - }, - { - value: ThresholdLineStyles.DASHED, - text: i18n.translate('visTypeVislib.thresholdLine.style.dashedText', { - defaultMessage: 'Dashed', - }), - }, - { - value: ThresholdLineStyles.DOT_DASHED, - text: i18n.translate('visTypeVislib.thresholdLine.style.dotdashedText', { - defaultMessage: 'Dot-dashed', - }), - }, -]; - -const getRotateOptions = () => [ - { - text: i18n.translate('visTypeVislib.categoryAxis.rotate.horizontalText', { - defaultMessage: 'Horizontal', - }), - value: Rotates.HORIZONTAL, - }, - { - text: i18n.translate('visTypeVislib.categoryAxis.rotate.verticalText', { - defaultMessage: 'Vertical', - }), - value: Rotates.VERTICAL, - }, - { - text: i18n.translate('visTypeVislib.categoryAxis.rotate.angledText', { - defaultMessage: 'Angled', - }), - value: Rotates.ANGLED, - }, -]; - -export const GaugeTypes = Object.freeze({ - ARC: 'Arc' as 'Arc', - CIRCLE: 'Circle' as 'Circle', -}); -export type GaugeTypes = $Values; - -const getGaugeTypes = () => [ - { - text: i18n.translate('visTypeVislib.gauge.gaugeTypes.arcText', { - defaultMessage: 'Arc', - }), - value: GaugeTypes.ARC, - }, - { - text: i18n.translate('visTypeVislib.gauge.gaugeTypes.circleText', { - defaultMessage: 'Circle', - }), - value: GaugeTypes.CIRCLE, - }, -]; - -export const Alignments = Object.freeze({ - AUTOMATIC: 'automatic' as 'automatic', - HORIZONTAL: 'horizontal' as 'horizontal', - VERTICAL: 'vertical' as 'vertical', -}); -export type Alignments = $Values; - -const getAlignments = () => [ - { - text: i18n.translate('visTypeVislib.gauge.alignmentAutomaticTitle', { - defaultMessage: 'Automatic', - }), - value: Alignments.AUTOMATIC, - }, - { - text: i18n.translate('visTypeVislib.gauge.alignmentHorizontalTitle', { - defaultMessage: 'Horizontal', - }), - value: Alignments.HORIZONTAL, - }, - { - text: i18n.translate('visTypeVislib.gauge.alignmentVerticalTitle', { - defaultMessage: 'Vertical', - }), - value: Alignments.VERTICAL, - }, -]; - -const getConfigCollections = () => ({ - legendPositions: getPositions(), - positions: getPositions(), - chartTypes: getChartTypes(), - axisModes: getAxisModes(), - scaleTypes: getScaleTypes(), - chartModes: getChartModes(), - interpolationModes: getInterpolationModes(), - thresholdLineStyles: getThresholdLineStyles(), -}); - -const getGaugeCollections = () => ({ - gaugeTypes: getGaugeTypes(), - alignments: getAlignments(), - colorSchemas, -}); - -const getHeatmapCollections = () => ({ - legendPositions: getPositions(), - scales: getScaleTypes(), - colorSchemas, -}); - -export { - getConfigCollections, - getGaugeCollections, - getHeatmapCollections, - getPositions, - getRotateOptions, - getScaleTypes, - getInterpolationModes, - getChartTypes, - getChartModes, - getAxisModes, -}; diff --git a/src/plugins/vis_type_vislib/public/vis_renderer.tsx b/src/plugins/vis_type_vislib/public/vis_renderer.tsx index 9c697f481e63e..205c22092ac61 100644 --- a/src/plugins/vis_type_vislib/public/vis_renderer.tsx +++ b/src/plugins/vis_type_vislib/public/vis_renderer.tsx @@ -26,9 +26,13 @@ import { ChartsPluginSetup } from '../../charts/public'; import { VisTypeVislibCoreSetup } from './plugin'; import { VislibRenderValue, vislibVisName } from './vis_type_vislib_vis_fn'; +import { VislibChartType } from './types'; +import { PieRenderValue } from './pie_fn'; -function shouldShowNoResultsMessage(visData: any, visType: string): boolean { - if (['goal', 'gauge'].includes(visType)) { +const VislibWrapper = lazy(() => import('./vis_wrapper')); + +function shouldShowNoResultsMessage(visData: any, visType: VislibChartType): boolean { + if (['goal', 'gauge'].includes(visType as string)) { return false; } @@ -38,13 +42,12 @@ function shouldShowNoResultsMessage(visData: any, visType: string): boolean { return Boolean(isZeroHits); } -const VislibWrapper = lazy(() => import('./vis_wrapper')); - export const getVislibVisRenderer: ( core: VisTypeVislibCoreSetup, charts: ChartsPluginSetup -) => ExpressionRenderDefinition = (core, charts) => ({ +) => ExpressionRenderDefinition = (core, charts) => ({ name: vislibVisName, + displayName: 'Vislib visualization', reuseDomNode: true, render: async (domNode, config, handlers) => { const showNoResult = shouldShowNoResultsMessage(config.visData, config.visType); diff --git a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts index c5fa8f36f43e3..d627b8587882a 100644 --- a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts +++ b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts @@ -23,18 +23,18 @@ import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressio // @ts-ignore import { vislibSeriesResponseHandler } from './vislib/response_handler'; -import { BasicVislibParams } from './types'; +import { BasicVislibParams, VislibChartType } from './types'; export const vislibVisName = 'vislib_vis'; interface Arguments { - type: string; + type: Exclude; visConfig: string; } export interface VislibRenderValue { - visData: any; - visType: string; + visType: Exclude; + visData: unknown; visConfig: BasicVislibParams; } @@ -64,11 +64,15 @@ export const createVisTypeVislibVisFn = (): VisTypeVislibExpressionFunctionDefin help: 'vislib vis config', }, }, - fn(context, args) { - const visType = args.type; + fn(context, args, handlers) { + const visType = args.type as Exclude; const visConfig = JSON.parse(args.visConfig) as BasicVislibParams; const visData = vislibSeriesResponseHandler(context, visConfig.dimensions); + if (handlers?.inspectorAdapters?.tables) { + handlers.inspectorAdapters.tables.logDatatable('default', context); + } + return { type: 'render', as: vislibVisName, diff --git a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts index 1b43a213c618d..28415e3e2fa8c 100644 --- a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts +++ b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts @@ -36,3 +36,9 @@ export const visLibVisTypeDefinitions = [ gaugeVisTypeDefinition, goalVisTypeDefinition, ]; + +export const convertedTypeDefinitions = [ + heatmapVisTypeDefinition, + gaugeVisTypeDefinition, + goalVisTypeDefinition, +]; diff --git a/src/plugins/vis_type_vislib/public/vis_wrapper.tsx b/src/plugins/vis_type_vislib/public/vis_wrapper.tsx index 980ba1c175885..280a957396c34 100644 --- a/src/plugins/vis_type_vislib/public/vis_wrapper.tsx +++ b/src/plugins/vis_type_vislib/public/vis_wrapper.tsx @@ -27,10 +27,11 @@ import { ChartsPluginSetup } from '../../charts/public'; import { VislibRenderValue } from './vis_type_vislib_vis_fn'; import { createVislibVisController, VislibVisController } from './vis_controller'; import { VisTypeVislibCoreSetup } from './plugin'; +import { PieRenderValue } from './pie_fn'; import './index.scss'; -type VislibWrapperProps = VislibRenderValue & { +type VislibWrapperProps = (VislibRenderValue | PieRenderValue) & { core: VisTypeVislibCoreSetup; charts: ChartsPluginSetup; handlers: IInterpreterRenderHandlers; diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss b/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss index a06f0cb00787b..cb63811800c2d 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss @@ -3,8 +3,6 @@ // NOTE: Some of the styles attempt to align with the TSVB legend $visLegendWidth: 150px; -$visColorPickerWidth: $euiSizeM * 10; -$visLegendLineHeight: $euiSize; .visLegend__toggle { border-radius: $euiBorderRadius; @@ -81,20 +79,3 @@ $visLegendLineHeight: $euiSize; visibility: hidden; } } - -.visLegend__valueColorPicker { - width: ($euiSizeL * 8); // 8 columns -} - -.visLegend__valueColorPickerDot { - cursor: pointer; - - &:hover { - transform: scale(1.4); - } - - &-isSelected { - border: $euiSizeXS solid; - border-radius: 100%; - } -} diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index 7acc97404c11c..5b1a2b3975367 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -243,7 +243,7 @@ describe('VisLegend Component', () => { const first = getLegendItems(wrapper).first(); first.simulate('click'); - expect(wrapper.exists('.visLegend__valueDetails')).toBe(true); + expect(wrapper.exists('.visColorPicker')).toBe(true); }); }); @@ -256,8 +256,8 @@ describe('VisLegend Component', () => { const first = getLegendItems(wrapper).first(); first.simulate('click'); - const popover = wrapper.find('.visLegend__valueDetails').first(); - const firstColor = popover.find('.visLegend__valueColorPickerDot').first(); + const popover = wrapper.find('.visColorPicker').first(); + const firstColor = popover.find('.visColorPicker__valueDot').first(); firstColor.simulate('click'); const colors = mockState.get('vis.colors'); diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index cec97f0cadf11..5065642d88c6b 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -78,18 +78,20 @@ export class VisLegend extends PureComponent { }); }; - setColor = (label: string, color: string) => (event: BaseSyntheticEvent) => { + setColor = (label: string | number, color: string | null, event: BaseSyntheticEvent) => { if ((event as KeyboardEvent).key && (event as KeyboardEvent).key !== keys.ENTER) { return; } - const colors = this.props.uiState?.get('vis.colors') || {}; - if (colors[label] === color) delete colors[label]; - else colors[label] = color; - this.props.uiState?.setSilent('vis.colors', null); - this.props.uiState?.set('vis.colors', colors); - this.props.uiState?.emit('colorChanged'); - this.refresh(); + this.setState({ selectedLabel: null }, () => { + const colors = this.props.uiState?.get('vis.colors') || {}; + if (colors[label] === color || !color) delete colors[label]; + else colors[label] = color; + this.props.uiState?.setSilent('vis.colors', null); + this.props.uiState?.set('vis.colors', colors); + this.props.uiState?.emit('colorChanged'); + this.refresh(); + }); }; filter = ({ values: data }: LegendItem, negate: boolean) => { diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx index 39e6fb2d2aff4..6c7e343a22d8f 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx @@ -18,10 +18,8 @@ */ import React, { memo, useState, BaseSyntheticEvent, KeyboardEvent } from 'react'; -import classNames from 'classnames'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPopover, keys, @@ -33,7 +31,8 @@ import { EuiButtonGroupOptionProps, } from '@elastic/eui'; -import { legendColors, LegendItem } from './models'; +import { LegendItem } from './models'; +import { ColorPicker } from '../../../../../charts/public'; interface Props { item: LegendItem; @@ -45,7 +44,7 @@ interface Props { onSelect: (label: string | null) => (event?: BaseSyntheticEvent) => void; onHighlight: (event: BaseSyntheticEvent) => void; onUnhighlight: (event: BaseSyntheticEvent) => void; - setColor: (label: string, color: string) => (event: BaseSyntheticEvent) => void; + setColor: (label: string, color: string | null, event: BaseSyntheticEvent) => void; getColor: (label: string) => string; } @@ -159,40 +158,14 @@ const VisLegendItemComponent = ({ closePopover={onSelect(null)} panelPaddingSize="s" > -
- {canFilter && renderFilterBar()} + {canFilter && renderFilterBar()} -
- - - - {legendColors.map((color) => ( - - ))} -
-
+ setColor(item.label, c, e)} + /> ); diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts index ae6b365efc0cd..1b51fb8a53d11 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts @@ -17,9 +17,10 @@ * under the License. */ +import type { Dimension } from '../../../../../vis_type_xy/public'; + import { addToSiri, Serie } from './_add_to_siri'; import { Point } from './_get_point'; -import { Dimension } from './point_series'; describe('addToSiri', function () { it('creates a new series the first time it sees an id', function () { diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts index 5e5185d6c31ab..2e04e916e85f2 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts @@ -17,8 +17,10 @@ * under the License. */ +import { getAggId } from '../../../../../vis_type_xy/public'; +import type { Dimension } from '../../../../../vis_type_xy/public'; + import { Point } from './_get_point'; -import { Dimension } from './point_series'; export interface Serie { id: string; @@ -48,7 +50,7 @@ export function addToSiri( } series.set(id, { - id: id.split('-').pop() as string, + id: getAggId(id), rawId: id, label: yLabel == null ? id : yLabel, count: 0, diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts index fb14b04357f8f..5efde63427573 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts @@ -17,8 +17,10 @@ * under the License. */ +import type { Dimension, Dimensions } from '../../../../../vis_type_xy/public'; + import { getAspects } from './_get_aspects'; -import { Dimension, Dimensions, Aspect } from './point_series'; +import { Aspect } from './point_series'; import { Table, Row } from '../../types'; describe('getAspects', function () { diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts index 87819aa9b2a5c..e4094a4549841 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts @@ -17,8 +17,10 @@ * under the License. */ +import type { Dimensions } from '../../../../../vis_type_xy/public'; + import { makeFakeXAspect } from './_fake_x_aspect'; -import { Dimensions, Aspects } from './point_series'; +import { Aspects } from './point_series'; import { Table } from '../../types'; /** diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts index be44975bd4eb0..03d41096a8c6f 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts @@ -18,6 +18,7 @@ */ import { IFieldFormatsRegistry } from '../../../../../data/common'; + import { getPoint } from './_get_point'; import { setFormatService } from '../../../services'; import { Aspect } from './point_series'; @@ -94,7 +95,12 @@ describe('getPoint', function () { it('should call deserialize', function () { const seriesAspect = [ - { accessor: '1', format: { id: 'number', params: { pattern: '$' } } } as Aspect, + { + title: 'series', + accessor: '1', + format: { id: 'number', params: { pattern: '$' } }, + params: {}, + } as Aspect, ]; getPoint(table, xAspect, seriesAspect, row, 0, yAspect); diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts index 5eb1e65965318..fb42416abe249 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts @@ -41,7 +41,7 @@ export interface Point { table: Table; column: number; row: number; - value: number; + value: string; title: string; }; parent: Aspect | null; @@ -94,7 +94,7 @@ export function getPoint( table: table.$parent.table, column: table.$parent.column, row: table.$parent.row, - value: table.$parent.key, + value: table.$parent.formattedKey, title: table.$parent.name, }, parent: series ? series[0] : null, diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts index 3f0560c2c9f28..aa24530b025dc 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts @@ -18,16 +18,12 @@ */ import moment from 'moment'; + +import type { DateHistogramParams, HistogramParams } from '../../../../../vis_type_xy/public'; + import { initXAxis } from './_init_x_axis'; import { makeFakeXAspect } from './_fake_x_aspect'; -import { - Aspects, - Chart, - DateHistogramOrdered, - DateHistogramParams, - HistogramOrdered, - HistogramParams, -} from './point_series'; +import { Aspects, Chart, DateHistogramOrdered, HistogramOrdered } from './point_series'; import { Table, Column } from '../../types'; describe('initXAxis', function () { @@ -110,7 +106,7 @@ describe('initXAxis', function () { it('reads the date interval param from the x agg', function () { const dateHistogramParams = chart.aspects.x[0].params as DateHistogramParams; - dateHistogramParams.interval = 'P1D'; + dateHistogramParams.interval = moment.duration(1, 'd').asMilliseconds(); dateHistogramParams.intervalESValue = 1; dateHistogramParams.intervalESUnit = 'd'; dateHistogramParams.date = true; diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts index b2e4d6e4b40c9..7dd1dd259f785 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts @@ -19,8 +19,11 @@ import moment from 'moment'; import _ from 'lodash'; + +import type { DateHistogramParams } from '../../../../../vis_type_xy/public/types'; + import { orderedDateAxis } from './_ordered_date_axis'; -import { DateHistogramParams, OrderedChart } from './point_series'; +import { OrderedChart } from './point_series'; describe('orderedDateAxis', function () { const baseArgs = { diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts index 2a5ab197c9ab7..7c343af7840ee 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts @@ -18,7 +18,10 @@ */ import _ from 'lodash'; -import { buildPointSeriesData, Dimensions } from './point_series'; + +import type { Dimensions } from '../../../../../vis_type_xy/public'; + +import { buildPointSeriesData } from './point_series'; import { Table, Column } from '../../types'; import { setFormatService } from '../../../services'; import { Serie } from './_add_to_siri'; diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts index f40d01e6a8123..2bc669e0b77a3 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts @@ -18,6 +18,14 @@ */ import { Duration } from 'moment'; + +import type { + Dimension, + Dimensions, + DateHistogramParams, + HistogramParams, +} from '../../../../../vis_type_xy/public'; + import { getSeries } from './_get_series'; import { getAspects } from './_get_aspects'; import { initYAxis } from './_init_y_axis'; @@ -26,41 +34,6 @@ import { orderedDateAxis } from './_ordered_date_axis'; import { Serie } from './_add_to_siri'; import { Column, Table } from '../../types'; -export interface DateHistogramParams { - date: boolean; - interval: number | string; - intervalESValue: number; - intervalESUnit: string; - format: string; - bounds?: { - min: string | number; - max: string | number; - }; -} -export interface HistogramParams { - interval: number; -} -export interface FakeParams { - defaultValue: string; -} -export interface Dimension { - accessor: number; - format: { - id?: string; - params?: { pattern?: string; [key: string]: any }; - }; - params: DateHistogramParams | HistogramParams | FakeParams | {}; -} - -export interface Dimensions { - x: Dimension | null; - y: Dimension[]; - z?: Dimension[]; - series?: Dimension | Dimension[]; - width?: Dimension[]; - splitRow?: Dimension[]; - splitColumn?: Dimension[]; -} export interface Aspect { accessor: Column['id']; column?: Dimension['accessor']; diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/handler.js b/src/plugins/vis_type_vislib/public/vislib/lib/handler.js index 938ea3adcb9b5..e1908a8483feb 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/handler.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/handler.js @@ -21,15 +21,16 @@ import d3 from 'd3'; import _ from 'lodash'; import MarkdownIt from 'markdown-it'; +import { dispatchRenderComplete } from '../../../../kibana_utils/public'; + +import { visTypes as chartTypes } from '../visualizations/vis_types'; import { NoResults } from '../errors'; import { Layout } from './layout/layout'; import { ChartTitle } from './chart_title'; import { Alerts } from './alerts'; import { Axis } from './axis/axis'; import { ChartGrid as Grid } from './chart_grid'; -import { visTypes as chartTypes } from '../visualizations/vis_types'; import { Binder } from './binder'; -import { dispatchRenderComplete } from '../../../../kibana_utils/public'; const markdownIt = new MarkdownIt({ html: false, diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json index d52cb18727c05..50d6eab03e3f7 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json @@ -31,7 +31,7 @@ }, "params": { "date": true, - "interval": "P1D", + "interval": 86400000, "format": "YYYY-MM-DD", "bounds": { "min": "2019-05-10T04:00:00.000Z", @@ -128,7 +128,7 @@ }, "xAxisLabel": "timestamp per day", "ordered": { - "interval": "P1D", + "interval": 86400000, "date": true, "min": 1557460800000, "max": 1557656337342 diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json index 6e1a707229974..1987c59f6722b 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json @@ -31,7 +31,7 @@ }, "params": { "date": true, - "interval": "P1D", + "interval": 86400000, "format": "YYYY-MM-DD", "bounds": { "min": "2019-05-10T04:00:00.000Z", @@ -128,7 +128,7 @@ }, "xAxisLabel": "timestamp per day", "ordered": { - "interval": "P1D", + "interval": 86400000, "date": true, "min": 1557460800000, "max": 1557656337342 @@ -460,4 +460,4 @@ "50th percentile of AvgTicketPrice" ] } -} \ No newline at end of file +} diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json index f7dd18f5eb712..ae1f3cbf24c33 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json @@ -32,7 +32,7 @@ }, "params": { "date": true, - "interval": "P1D", + "interval": 86400000, "format": "YYYY-MM-DD", "bounds": { "min": "2019-05-10T04:00:00.000Z", @@ -453,4 +453,4 @@ } ], "enableHover": true -} \ No newline at end of file +} diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json index 02062c987564e..f2ee245a8431f 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json @@ -32,7 +32,7 @@ }, "params": { "date": true, - "interval": "P1D", + "interval": 86400000, "format": "YYYY-MM-DD", "bounds": { "min": "2019-05-10T04:00:00.000Z", @@ -455,4 +455,4 @@ } ], "enableHover": true -} \ No newline at end of file +} diff --git a/src/plugins/vis_type_vislib/public/vislib/response_handler.js b/src/plugins/vis_type_vislib/public/vislib/response_handler.js index 871ce97ad4480..9028b53fbd003 100644 --- a/src/plugins/vis_type_vislib/public/vislib/response_handler.js +++ b/src/plugins/vis_type_vislib/public/vislib/response_handler.js @@ -34,14 +34,16 @@ function tableResponseHandler(table, dimensions) { table.rows.forEach((row, rowIndex) => { const splitValue = row[splitColumn.id]; + const formattedValue = splitColumnFormatter.convert(splitValue); if (!splitMap.hasOwnProperty(splitValue)) { splitMap[splitValue] = splitIndex++; const tableGroup = { $parent: converted, - title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, + title: `${formattedValue}: ${splitColumn.name}`, name: splitColumn.name, key: splitValue, + formattedKey: formattedValue, column: splitColumnIndex, row: rowIndex, table, diff --git a/src/plugins/vis_type_vislib/public/vislib/types.ts b/src/plugins/vis_type_vislib/public/vislib/types.ts index ad59603663b84..5096015c99a90 100644 --- a/src/plugins/vis_type_vislib/public/vislib/types.ts +++ b/src/plugins/vis_type_vislib/public/vislib/types.ts @@ -33,6 +33,7 @@ export interface TableParent { column: number; row: number; key: number; + formattedKey: string; name: string; } export interface Table { diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.js index a2b830ffaa781..40d15733b418d 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.js @@ -22,8 +22,8 @@ import { Chart } from './_chart'; import { gaugeTypes } from './gauges/gauge_types'; export class GaugeChart extends Chart { - constructor(handler, chartEl, chartData, deps) { - super(handler, chartEl, chartData, deps); + constructor(handler, chartEl, chartData, uiSettings) { + super(handler, chartEl, chartData, uiSettings); this.gaugeConfig = handler.visConfig.get('gauge', {}); this.gauge = new gaugeTypes[this.gaugeConfig.type](this); } diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js index 4564721464767..d725c29eef310 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js @@ -40,8 +40,8 @@ const defaults = { * @param chartData {Object} Elasticsearch query results for this specific chart */ export class HeatmapChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs, deps) { - super(handler, chartEl, chartData, seriesConfigArgs, deps); + constructor(handler, chartEl, chartData, seriesConfigArgs, core) { + super(handler, chartEl, chartData, seriesConfigArgs, core); this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); diff --git a/src/plugins/vis_type_vislib/server/ui_settings.ts b/src/plugins/vis_type_vislib/server/ui_settings.ts index a48cbbae3d0ca..58564b4055daa 100644 --- a/src/plugins/vis_type_vislib/server/ui_settings.ts +++ b/src/plugins/vis_type_vislib/server/ui_settings.ts @@ -24,6 +24,8 @@ import { UiSettingsParams } from 'kibana/server'; import { DIMMING_OPACITY_SETTING, HEATMAP_MAX_BUCKETS_SETTING } from '../common'; export const uiSettings: Record = { + // TODO: move this to vis_type_xy when vislib is removed + // https://github.com/elastic/kibana/issues/56143 [DIMMING_OPACITY_SETTING]: { name: i18n.translate('visTypeVislib.advancedSettings.visualization.dimmingOpacityTitle', { defaultMessage: 'Dimming opacity', diff --git a/src/plugins/vis_type_xy/README.md b/src/plugins/vis_type_xy/README.md index 70ddb21c1e9db..549fc4e3ea189 100644 --- a/src/plugins/vis_type_xy/README.md +++ b/src/plugins/vis_type_xy/README.md @@ -1,2 +1,2 @@ Contains the new xy-axis chart using the elastic-charts library, which will eventually -replace the vislib xy-axis (bar, area, line) charts. \ No newline at end of file +replace the vislib xy-axis charts including bar, area, and line. diff --git a/src/plugins/vis_type_xy/common/index.ts b/src/plugins/vis_type_xy/common/index.ts new file mode 100644 index 0000000000000..bf498229a1b54 --- /dev/null +++ b/src/plugins/vis_type_xy/common/index.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $Values } from '@kbn/utility-types'; + +/** + * Type of charts able to render + */ +export const ChartType = Object.freeze({ + Line: 'line' as const, + Area: 'area' as const, + Histogram: 'histogram' as const, +}); +export type ChartType = $Values; + +/** + * Type of xy visualizations + */ +export type XyVisType = ChartType | 'horizontal_bar'; + +export const CHARTS_LIBRARY = 'visualization:visualize:chartsLibrary'; diff --git a/src/plugins/vis_type_xy/jest.config.js b/src/plugins/vis_type_xy/jest.config.js new file mode 100644 index 0000000000000..556e518d4f4e1 --- /dev/null +++ b/src/plugins/vis_type_xy/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/vis_type_xy'], +}; diff --git a/src/plugins/vis_type_xy/kibana.json b/src/plugins/vis_type_xy/kibana.json index ca02da45e9112..14c3ce36bf375 100644 --- a/src/plugins/vis_type_xy/kibana.json +++ b/src/plugins/vis_type_xy/kibana.json @@ -3,5 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["charts", "expressions", "visualizations"] + "requiredPlugins": ["charts", "data", "expressions", "visualizations"], + "requiredBundles": ["kibanaUtils", "visDefaultEditor"] } diff --git a/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap new file mode 100644 index 0000000000000..e6665c26a2815 --- /dev/null +++ b/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`xy vis toExpressionAst function should match basic snapshot 1`] = ` +Object { + "addArgument": [Function], + "arguments": Object { + "type": Array [ + "area", + ], + "visConfig": Array [ + "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", + ], + }, + "getArgument": [Function], + "name": "xy_vis", + "removeArgument": [Function], + "replaceArgument": [Function], + "toAst": [Function], + "toString": [Function], + "type": "expression_function_builder", +} +`; diff --git a/src/plugins/vis_type_xy/public/_chart.scss b/src/plugins/vis_type_xy/public/_chart.scss new file mode 100644 index 0000000000000..ac9d4ed04aec4 --- /dev/null +++ b/src/plugins/vis_type_xy/public/_chart.scss @@ -0,0 +1,7 @@ +.xyChart__container { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} diff --git a/src/plugins/vis_type_xy/public/components/_detailed_tooltip.scss b/src/plugins/vis_type_xy/public/components/_detailed_tooltip.scss new file mode 100644 index 0000000000000..91b0a8d023290 --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/_detailed_tooltip.scss @@ -0,0 +1,34 @@ +.detailedTooltip { + @include euiToolTipStyle('s'); + pointer-events: none; + max-width: $euiSizeXL * 10; + overflow: hidden; + padding: $euiSizeS; + + table { + td, + th { + text-align: left; + padding: $euiSizeXS; + overflow-wrap: break-word; + word-wrap: break-word; + } + } +} + +.detailedTooltip__header { + > :last-child { + margin-bottom: $euiSizeS; + } +} + +.detailedTooltip__labelContainer, +.detailedTooltip__valueContainer { + overflow-wrap: break-word; + word-wrap: break-word; +} + +.detailedTooltip__label { + font-weight: $euiFontWeightMedium; + color: shade($euiColorGhost, 20%); +} diff --git a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx new file mode 100644 index 0000000000000..3427baed41b8d --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx @@ -0,0 +1,142 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { isNil } from 'lodash'; + +import { + CustomTooltip, + TooltipValue, + TooltipValueFormatter, + XYChartSeriesIdentifier, +} from '@elastic/charts'; + +import { BUCKET_TYPES } from '../../../data/public'; + +import { Aspects } from '../types'; + +import './_detailed_tooltip.scss'; +import { fillEmptyValue } from '../utils/get_series_name_fn'; +import { COMPLEX_SPLIT_ACCESSOR } from '../utils/accessors'; + +interface TooltipData { + label: string; + value: string; +} + +const getTooltipData = ( + aspects: Aspects, + header: TooltipValue | null, + value: TooltipValue +): TooltipData[] => { + const data: TooltipData[] = []; + + if (header) { + const xFormatter = + aspects.x.aggType === BUCKET_TYPES.DATE_RANGE || aspects.x.aggType === BUCKET_TYPES.RANGE + ? null + : aspects.x.formatter; + data.push({ + label: aspects.x.title, + value: xFormatter ? xFormatter(header.value) : `${header.value}`, + }); + } + + const valueSeries = value.seriesIdentifier as XYChartSeriesIdentifier; + const yAccessor = aspects.y.find(({ accessor }) => accessor === valueSeries.yAccessor) ?? null; + + if (yAccessor) { + data.push({ + label: yAccessor.title, + value: yAccessor.formatter ? yAccessor.formatter(value.value) : `${value.value}`, + }); + } + + if (aspects.z && !isNil(value.markValue)) { + data.push({ + label: aspects.z.title, + value: aspects.z.formatter ? aspects.z.formatter(value.markValue) : `${value.markValue}`, + }); + } + + valueSeries.splitAccessors.forEach((splitValue, key) => { + const split = (aspects.series ?? []).find(({ accessor }, i) => { + return accessor === key || key === `${COMPLEX_SPLIT_ACCESSOR}::${i}`; + }); + + if (split) { + data.push({ + label: split?.title, + value: + split?.formatter && !key.toString().startsWith(COMPLEX_SPLIT_ACCESSOR) + ? split?.formatter(splitValue) + : `${splitValue}`, + }); + } + }); + + return data; +}; + +const renderData = ({ label, value: rawValue }: TooltipData, index: number) => { + const value = fillEmptyValue(rawValue); + return label && value ? ( + + +
{label}
+ + + +
{value}
+ + + ) : null; +}; + +export const getDetailedTooltip = (aspects: Aspects) => ( + headerFormatter?: TooltipValueFormatter +): CustomTooltip => { + return function DetailedTooltip({ header, values }) { + // Note: first value is not necessarily the closest value + // To be fixed with https://github.com/elastic/elastic-charts/issues/835 + // TODO: Allow multiple values to be displayed in tooltip + const highlightedValue = values.find(({ isHighlighted }) => isHighlighted); + + if (!highlightedValue) { + return null; + } + + const tooltipData = getTooltipData(aspects, header, highlightedValue); + + if (tooltipData.length === 0) { + return null; + } + + return ( +
+ {headerFormatter && header && ( +
{headerFormatter(header)}
+ )} + + {tooltipData.map(renderData)} +
+
+ ); + }; +}; diff --git a/src/plugins/vis_type_xy/public/components/index.ts b/src/plugins/vis_type_xy/public/components/index.ts new file mode 100644 index 0000000000000..d8d55c77b7a8a --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { XYAxis } from './xy_axis'; +export { XYEndzones } from './xy_endzones'; +export { XYCurrentTime } from './xy_current_time'; +export { XYSettings } from './xy_settings'; +export { XYThresholdLine } from './xy_threshold_line'; +export { SplitChartWarning } from './split_chart_warning'; diff --git a/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx b/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx new file mode 100644 index 0000000000000..a9aa2bf24601b --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC } from 'react'; + +import { EuiLink, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { getDocLinks } from '../services'; + +export const SplitChartWarning: FC = () => { + const advancedSettingsLink = getDocLinks().links.management.visualizationSettings; + + return ( + + + + + ), + }} + /> + + ); +}; diff --git a/src/plugins/inspector/common/adapters/data/types.ts b/src/plugins/vis_type_xy/public/components/xy_axis.tsx similarity index 53% rename from src/plugins/inspector/common/adapters/data/types.ts rename to src/plugins/vis_type_xy/public/components/xy_axis.tsx index 040724f4ae36e..b64f2bf1ec0ec 100644 --- a/src/plugins/inspector/common/adapters/data/types.ts +++ b/src/plugins/vis_type_xy/public/components/xy_axis.tsx @@ -17,32 +17,39 @@ * under the License. */ -export interface TabularDataValue { - formatted: string; - raw: unknown; -} +import React, { FC } from 'react'; -export interface TabularDataColumn { - name: string; - field: string; - filter?: (value: TabularDataValue) => void; - filterOut?: (value: TabularDataValue) => void; -} +import { Axis } from '@elastic/charts'; -export type TabularDataRow = Record; +import { AxisConfig } from '../types'; -export interface TabularData { - columns: TabularDataColumn[]; - rows: TabularDataRow[]; -} +type XYAxisPros = AxisConfig; -export type TabularCallback = () => TabularData | Promise; - -export interface TabularHolder { - data: TabularData | null; - options: TabularLoaderOptions; -} - -export interface TabularLoaderOptions { - returnsFormattedValues?: boolean; -} +export const XYAxis: FC = ({ + id, + title, + show, + position, + groupId, + grid, + ticks, + domain, + style, + integersOnly, +}) => ( + +); diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/__mocks__/@elastic/charts.js b/src/plugins/vis_type_xy/public/components/xy_current_time.tsx similarity index 61% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/__mocks__/@elastic/charts.js rename to src/plugins/vis_type_xy/public/components/xy_current_time.tsx index 19bfe685cac90..66ad4fda3bec7 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/__mocks__/@elastic/charts.js +++ b/src/plugins/vis_type_xy/public/components/xy_current_time.tsx @@ -17,28 +17,21 @@ * under the License. */ -export const CurveType = { - CURVE_CARDINAL: 0, - CURVE_NATURAL: 1, - CURVE_MONOTONE_X: 2, - CURVE_MONOTONE_Y: 3, - CURVE_BASIS: 4, - CURVE_CATMULL_ROM: 5, - CURVE_STEP: 6, - CURVE_STEP_AFTER: 7, - CURVE_STEP_BEFORE: 8, - LINEAR: 9, -}; +import React, { FC } from 'react'; +import { DomainRange } from '@elastic/charts'; +import { CurrentTime } from '../../../charts/public'; -export const ScaleType = { - Linear: 'linear', - Ordinal: 'ordinal', - Log: 'log', - Sqrt: 'sqrt', - Time: 'time', -}; +interface XYCurrentTime { + enabled: boolean; + isDarkMode: boolean; + domain?: DomainRange; +} -export const BarSeries = () => null; -export const AreaSeries = () => null; +export const XYCurrentTime: FC = ({ enabled, isDarkMode, domain }) => { + if (!enabled) { + return null; + } -export { LIGHT_THEME, DARK_THEME } from '@elastic/charts'; + const domainEnd = domain && 'max' in domain ? domain.max : undefined; + return ; +}; diff --git a/src/plugins/vis_type_xy/public/components/xy_endzones.tsx b/src/plugins/vis_type_xy/public/components/xy_endzones.tsx new file mode 100644 index 0000000000000..33e1d1e18bb1d --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/xy_endzones.tsx @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC } from 'react'; + +import { DomainRange } from '@elastic/charts'; + +import { Endzones } from '../../../charts/public'; + +interface XYEndzones { + enabled: boolean; + isDarkMode: boolean; + isFullBin: boolean; + hideTooltips?: boolean; + domain?: DomainRange; + adjustedDomain?: DomainRange; +} + +export const XYEndzones: FC = ({ + enabled, + isDarkMode, + isFullBin, + hideTooltips, + domain, + adjustedDomain, +}) => { + if ( + enabled && + domain && + adjustedDomain && + 'min' in domain && + 'max' in domain && + domain.minInterval !== undefined && + 'min' in adjustedDomain && + 'max' in adjustedDomain + ) { + return ( + + ); + } + + return null; +}; diff --git a/src/plugins/vis_type_xy/public/components/xy_settings.tsx b/src/plugins/vis_type_xy/public/components/xy_settings.tsx new file mode 100644 index 0000000000000..3682fdf3350b0 --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/xy_settings.tsx @@ -0,0 +1,182 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC } from 'react'; + +import { + Direction, + Settings, + DomainRange, + Position, + PartialTheme, + ElementClickListener, + BrushEndListener, + RenderChangeListener, + LegendAction, + LegendColorPicker, + TooltipProps, + TickFormatter, +} from '@elastic/charts'; + +import { renderEndzoneTooltip } from '../../../charts/public'; + +import { getThemeService, getUISettings } from '../services'; +import { VisConfig } from '../types'; +import { fillEmptyValue } from '../utils/get_series_name_fn'; + +declare global { + interface Window { + /** + * Flag used to enable debugState on elastic charts + */ + _echDebugStateFlag?: boolean; + } +} + +type XYSettingsProps = Pick< + VisConfig, + | 'markSizeRatio' + | 'rotation' + | 'enableHistogramMode' + | 'tooltip' + | 'isTimeChart' + | 'xAxis' + | 'orderBucketsBySum' +> & { + xDomain?: DomainRange; + adjustedXDomain?: DomainRange; + showLegend: boolean; + onElementClick: ElementClickListener; + onBrushEnd?: BrushEndListener; + onRenderChange: RenderChangeListener; + legendAction?: LegendAction; + legendColorPicker: LegendColorPicker; + legendPosition: Position; +}; + +export const XYSettings: FC = ({ + markSizeRatio, + rotation, + enableHistogramMode, + tooltip, + isTimeChart, + xAxis, + orderBucketsBySum, + xDomain, + adjustedXDomain, + showLegend, + onElementClick, + onBrushEnd, + onRenderChange, + legendAction, + legendColorPicker, + legendPosition, +}) => { + const themeService = getThemeService(); + const theme = themeService.useChartsTheme(); + const baseTheme = themeService.useChartsBaseTheme(); + const dimmingOpacity = getUISettings().get('visualization:dimmingOpacity'); + const fontSize = + typeof theme.barSeriesStyle?.displayValue?.fontSize === 'number' + ? { min: theme.barSeriesStyle?.displayValue?.fontSize } + : theme.barSeriesStyle?.displayValue?.fontSize ?? { min: 8 }; + + const themeOverrides: PartialTheme = { + markSizeRatio, + sharedStyle: { + unhighlighted: { + opacity: dimmingOpacity, + }, + }, + barSeriesStyle: { + displayValue: { + fontSize, + alignment: { + horizontal: 'center', + vertical: 'middle', + }, + }, + }, + axes: { + axisTitle: { + padding: { + outer: 10, + }, + }, + }, + chartMargins: + legendPosition === Position.Top || legendPosition === Position.Right + ? { + bottom: (theme.chartMargins?.bottom ?? 0) + 10, + } + : { + right: (theme.chartMargins?.right ?? 0) + 10, + }, + }; + + const headerValueFormatter: TickFormatter | undefined = xAxis.ticks?.formatter + ? (value) => fillEmptyValue(xAxis.ticks?.formatter?.(value)) ?? '' + : undefined; + const headerFormatter = + isTimeChart && xDomain && adjustedXDomain + ? renderEndzoneTooltip( + xDomain.minInterval, + 'min' in xDomain ? xDomain.min : undefined, + 'max' in xDomain ? xDomain.max : undefined, + headerValueFormatter, + !tooltip.detailedTooltip + ) + : headerValueFormatter && + (tooltip.detailedTooltip ? undefined : ({ value }: any) => headerValueFormatter(value)); + + const tooltipProps: TooltipProps = tooltip.detailedTooltip + ? { + ...tooltip, + customTooltip: tooltip.detailedTooltip(headerFormatter), + headerFormatter: undefined, + } + : { ...tooltip, headerFormatter }; + + return ( + + ); +}; diff --git a/src/plugins/inspector/common/adapters/data/data_adapter.ts b/src/plugins/vis_type_xy/public/components/xy_threshold_line.tsx similarity index 52% rename from src/plugins/inspector/common/adapters/data/data_adapter.ts rename to src/plugins/vis_type_xy/public/components/xy_threshold_line.tsx index a21aa7db39145..46b0a009807a2 100644 --- a/src/plugins/inspector/common/adapters/data/data_adapter.ts +++ b/src/plugins/vis_type_xy/public/components/xy_threshold_line.tsx @@ -17,24 +17,42 @@ * under the License. */ -import { EventEmitter } from 'events'; -import { TabularCallback, TabularHolder, TabularLoaderOptions } from './types'; +import React, { FC } from 'react'; -export class DataAdapter extends EventEmitter { - private tabular?: TabularCallback; - private tabularOptions?: TabularLoaderOptions; +import { AnnotationDomainTypes, LineAnnotation } from '@elastic/charts'; - public setTabularLoader(callback: TabularCallback, options: TabularLoaderOptions = {}): void { - this.tabular = callback; - this.tabularOptions = options; - this.emit('change', 'tabular'); - } +import { ThresholdLineConfig } from '../types'; + +type XYThresholdLineProps = ThresholdLineConfig & { + groupId?: string; +}; - public getTabular(): Promise { - if (!this.tabular || !this.tabularOptions) { - return Promise.resolve({ data: null, options: {} }); - } - const options = this.tabularOptions; - return Promise.resolve(this.tabular()).then((data) => ({ data, options })); +export const XYThresholdLine: FC = ({ + show, + value: dataValue, + color, + width, + groupId, + dash, +}) => { + if (!show) { + return null; } -} + + return ( + + ); +}; diff --git a/src/plugins/vis_type_xy/public/config/get_agg_id.ts b/src/plugins/vis_type_xy/public/config/get_agg_id.ts new file mode 100644 index 0000000000000..7923145d83013 --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_agg_id.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Get agg id from accessor + * + * For now this is determined by the esaggs column name. Could be cleaned up in the future. + */ +export const getAggId = (accessor: string) => (accessor ?? '').split('-').pop() ?? ''; diff --git a/src/plugins/vis_type_xy/public/config/get_aspects.ts b/src/plugins/vis_type_xy/public/config/get_aspects.ts new file mode 100644 index 0000000000000..73096de0a5d51 --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_aspects.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { compact } from 'lodash'; + +import { i18n } from '@kbn/i18n'; + +import { DatatableColumn } from '../../../expressions/public'; + +import { Aspect, Dimension, Aspects, Dimensions } from '../types'; +import { getFormatService } from '../services'; +import { getAggId } from './get_agg_id'; + +export function getEmptyAspect(): Aspect { + return { + accessor: null, + aggId: null, + aggType: null, + title: i18n.translate('visTypeXy.aggResponse.allDocsTitle', { + defaultMessage: 'All docs', + }), + params: { + defaultValue: '_all', + }, + }; +} +export function getAspects(columns: DatatableColumn[], { x, y, z, series }: Dimensions): Aspects { + const seriesDimensions = Array.isArray(series) || series === undefined ? series : [series]; + + return { + x: getAspectsFromDimension(columns, x) ?? getEmptyAspect(), + y: getAspectsFromDimension(columns, y) ?? [], + z: z && z?.length > 0 ? getAspectsFromDimension(columns, z[0]) : undefined, + series: getAspectsFromDimension(columns, seriesDimensions), + }; +} + +function getAspectsFromDimension( + columns: DatatableColumn[], + dimension?: Dimension | null +): Aspect | undefined; +function getAspectsFromDimension( + columns: DatatableColumn[], + dimensions?: Dimension[] | null +): Aspect[] | undefined; +function getAspectsFromDimension( + columns: DatatableColumn[], + dimensions?: Dimension | Dimension[] | null +): Aspect[] | Aspect | undefined { + if (!dimensions) { + return; + } + + if (Array.isArray(dimensions)) { + return compact( + dimensions.map((d) => { + const column = d && columns[d.accessor]; + return column && getAspect(column, d); + }) + ); + } + + const column = columns[dimensions.accessor]; + return column && getAspect(column, dimensions); +} + +const getAspect = ( + { id: accessor, name: title }: DatatableColumn, + { accessor: column, format, params, aggType }: Dimension +): Aspect => ({ + accessor, + column, + title, + format, + aggType, + aggId: getAggId(accessor), + formatter: (value: any) => getFormatService().deserialize(format).convert(value), + params, +}); diff --git a/src/plugins/vis_type_xy/public/config/get_axis.ts b/src/plugins/vis_type_xy/public/config/get_axis.ts new file mode 100644 index 0000000000000..43a9e25e0e287 --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_axis.ts @@ -0,0 +1,198 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { identity, isNil } from 'lodash'; + +import { AxisSpec, TickFormatter, YDomainRange, ScaleType as ECScaleType } from '@elastic/charts'; + +import { LabelRotation } from '../../../charts/public'; +import { BUCKET_TYPES } from '../../../data/public'; + +import { + Aspect, + CategoryAxis, + Grid, + AxisConfig, + TickOptions, + ScaleConfig, + Scale, + ScaleType, + AxisType, + XScaleType, + YScaleType, + SeriesParam, +} from '../types'; +import { fillEmptyValue } from '../utils/get_series_name_fn'; + +export function getAxis( + { type, title: axisTitle, labels, scale: axisScale, ...axis }: CategoryAxis, + { categoryLines, valueAxis }: Grid, + { params, format, formatter, title: fallbackTitle = '', aggType }: Aspect, + seriesParams: SeriesParam[], + isDateHistogram = false +): AxisConfig { + const isCategoryAxis = type === AxisType.Category; + // Hide unassigned axis, not supported in elastic charts + // TODO: refactor when disallowing unassigned axes + // https://github.com/elastic/kibana/issues/82752 + const show = + (isCategoryAxis || seriesParams.some(({ valueAxis: id }) => id === axis.id)) && axis.show; + const groupId = axis.id; + + const grid = isCategoryAxis + ? { + show: categoryLines, + } + : { + show: valueAxis === axis.id, + }; + // Date range formatter applied on xAccessor + const tickFormatter = + aggType === BUCKET_TYPES.DATE_RANGE || aggType === BUCKET_TYPES.RANGE ? identity : formatter; + const ticks: TickOptions = { + formatter: tickFormatter, + labelFormatter: getLabelFormatter(labels.truncate, tickFormatter), + show: labels.show, + rotation: labels.rotate, + showOverlappingLabels: !labels.filter, + showDuplicates: !labels.filter, + }; + const scale = getScale(axisScale, params, format, isCategoryAxis); + const title = axisTitle.text || fallbackTitle; + const fallbackRotation = + isCategoryAxis && isDateHistogram ? LabelRotation.Horizontal : LabelRotation.Vertical; + + return { + ...axis, + show, + groupId, + title, + ticks, + grid, + scale, + style: getAxisStyle(ticks, title, fallbackRotation), + domain: getAxisDomain(scale, isCategoryAxis), + integersOnly: aggType === 'count', + }; +} + +function getLabelFormatter( + truncate?: number | null, + formatter?: TickFormatter +): TickFormatter | undefined { + if (truncate === null || truncate === undefined) { + return formatter; + } + + return (value: any) => { + const formattedStringValue = `${formatter ? formatter(value) : value}`; + const finalValue = fillEmptyValue(formattedStringValue); + + if (finalValue.length > truncate) { + return `${finalValue.slice(0, truncate)}...`; + } + + return finalValue; + }; +} + +function getScaleType( + scale?: Scale, + isNumber?: boolean, + isTime = false, + isHistogram = false +): ECScaleType | undefined { + if (isTime) return ECScaleType.Time; + if (isHistogram) return ECScaleType.Linear; + + if (!isNumber) { + return ECScaleType.Ordinal; + } + + const type = scale?.type; + if (type === ScaleType.SquareRoot) { + return ECScaleType.Sqrt; + } + + return type; +} + +function getScale( + scale: Scale, + params: Aspect['params'], + format: Aspect['format'], + isCategoryAxis: boolean +): ScaleConfig { + const type = (isCategoryAxis + ? getScaleType( + scale, + format?.id === 'number' || (format?.params?.id === 'number' && format?.id !== 'range'), + 'date' in params, + 'interval' in params + ) + : getScaleType(scale, true)) as S; + + return { + ...scale, + type, + }; +} + +function getAxisStyle( + ticks?: TickOptions, + title?: string, + rotationFallback: LabelRotation = LabelRotation.Vertical +): AxisSpec['style'] { + return { + axisTitle: { + visible: (title ?? '').trim().length > 0, + }, + tickLabel: { + visible: ticks?.show, + rotation: -(ticks?.rotation ?? rotationFallback), + }, + }; +} + +function getAxisDomain( + scale: ScaleConfig, + isCategoryAxis: boolean +): YDomainRange | undefined { + if (isCategoryAxis || !scale) { + return; + } + + const { min, max, defaultYExtents, boundsMargin } = scale; + const fit = defaultYExtents; + const padding = boundsMargin; + + if (!isNil(min) && !isNil(max)) { + return { fit, padding, min, max }; + } + + if (!isNil(min)) { + return { fit, padding, min }; + } + + if (!isNil(max)) { + return { fit, padding, max }; + } + + return { fit, padding }; +} diff --git a/src/plugins/vis_type_xy/public/config/get_config.ts b/src/plugins/vis_type_xy/public/config/get_config.ts new file mode 100644 index 0000000000000..f0c5740b1307b --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_config.ts @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleContinuousType } from '@elastic/charts/dist/scales'; + +import { Datatable } from '../../../expressions/public'; +import { BUCKET_TYPES } from '../../../data/public'; + +import { + Aspect, + AxisConfig, + DateHistogramParams, + SeriesParam, + VisConfig, + VisParams, + XScaleType, + YScaleType, +} from '../types'; +import { getThresholdLine } from './get_threshold_line'; +import { getRotation } from './get_rotation'; +import { getTooltip } from './get_tooltip'; +import { getLegend } from './get_legend'; +import { getAxis } from './get_axis'; +import { getAspects } from './get_aspects'; +import { ChartType } from '../index'; + +export function getConfig(table: Datatable, params: VisParams): VisConfig { + const { + thresholdLine, + orderBucketsBySum, + addTimeMarker, + radiusRatio, + labels, + fittingFunction, + detailedTooltip, + isVislibVis, + } = params; + const aspects = getAspects(table.columns, params.dimensions); + const xAxis = getAxis( + params.categoryAxes[0], + params.grid, + aspects.x, + params.seriesParams, + params.dimensions.x?.aggType === BUCKET_TYPES.DATE_HISTOGRAM + ); + const tooltip = getTooltip(aspects, params); + const yAxes = params.valueAxes.map((a) => + // uses first y aspect in array for formatting axis + getAxis(a, params.grid, aspects.y[0], params.seriesParams) + ); + const enableHistogramMode = + (params.dimensions.x?.aggType === BUCKET_TYPES.DATE_HISTOGRAM || + params.dimensions.x?.aggType === BUCKET_TYPES.HISTOGRAM) && + shouldEnableHistogramMode(params.seriesParams, aspects.y, yAxes); + const isTimeChart = (aspects.x.params as DateHistogramParams).date ?? false; + + return { + // NOTE: downscale ratio to match current vislib implementation + markSizeRatio: radiusRatio * 0.6, + fittingFunction, + detailedTooltip, + orderBucketsBySum, + isTimeChart, + isVislibVis, + showCurrentTime: addTimeMarker && isTimeChart, + showValueLabel: labels.show ?? false, + enableHistogramMode, + tooltip, + aspects, + xAxis, + yAxes, + legend: getLegend(params), + rotation: getRotation(params.categoryAxes[0]), + thresholdLine: getThresholdLine(thresholdLine, yAxes, params.seriesParams), + }; +} + +/** + * disables histogram mode for any config that has non-stacked clustered bars + * + * @param seriesParams + * @param yAspects + * @param yAxes + */ +const shouldEnableHistogramMode = ( + seriesParams: SeriesParam[], + yAspects: Aspect[], + yAxes: Array> +): boolean => { + const bars = seriesParams.filter(({ type, data: { id: paramId } }) => { + return ( + type === ChartType.Histogram && yAspects.find(({ aggId }) => aggId === paramId) !== undefined + ); + }); + + if (bars.length === 1) { + return true; + } + + const groupIds = [ + ...bars.reduce>((acc, { valueAxis: groupId, mode }) => { + acc.add(groupId); + return acc; + }, new Set()), + ]; + + if (groupIds.length > 1) { + return false; + } + + const test = bars.every(({ valueAxis: groupId, mode }) => { + const yAxisScale = yAxes.find(({ groupId: axisGroupId }) => axisGroupId === groupId)?.scale; + + return mode === 'stacked' || yAxisScale?.mode === 'percentage'; + }); + + return test; +}; diff --git a/src/plugins/vis_type_xy/public/config/get_legend.ts b/src/plugins/vis_type_xy/public/config/get_legend.ts new file mode 100644 index 0000000000000..a376ec3c4fb96 --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_legend.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendOptions, VisParams } from '../types'; + +export function getLegend({ addLegend, legendPosition }: VisParams): LegendOptions { + return { + show: addLegend, + position: legendPosition, + }; +} diff --git a/src/plugins/vis_type_xy/public/config/get_rotation.ts b/src/plugins/vis_type_xy/public/config/get_rotation.ts new file mode 100644 index 0000000000000..3c1e9a8f9130a --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_rotation.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Rotation } from '@elastic/charts'; + +import { CategoryAxis } from '../types'; + +export function getRotation({ position }: CategoryAxis): Rotation { + if (position === 'left' || position === 'right') { + return 90; + } + + return 0; +} diff --git a/src/plugins/vis_type_xy/public/config/get_threshold_line.ts b/src/plugins/vis_type_xy/public/config/get_threshold_line.ts new file mode 100644 index 0000000000000..861769f829074 --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_threshold_line.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + ThresholdLineConfig, + ThresholdLine, + ThresholdLineStyle, + AxisConfig, + SeriesParam, + YScaleType, +} from '../types'; + +export function getThresholdLine( + { style, ...rest }: ThresholdLine, + yAxes: Array>, + seriesParams: SeriesParam[] +): ThresholdLineConfig { + const groupId = yAxes.find(({ id }) => seriesParams.some(({ valueAxis }) => id === valueAxis)) + ?.groupId; + + return { + ...rest, + dash: getDash(style), + groupId, + }; +} + +function getDash(style: ThresholdLineStyle): number[] | undefined { + switch (style) { + case ThresholdLineStyle.Dashed: + return [10, 5]; + case ThresholdLineStyle.DotDashed: + return [20, 5, 5, 5]; + case ThresholdLineStyle.Full: + default: + return; + } +} diff --git a/src/plugins/vis_type_xy/public/config/get_tooltip.ts b/src/plugins/vis_type_xy/public/config/get_tooltip.ts new file mode 100644 index 0000000000000..87146eeca9155 --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_tooltip.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TooltipType } from '@elastic/charts'; + +import { Aspects, VisParams, TooltipConfig } from '../types'; +import { getDetailedTooltip } from '../components/detailed_tooltip'; + +export function getTooltip( + aspects: Aspects, + { addTooltip, detailedTooltip }: VisParams +): TooltipConfig { + return { + type: addTooltip ? TooltipType.VerticalCursor : TooltipType.None, + detailedTooltip: detailedTooltip ? getDetailedTooltip(aspects) : undefined, + }; +} diff --git a/src/plugins/vis_type_xy/public/config/index.ts b/src/plugins/vis_type_xy/public/config/index.ts new file mode 100644 index 0000000000000..2b0e32f726a6c --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { getConfig } from './get_config'; +export { getAggId } from './get_agg_id'; diff --git a/src/plugins/vis_type_xy/public/editor/collections.ts b/src/plugins/vis_type_xy/public/editor/collections.ts new file mode 100644 index 0000000000000..0149bc476f5c5 --- /dev/null +++ b/src/plugins/vis_type_xy/public/editor/collections.ts @@ -0,0 +1,201 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Fit } from '@elastic/charts'; + +import { AxisMode, ChartMode, InterpolationMode, ThresholdLineStyle } from '../types'; +import { ChartType } from '../../common'; +import { LabelRotation } from '../../../charts/public'; +import { getScaleTypes } from './scale_types'; +import { getPositions } from './positions'; + +export { getScaleTypes, getPositions }; + +export const getChartTypes = () => [ + { + text: i18n.translate('visTypeXy.chartTypes.lineText', { + defaultMessage: 'Line', + }), + value: ChartType.Line, + }, + { + text: i18n.translate('visTypeXy.chartTypes.areaText', { + defaultMessage: 'Area', + }), + value: ChartType.Area, + }, + { + text: i18n.translate('visTypeXy.chartTypes.barText', { + defaultMessage: 'Bar', + }), + value: ChartType.Histogram, + }, +]; + +export const getChartModes = () => [ + { + text: i18n.translate('visTypeXy.chartModes.normalText', { + defaultMessage: 'Normal', + }), + value: ChartMode.Normal, + }, + { + text: i18n.translate('visTypeXy.chartModes.stackedText', { + defaultMessage: 'Stacked', + }), + value: ChartMode.Stacked, + }, +]; + +export const getInterpolationModes = () => [ + { + text: i18n.translate('visTypeXy.interpolationModes.straightText', { + defaultMessage: 'Straight', + }), + value: InterpolationMode.Linear, + }, + { + text: i18n.translate('visTypeXy.interpolationModes.smoothedText', { + defaultMessage: 'Smoothed', + }), + value: InterpolationMode.Cardinal, + }, + { + text: i18n.translate('visTypeXy.interpolationModes.steppedText', { + defaultMessage: 'Stepped', + }), + value: InterpolationMode.StepAfter, + }, +]; + +export const getAxisModes = () => [ + { + text: i18n.translate('visTypeXy.axisModes.normalText', { + defaultMessage: 'Normal', + }), + value: AxisMode.Normal, + }, + { + text: i18n.translate('visTypeXy.axisModes.percentageText', { + defaultMessage: 'Percentage', + }), + value: AxisMode.Percentage, + }, + { + text: i18n.translate('visTypeXy.axisModes.wiggleText', { + defaultMessage: 'Wiggle', + }), + value: AxisMode.Wiggle, + }, + { + text: i18n.translate('visTypeXy.axisModes.silhouetteText', { + defaultMessage: 'Silhouette', + }), + value: AxisMode.Silhouette, + }, +]; + +export const getThresholdLineStyles = () => [ + { + value: ThresholdLineStyle.Full, + text: i18n.translate('visTypeXy.thresholdLine.style.fullText', { + defaultMessage: 'Full', + }), + }, + { + value: ThresholdLineStyle.Dashed, + text: i18n.translate('visTypeXy.thresholdLine.style.dashedText', { + defaultMessage: 'Dashed', + }), + }, + { + value: ThresholdLineStyle.DotDashed, + text: i18n.translate('visTypeXy.thresholdLine.style.dotdashedText', { + defaultMessage: 'Dot-dashed', + }), + }, +]; + +export const getRotateOptions = () => [ + { + text: i18n.translate('visTypeXy.categoryAxis.rotate.horizontalText', { + defaultMessage: 'Horizontal', + }), + value: LabelRotation.Horizontal, + }, + { + text: i18n.translate('visTypeXy.categoryAxis.rotate.verticalText', { + defaultMessage: 'Vertical', + }), + value: LabelRotation.Vertical, + }, + { + text: i18n.translate('visTypeXy.categoryAxis.rotate.angledText', { + defaultMessage: 'Angled', + }), + value: LabelRotation.Angled, + }, +]; + +export const getFittingFunctions = () => + [ + { + value: Fit.None, + text: i18n.translate('visTypeXy.fittingFunctionsTitle.none', { + defaultMessage: 'Hide (Do not fill gaps)', + }), + }, + { + value: Fit.Zero, + text: i18n.translate('visTypeXy.fittingFunctionsTitle.zero', { + defaultMessage: 'Zero (Fill gaps with zeros)', + }), + }, + { + value: Fit.Linear, + text: i18n.translate('visTypeXy.fittingFunctionsTitle.linear', { + defaultMessage: 'Linear (Fill gaps with a line)', + }), + }, + { + value: Fit.Carry, + text: i18n.translate('visTypeXy.fittingFunctionsTitle.carry', { + defaultMessage: 'Last (Fill gaps with the last value)', + }), + }, + { + value: Fit.Lookahead, + text: i18n.translate('visTypeXy.fittingFunctionsTitle.lookahead', { + defaultMessage: 'Next (Fill gaps with the next value)', + }), + }, + ] as const; + +export const getConfigCollections = () => ({ + legendPositions: getPositions(), + positions: getPositions(), + chartTypes: getChartTypes(), + axisModes: getAxisModes(), + scaleTypes: getScaleTypes(), + chartModes: getChartModes(), + interpolationModes: getInterpolationModes(), + thresholdLineStyles: getThresholdLineStyles(), + fittingFunctions: getFittingFunctions(), +}); diff --git a/src/plugins/vis_type_vislib/public/utils/common_config.tsx b/src/plugins/vis_type_xy/public/editor/common_config.tsx similarity index 59% rename from src/plugins/vis_type_vislib/public/utils/common_config.tsx rename to src/plugins/vis_type_xy/public/editor/common_config.tsx index de867dc72bba7..31d1a9d539621 100644 --- a/src/plugins/vis_type_vislib/public/utils/common_config.tsx +++ b/src/plugins/vis_type_xy/public/editor/common_config.tsx @@ -20,36 +20,37 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { PointSeriesOptions, MetricsAxisOptions } from '../components/options'; -import { ValidationWrapper } from '../components/common'; -import { BasicVislibParams } from '../types'; +import { VisOptionsProps } from '../../../vis_default_editor/public'; -function getAreaOptionTabs() { +import { VisParams } from '../types'; +import { MetricsAxisOptions, PointSeriesOptions } from './components/options'; +import { ValidationWrapper } from './components/common'; + +export function getOptionTabs(showElasticChartsOptions = false) { return [ { name: 'advanced', - title: i18n.translate('visTypeVislib.area.tabs.metricsAxesTitle', { + title: i18n.translate('visTypeXy.area.tabs.metricsAxesTitle', { defaultMessage: 'Metrics & axes', }), - editor: (props: VisOptionsProps) => ( + editor: (props: VisOptionsProps) => ( ), }, { name: 'options', - title: i18n.translate('visTypeVislib.area.tabs.panelSettingsTitle', { + title: i18n.translate('visTypeXy.area.tabs.panelSettingsTitle', { defaultMessage: 'Panel settings', }), - editor: (props: VisOptionsProps) => ( - + editor: (props: VisOptionsProps) => ( + ), }, ]; } - -const countLabel = i18n.translate('visTypeVislib.area.countText', { - defaultMessage: 'Count', -}); - -export { getAreaOptionTabs, countLabel }; diff --git a/src/plugins/vis_type_vislib/public/components/common/index.ts b/src/plugins/vis_type_xy/public/editor/components/common/index.ts similarity index 100% rename from src/plugins/vis_type_vislib/public/components/common/index.ts rename to src/plugins/vis_type_xy/public/editor/components/common/index.ts diff --git a/src/plugins/vis_type_vislib/public/components/common/truncate_labels.tsx b/src/plugins/vis_type_xy/public/editor/components/common/truncate_labels.tsx similarity index 96% rename from src/plugins/vis_type_vislib/public/components/common/truncate_labels.tsx rename to src/plugins/vis_type_xy/public/editor/components/common/truncate_labels.tsx index 6f55f6f070d00..ae1f34376bbf8 100644 --- a/src/plugins/vis_type_vislib/public/components/common/truncate_labels.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/common/truncate_labels.tsx @@ -33,7 +33,7 @@ function TruncateLabelsOption({ disabled, value = null, setValue }: TruncateLabe return ( extends VisOptionsProps { +export interface ValidationVisOptionsProps extends VisOptionsProps { setMultipleValidity(paramName: string, isValid: boolean): void; + extraProps?: E; } -interface ValidationWrapperProps extends VisOptionsProps { - component: React.ComponentType>; +interface ValidationWrapperProps extends VisOptionsProps { + component: React.ComponentType>; + extraProps?: E; } interface Item { isValid: boolean; } -function ValidationWrapper({ +function ValidationWrapper({ component: Component, ...rest -}: ValidationWrapperProps) { +}: ValidationWrapperProps) { const [panelState, setPanelState] = useState({} as { [key: string]: Item }); const isPanelValid = Object.values(panelState).every((item) => item.isValid); const { setValidity } = rest; diff --git a/src/plugins/vis_type_vislib/public/components/index.ts b/src/plugins/vis_type_xy/public/editor/components/index.ts similarity index 100% rename from src/plugins/vis_type_vislib/public/components/index.ts rename to src/plugins/vis_type_xy/public/editor/components/index.ts diff --git a/src/plugins/vis_type_xy/public/editor/components/options/index.tsx b/src/plugins/vis_type_xy/public/editor/components/options/index.tsx new file mode 100644 index 0000000000000..3eba7fa3ad818 --- /dev/null +++ b/src/plugins/vis_type_xy/public/editor/components/options/index.tsx @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; + +import { VisParams } from '../../../types'; +import { ValidationVisOptionsProps } from '../common'; + +const PointSeriesOptionsLazy = lazy(() => import('./point_series')); +const MetricsAxisOptionsLazy = lazy(() => import('./metrics_axes')); + +export const PointSeriesOptions = ( + props: ValidationVisOptionsProps< + VisParams, + { + showElasticChartsOptions: boolean; + } + > +) => ; + +export const MetricsAxisOptions = (props: ValidationVisOptionsProps) => ( + +); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap similarity index 94% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap index 2b7c03084ec65..bc66c1940ac72 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap @@ -10,7 +10,7 @@ exports[`CategoryAxisPanel component should init with the default set of props 1

diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap similarity index 99% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap index 09e0753d592e5..c5ce09d4d78af 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap @@ -74,7 +74,6 @@ exports[`MetricsAxisOptions component should init with the default set of props /> diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap similarity index 98% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap index ed7ae45eed3a5..594511010b745 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap @@ -18,7 +18,7 @@ exports[`ValueAxesPanel component should init with the default set of props 1`]

@@ -120,7 +120,6 @@ exports[`ValueAxesPanel component should init with the default set of props 1`] } } index={0} - isCategoryAxisHorizontal={false} onValueAxisPositionChanged={[MockFunction]} setMultipleValidity={[MockFunction]} setParamByIndex={[MockFunction]} @@ -303,7 +302,6 @@ exports[`ValueAxesPanel component should init with the default set of props 1`] } } index={1} - isCategoryAxisHorizontal={false} onValueAxisPositionChanged={[MockFunction]} setMultipleValidity={[MockFunction]} setParamByIndex={[MockFunction]} diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap similarity index 96% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap index c4142fb487b6a..3f3ee66a85924 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap @@ -7,22 +7,18 @@ exports[`ValueAxisOptions component should init with the default set of props 1` options={ Array [ Object { - "disabled": false, "text": "Top", "value": "top", }, Object { - "disabled": true, "text": "Left", "value": "left", }, Object { - "disabled": true, "text": "Right", "value": "right", }, Object { - "disabled": false, "text": "Bottom", "value": "bottom", }, diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.test.tsx similarity index 93% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.test.tsx index 44ed0d5aeddab..3bf844618720b 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.test.tsx @@ -20,16 +20,16 @@ import React from 'react'; import { shallow } from 'enzyme'; import { CategoryAxisPanel, CategoryAxisPanelProps } from './category_axis_panel'; -import { Axis } from '../../../types'; -import { Positions } from '../../../utils/collections'; +import { CategoryAxis } from '../../../../types'; import { LabelOptions } from './label_options'; import { categoryAxis, vis } from './mocks'; +import { Position } from '@elastic/charts'; describe('CategoryAxisPanel component', () => { let setCategoryAxis: jest.Mock; let onPositionChanged: jest.Mock; let defaultProps: CategoryAxisPanelProps; - let axis: Axis; + let axis: CategoryAxis; beforeEach(() => { setCategoryAxis = jest.fn(); @@ -60,7 +60,7 @@ describe('CategoryAxisPanel component', () => { }); it('should call onPositionChanged when position is changed', () => { - const value = Positions.RIGHT; + const value = Position.Right; const comp = shallow(); comp.find({ paramName: 'position' }).prop('setValue')('position', value); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx similarity index 78% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx index 468fb1f8c315a..a551163747526 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx @@ -19,20 +19,21 @@ import React, { useCallback } from 'react'; -import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { Position } from '@elastic/charts'; + +import { SelectOption, SwitchOption } from '../../../../../../charts/public'; +import { VisOptionsProps } from '../../../../../../vis_default_editor/public'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { Axis } from '../../../types'; -import { SelectOption, SwitchOption } from '../../../../../charts/public'; import { LabelOptions, SetAxisLabel } from './label_options'; -import { Positions } from '../../../utils/collections'; +import { CategoryAxis } from '../../../../types'; export interface CategoryAxisPanelProps { - axis: Axis; - onPositionChanged: (position: Positions) => void; - setCategoryAxis: (value: Axis) => void; + axis: CategoryAxis; + onPositionChanged: (position: Position) => void; + setCategoryAxis: (value: CategoryAxis) => void; vis: VisOptionsProps['vis']; } @@ -43,7 +44,7 @@ function CategoryAxisPanel({ setCategoryAxis, }: CategoryAxisPanelProps) { const setAxis = useCallback( - (paramName: T, value: Axis[T]) => { + (paramName: T, value: CategoryAxis[T]) => { const updatedAxis = { ...axis, [paramName]: value, @@ -54,7 +55,7 @@ function CategoryAxisPanel({ ); const setPosition = useCallback( - (paramName: 'position', value: Axis['position']) => { + (paramName: 'position', value: CategoryAxis['position']) => { setAxis(paramName, value); onPositionChanged(value); }, @@ -77,7 +78,7 @@ function CategoryAxisPanel({

@@ -85,7 +86,7 @@ function CategoryAxisPanel({ { let setParamByIndex: jest.Mock; @@ -53,14 +54,14 @@ describe('ChartOptions component', () => { }); it('should show LineOptions when type is line', () => { - chart.type = ChartTypes.LINE; + chart.type = ChartType.Line; const comp = shallow(); expect(comp.find(LineOptions).exists()).toBeTruthy(); }); it('should show line mode when type is area', () => { - chart.type = ChartTypes.AREA; + chart.type = ChartType.Area; const comp = shallow(); expect(comp.find({ paramName: 'interpolate' }).exists()).toBeTruthy(); @@ -78,8 +79,8 @@ describe('ChartOptions component', () => { it('should call setParamByIndex when mode is changed', () => { const comp = shallow(); const paramName = 'mode'; - comp.find({ paramName }).prop('setValue')(paramName, ChartModes.NORMAL); + comp.find({ paramName }).prop('setValue')(paramName, ChartMode.Normal); - expect(setParamByIndex).toBeCalledWith('seriesParams', 0, paramName, ChartModes.NORMAL); + expect(setParamByIndex).toBeCalledWith('seriesParams', 0, paramName, ChartMode.Normal); }); }); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx similarity index 80% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx index 623a8d1f348e9..c379fa30b49b8 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx @@ -22,12 +22,13 @@ import React, { useMemo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { Vis } from '../../../../../visualizations/public'; -import { SeriesParam, ValueAxis } from '../../../types'; -import { ChartTypes } from '../../../utils/collections'; -import { SelectOption } from '../../../../../charts/public'; +import { Vis } from '../../../../../../visualizations/public'; +import { SelectOption } from '../../../../../../charts/public'; + +import { SeriesParam, ValueAxis } from '../../../../types'; import { LineOptions } from './line_options'; import { SetParamByIndex, ChangeValueAxis } from '.'; +import { ChartType } from '../../../../../common'; export type SetChart = (paramName: T, value: SeriesParam[T]) => void; @@ -69,7 +70,7 @@ function ChartOptions({ value: id, })), { - text: i18n.translate('visTypeVislib.controls.pointSeries.series.newAxisLabel', { + text: i18n.translate('visTypeXy.controls.pointSeries.series.newAxisLabel', { defaultMessage: 'New axis…', }), value: 'new', @@ -82,7 +83,7 @@ function ChartOptions({ <> - {chart.type === ChartTypes.AREA && ( + {chart.type === ChartType.Area && ( <> )} - {chart.type === ChartTypes.LINE && ( - - )} + {chart.type === ChartType.Line && } ); } diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.test.tsx similarity index 99% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.test.tsx index 4798c67928f7f..82097271f8814 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.test.tsx @@ -19,6 +19,7 @@ import React from 'react'; import { shallow, mount } from 'enzyme'; + import { CustomExtentsOptions, CustomExtentsOptionsProps } from './custom_extents_options'; import { YExtents } from './y_extents'; import { valueAxis } from './mocks'; diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx similarity index 87% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx index 634d6b3f0641c..86a0c56e46942 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx @@ -18,10 +18,12 @@ */ import React, { useCallback, useEffect } from 'react'; + import { i18n } from '@kbn/i18n'; -import { ValueAxis } from '../../../types'; -import { NumberInputOption, SwitchOption } from '../../../../../charts/public'; +import { NumberInputOption, SwitchOption } from '../../../../../../charts/public'; + +import { ValueAxis } from '../../../../types'; import { YExtents } from './y_extents'; import { SetScale } from './value_axis_options'; @@ -39,7 +41,7 @@ function CustomExtentsOptions({ setValueAxisScale, }: CustomExtentsOptionsProps) { const invalidBoundsMarginMessage = i18n.translate( - 'visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin', + 'visTypeXy.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin', { defaultMessage: 'Bounds margin must be greater than or equal to 0.' } ); @@ -84,12 +86,9 @@ function CustomExtentsOptions({ return ( <> ({ + SeriesPanel: () => 'SeriesPanel', +})); +jest.mock('./category_axis_panel', () => ({ + CategoryAxisPanel: () => 'CategoryAxisPanel', +})); +jest.mock('./value_axes_panel', () => ({ + ValueAxesPanel: () => 'ValueAxesPanel', +})); + +const SERIES_PARAMS = 'seriesParams'; +const VALUE_AXES = 'valueAxes'; + +const aggCount: IAggConfig = { + id: '1', + type: { name: 'count' }, + makeLabel: () => 'Count', +} as IAggConfig; + +const aggAverage: IAggConfig = { + id: '2', + type: { name: 'average' } as IAggType, + makeLabel: () => 'Average', +} as IAggConfig; + +const createAggs = (aggs: any[]) => ({ + aggs, + bySchemaName: () => aggs, +}); + +describe('MetricsAxisOptions component', () => { + let setValue: jest.Mock; + let defaultProps: ValidationVisOptionsProps; + let axis: ValueAxis; + let axisRight: ValueAxis; + let chart: SeriesParam; + + beforeEach(() => { + setValue = jest.fn(); + + axis = { + ...valueAxis, + name: 'LeftAxis-1', + position: Position.Left, + }; + axisRight = { + ...valueAxis, + id: 'ValueAxis-2', + name: 'RightAxis-1', + position: Position.Right, + }; + chart = { + ...seriesParam, + type: ChartType.Area, + }; + + defaultProps = { + aggs: createAggs([aggCount]), + isTabSelected: true, + vis: { + type: { + type: ChartType.Area, + schemas: { metrics: [{ name: 'metric' }] }, + }, + setState: jest.fn(), + serialize: jest.fn(), + }, + stateParams: { + valueAxes: [axis], + seriesParams: [chart], + categoryAxes: [categoryAxis], + grid: { + valueAxis: defaultValueAxisId, + }, + }, + setValue, + } as any; + }); + + it('should init with the default set of props', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + describe('useEffect', () => { + it('should update series when new agg is added', () => { + const component = mount(); + component.setProps({ + aggs: createAggs([aggCount, aggAverage]), + }); + + const updatedSeries = [chart, { ...chart, data: { id: '2', label: aggAverage.makeLabel() } }]; + expect(setValue).toHaveBeenLastCalledWith(SERIES_PARAMS, updatedSeries); + }); + + it('should update series when new agg label is changed', () => { + const component = mount(); + const agg = { id: aggCount.id, makeLabel: () => 'New label' }; + component.setProps({ + aggs: createAggs([agg]), + }); + + const updatedSeries = [{ ...chart, data: { id: agg.id, label: agg.makeLabel() } }]; + expect(setValue).toHaveBeenCalledWith(SERIES_PARAMS, updatedSeries); + }); + }); + + describe('updateAxisTitle', () => { + it('should not update the value axis title if custom title was set', () => { + defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; + const component = mount(); + const newAgg = { + ...aggCount, + makeLabel: () => 'Custom label', + }; + component.setProps({ + aggs: createAggs([newAgg]), + }); + const updatedValues = [{ ...axis, title: { text: newAgg.makeLabel() } }]; + expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES, updatedValues); + }); + + it('should set the custom title to match the value axis label when only one agg exists for that axis', () => { + const component = mount(); + const agg = { + id: aggCount.id, + params: { customLabel: 'Custom label' }, + makeLabel: () => 'Custom label', + }; + component.setProps({ + aggs: createAggs([agg]), + }); + + const updatedSeriesParams = [{ ...chart, data: { ...chart.data, label: agg.makeLabel() } }]; + const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; + + expect(setValue).toHaveBeenCalledTimes(5); + expect(setValue).toHaveBeenNthCalledWith(3, SERIES_PARAMS, updatedSeriesParams); + expect(setValue).toHaveBeenNthCalledWith(5, SERIES_PARAMS, updatedSeriesParams); + expect(setValue).toHaveBeenNthCalledWith(4, VALUE_AXES, updatedValues); + }); + + it('should not set the custom title to match the value axis label when more than one agg exists for that axis', () => { + const component = mount(); + const agg = { id: aggCount.id, makeLabel: () => 'Custom label' }; + component.setProps({ + aggs: createAggs([agg, aggAverage]), + stateParams: { + ...defaultProps.stateParams, + seriesParams: [chart, chart], + }, + }); + + expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); + }); + + it('should not overwrite the custom title with the value axis label if the custom title has been changed', () => { + defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; + const component = mount(); + const agg = { + id: aggCount.id, + params: { customLabel: 'Custom label' }, + makeLabel: () => 'Custom label', + }; + component.setProps({ + aggs: createAggs([agg]), + }); + + expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); + }); + }); + + it('should add value axis', () => { + const component = shallow(); + component.find(ValueAxesPanel).prop('addValueAxis')(); + + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, [axis, axisRight]); + }); + + describe('removeValueAxis', () => { + beforeEach(() => { + defaultProps.stateParams.valueAxes = [axis, axisRight]; + }); + + it('should remove value axis', () => { + const component = shallow(); + component.find(ValueAxesPanel).prop('removeValueAxis')(axis); + + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, [axisRight]); + }); + + it('should update seriesParams "valueAxis" prop', () => { + const updatedSeriesParam = { ...chart, valueAxis: 'ValueAxis-2' }; + const component = shallow(); + component.find(ValueAxesPanel).prop('removeValueAxis')(axis); + + expect(setValue).toHaveBeenCalledWith(SERIES_PARAMS, [updatedSeriesParam]); + }); + + it('should reset grid "valueAxis" prop', () => { + const updatedGrid = { valueAxis: undefined }; + defaultProps.stateParams.seriesParams[0].valueAxis = 'ValueAxis-2'; + const component = shallow(); + component.find(ValueAxesPanel).prop('removeValueAxis')(axis); + + expect(setValue).toHaveBeenCalledWith('grid', updatedGrid); + }); + }); + + describe('onValueAxisPositionChanged', () => { + const getProps = ( + valuePosition1: Position = Position.Right, + valuePosition2: Position = Position.Left + ): ValidationVisOptionsProps => ({ + ...defaultProps, + stateParams: { + ...defaultProps.stateParams, + valueAxes: [ + { + ...valueAxis, + id: 'ValueAxis-1', + position: valuePosition1, + }, + { + ...valueAxis, + id: 'ValueAxis-2', + position: valuePosition2, + }, + { + ...valueAxis, + id: 'ValueAxis-3', + position: valuePosition2, + }, + ], + categoryAxes: [ + { + ...categoryAxis, + position: mapPosition(valuePosition1), + }, + ], + }, + }); + + it('should update all value axes if another value axis changes from horizontal to vertical', () => { + const component = mount(); + setValue.mockClear(); + const onValueAxisPositionChanged = component + .find(ValueAxesPanel) + .prop('onValueAxisPositionChanged'); + onValueAxisPositionChanged(0, Position.Bottom); + expect(setValue).nthCalledWith(1, 'categoryAxes', [ + expect.objectContaining({ + id: 'CategoryAxis-1', + position: Position.Right, + }), + ]); + expect(setValue).nthCalledWith(2, 'valueAxes', [ + expect.objectContaining({ + id: 'ValueAxis-1', + position: Position.Bottom, + }), + expect.objectContaining({ + id: 'ValueAxis-2', + position: Position.Top, + }), + expect.objectContaining({ + id: 'ValueAxis-3', + position: Position.Top, + }), + ]); + }); + + it('should update all value axes if another value axis changes from vertical to horizontal', () => { + const component = mount(); + setValue.mockClear(); + const onValueAxisPositionChanged = component + .find(ValueAxesPanel) + .prop('onValueAxisPositionChanged'); + onValueAxisPositionChanged(1, Position.Left); + expect(setValue).nthCalledWith(1, 'categoryAxes', [ + expect.objectContaining({ + id: 'CategoryAxis-1', + position: Position.Top, + }), + ]); + expect(setValue).nthCalledWith(2, 'valueAxes', [ + expect.objectContaining({ + id: 'ValueAxis-1', + position: Position.Right, + }), + expect.objectContaining({ + id: 'ValueAxis-2', + position: Position.Left, + }), + expect.objectContaining({ + id: 'ValueAxis-3', + position: Position.Left, + }), + ]); + }); + + it('should update only changed value axis if value axis stays horizontal', () => { + const component = mount(); + setValue.mockClear(); + const onValueAxisPositionChanged = component + .find(ValueAxesPanel) + .prop('onValueAxisPositionChanged'); + onValueAxisPositionChanged(0, Position.Left); + expect(setValue).nthCalledWith(1, 'valueAxes', [ + expect.objectContaining({ + id: 'ValueAxis-1', + position: Position.Left, + }), + expect.objectContaining({ + id: 'ValueAxis-2', + position: Position.Left, + }), + expect.objectContaining({ + id: 'ValueAxis-3', + position: Position.Left, + }), + ]); + }); + + it('should update only changed value axis if value axis stays vertical', () => { + const component = mount(); + setValue.mockClear(); + const onValueAxisPositionChanged = component + .find(ValueAxesPanel) + .prop('onValueAxisPositionChanged'); + onValueAxisPositionChanged(1, Position.Top); + expect(setValue).nthCalledWith(1, 'valueAxes', [ + expect.objectContaining({ + id: 'ValueAxis-1', + position: Position.Top, + }), + expect.objectContaining({ + id: 'ValueAxis-2', + position: Position.Top, + }), + expect.objectContaining({ + id: 'ValueAxis-3', + position: Position.Bottom, + }), + ]); + }); + }); + + describe('onCategoryAxisPositionChanged', () => { + const getProps = ( + position: Position = Position.Bottom + ): ValidationVisOptionsProps => ({ + ...defaultProps, + stateParams: { + ...defaultProps.stateParams, + valueAxes: [ + { + ...valueAxis, + id: 'ValueAxis-1', + position: mapPosition(position), + }, + { + ...valueAxis, + id: 'ValueAxis-2', + position: mapPositionOpposite(mapPosition(position)), + }, + { + ...valueAxis, + id: 'ValueAxis-3', + position: mapPosition(position), + }, + ], + categoryAxes: [ + { + ...categoryAxis, + position, + }, + ], + }, + }); + + it('should update all value axes if category axis changes from horizontal to vertical', () => { + const component = mount(); + setValue.mockClear(); + const onPositionChanged = component.find(CategoryAxisPanel).prop('onPositionChanged'); + onPositionChanged(Position.Left); + expect(setValue).nthCalledWith(1, 'valueAxes', [ + expect.objectContaining({ + id: 'ValueAxis-1', + position: Position.Bottom, + }), + expect.objectContaining({ + id: 'ValueAxis-2', + position: Position.Top, + }), + expect.objectContaining({ + id: 'ValueAxis-3', + position: Position.Bottom, + }), + ]); + }); + + it('should update all value axes if category axis changes from vertical to horizontal', () => { + const component = mount(); + setValue.mockClear(); + const onPositionChanged = component.find(CategoryAxisPanel).prop('onPositionChanged'); + onPositionChanged(Position.Top); + expect(setValue).nthCalledWith(1, 'valueAxes', [ + expect.objectContaining({ + id: 'ValueAxis-1', + position: Position.Left, + }), + expect.objectContaining({ + id: 'ValueAxis-2', + position: Position.Right, + }), + expect.objectContaining({ + id: 'ValueAxis-3', + position: Position.Left, + }), + ]); + }); + + it('should not update value axes if category axis stays horizontal', () => { + const component = mount(); + setValue.mockClear(); + const onPositionChanged = component.find(CategoryAxisPanel).prop('onPositionChanged'); + onPositionChanged(Position.Top); + expect(setValue).not.toBeCalled(); + }); + + it('should not update value axes if category axis stays vertical', () => { + const component = mount(); + setValue.mockClear(); + const onPositionChanged = component.find(CategoryAxisPanel).prop('onPositionChanged'); + onPositionChanged(Position.Right); + expect(setValue).not.toBeCalled(); + }); + }); +}); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx similarity index 83% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx index 0862c47c35cff..9b4bf3127d45e 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx @@ -19,10 +19,12 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { cloneDeep, get } from 'lodash'; + import { EuiSpacer } from '@elastic/eui'; -import { IAggConfig } from 'src/plugins/data/public'; -import { BasicVislibParams, ValueAxis, SeriesParam, Axis } from '../../../types'; +import { IAggConfig } from '../../../../../../data/public'; + +import { VisParams, ValueAxis, SeriesParam, CategoryAxis } from '../../../../types'; import { ValidationVisOptionsProps } from '../../common'; import { SeriesPanel } from './series_panel'; import { CategoryAxisPanel } from './category_axis_panel'; @@ -34,6 +36,7 @@ import { getUpdatedAxisName, mapPositionOpposite, mapPosition, + mapPositionOpposingOpposite, } from './utils'; export type SetParamByIndex =

( @@ -51,11 +54,9 @@ export type ChangeValueAxis = ( const VALUE_AXIS_PREFIX = 'ValueAxis-'; -function MetricsAxisOptions(props: ValidationVisOptionsProps) { +function MetricsAxisOptions(props: ValidationVisOptionsProps) { const { stateParams, setValue, aggs, vis, isTabSelected } = props; - const [isCategoryAxisHorizontal, setIsCategoryAxisHorizontal] = useState(true); - const setParamByIndex: SetParamByIndex = useCallback( (axesName, index, paramName, value) => { const items = stateParams[axesName]; @@ -72,7 +73,7 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) ); const setCategoryAxis = useCallback( - (value: Axis) => { + (value: CategoryAxis) => { const categoryAxes = [...stateParams.categoryAxes]; categoryAxes[0] = value; setValue('categoryAxes', categoryAxes); @@ -170,33 +171,58 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) ); const onValueAxisPositionChanged = useCallback( - (index: number, value: ValueAxis['position']) => { + (index: number, axisPosition: ValueAxis['position']) => { + const isHorizontalAxis = isAxisHorizontal(axisPosition); const valueAxes = [...stateParams.valueAxes]; - const name = getUpdatedAxisName(value, valueAxes); + const name = getUpdatedAxisName(axisPosition, valueAxes); + const [categoryAxes] = stateParams.categoryAxes; - valueAxes[index] = { - ...valueAxes[index], - name, - position: value, - }; - setValue('valueAxes', valueAxes); + if (isAxisHorizontal(categoryAxes.position) === isHorizontalAxis) { + const updatedCategoryAxes = { + ...categoryAxes, + position: mapPosition(categoryAxes.position), + }; + + setValue('categoryAxes', [updatedCategoryAxes]); + + const oldPosition = valueAxes[index].position; + const newValueAxes = valueAxes.map(({ position, ...axis }, i) => ({ + ...axis, + position: + i === index + ? axisPosition + : mapPositionOpposingOpposite(position, oldPosition, axisPosition), + })); + setValue('valueAxes', newValueAxes); + } else { + valueAxes[index] = { + ...valueAxes[index], + name, + position: axisPosition, + }; + setValue('valueAxes', valueAxes); + } }, - [stateParams.valueAxes, setValue] + [stateParams.valueAxes, stateParams.categoryAxes, setValue] ); const onCategoryAxisPositionChanged = useCallback( - (chartPosition: Axis['position']) => { - const isChartHorizontal = isAxisHorizontal(chartPosition); - setIsCategoryAxisHorizontal(isAxisHorizontal(chartPosition)); - - stateParams.valueAxes.forEach((axis, index) => { - if (isAxisHorizontal(axis.position) === isChartHorizontal) { - const position = mapPosition(axis.position); - onValueAxisPositionChanged(index, position); - } - }); + (axisPosition: CategoryAxis['position']) => { + const isHorizontalAxis = isAxisHorizontal(axisPosition); + + if ( + stateParams.valueAxes.some( + ({ position }) => isAxisHorizontal(position) === isHorizontalAxis + ) + ) { + const newValueAxes = stateParams.valueAxes.map(({ position, ...axis }) => ({ + ...axis, + position: mapPosition(position), + })); + setValue('valueAxes', newValueAxes); + } }, - [stateParams.valueAxes, onValueAxisPositionChanged] + [setValue, stateParams.valueAxes] ); const addValueAxis = useCallback(() => { @@ -305,7 +331,6 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) ( - paramName: T, - value: Axis['labels'][T] -) => void; +export type SetAxisLabel = (paramName: T, value: Labels[T]) => void; export interface LabelOptionsProps { - axisLabels: Axis['labels']; + axisLabels: Labels; axisFilterCheckboxName: string; setAxisLabel: SetAxisLabel; } function LabelOptions({ axisLabels, axisFilterCheckboxName, setAxisLabel }: LabelOptionsProps) { const setAxisLabelRotate = useCallback( - (paramName: 'rotate', value: Axis['labels']['rotate']) => { + (paramName: 'rotate', value: Labels['rotate']) => { setAxisLabel(paramName, Number(value)); }, [setAxisLabel] @@ -54,7 +51,7 @@ function LabelOptions({ axisLabels, axisFilterCheckboxName, setAxisLabel }: Labe

@@ -62,7 +59,7 @@ function LabelOptions({ axisLabels, axisFilterCheckboxName, setAxisLabel }: Labe

@@ -57,13 +58,10 @@ function SeriesPanel({ seriesParams, ...chartProps }: SeriesPanelProps) { initialIsOpen={index === 0} buttonContent={chart.data.label} buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" - aria-label={i18n.translate( - 'visTypeVislib.controls.pointSeries.seriesAccordionAriaLabel', - { - defaultMessage: 'Toggle {agg} options', - values: { agg: chart.data.label }, - } - )} + aria-label={i18n.translate('visTypeXy.controls.pointSeries.seriesAccordionAriaLabel', { + defaultMessage: 'Toggle {agg} options', + values: { agg: chart.data.label }, + })} > <> diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/utils.ts similarity index 53% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/utils.ts index 708e8cf15f029..58216ee8953cd 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/utils.ts @@ -16,13 +16,14 @@ * specific language governing permissions and limitations * under the License. */ - import { upperFirst } from 'lodash'; -import { BasicVislibParams, ValueAxis, SeriesParam } from '../../../types'; -import { ChartModes, ChartTypes, InterpolationModes, Positions } from '../../../utils/collections'; +import { Position } from '@elastic/charts'; + +import { VisParams, ValueAxis, SeriesParam, ChartMode, InterpolationMode } from '../../../../types'; +import { ChartType } from '../../../../../common'; -const makeSerie = ( +export const makeSerie = ( id: string, label: string, defaultValueAxis: ValueAxis['id'], @@ -31,11 +32,11 @@ const makeSerie = ( const data = { id, label }; const defaultSerie = { show: true, - mode: ChartModes.NORMAL, - type: ChartTypes.LINE, + mode: ChartMode.Normal, + type: ChartType.Line, drawLinesBetweenPoints: true, showCircles: true, - interpolate: InterpolationModes.LINEAR, + interpolate: InterpolationMode.Linear, lineWidth: 2, valueAxis: defaultValueAxis, data, @@ -43,12 +44,12 @@ const makeSerie = ( return lastSerie ? { ...lastSerie, data } : defaultSerie; }; -const isAxisHorizontal = (position: Positions) => - [Positions.TOP, Positions.BOTTOM].includes(position as any); +export const isAxisHorizontal = (position: Position) => + [Position.Top, Position.Bottom].includes(position as any); const RADIX = 10; -function countNextAxisNumber(axisName: string, axisProp: 'id' | 'name' = 'id') { +export function countNextAxisNumber(axisName: string, axisProp: 'id' | 'name' = 'id') { return (value: number, axis: ValueAxis) => { const nameLength = axisName.length; if (axis[axisProp].substr(0, nameLength) === axisName) { @@ -63,9 +64,9 @@ function countNextAxisNumber(axisName: string, axisProp: 'id' | 'name' = 'id') { const AXIS_PREFIX = 'Axis-'; -const getUpdatedAxisName = ( +export const getUpdatedAxisName = ( axisPosition: ValueAxis['position'], - valueAxes: BasicVislibParams['valueAxes'] + valueAxes: VisParams['valueAxes'] ) => { const axisName = upperFirst(axisPosition) + AXIS_PREFIX; const nextAxisNameNumber = valueAxes.reduce(countNextAxisNumber(axisName, 'name'), 1); @@ -73,39 +74,56 @@ const getUpdatedAxisName = ( return `${axisName}${nextAxisNameNumber}`; }; -function mapPositionOpposite(position: Positions) { +/** + * Maps axis position to opposite position + * @param position + */ +export function mapPositionOpposite(position: Position) { switch (position) { - case Positions.BOTTOM: - return Positions.TOP; - case Positions.TOP: - return Positions.BOTTOM; - case Positions.LEFT: - return Positions.RIGHT; - case Positions.RIGHT: - return Positions.LEFT; + case Position.Bottom: + return Position.Top; + case Position.Top: + return Position.Bottom; + case Position.Left: + return Position.Right; + case Position.Right: + return Position.Left; default: throw new Error('Invalid legend position.'); } } -function mapPosition(position: Positions) { - switch (position) { - case Positions.BOTTOM: - return Positions.LEFT; - case Positions.TOP: - return Positions.RIGHT; - case Positions.LEFT: - return Positions.BOTTOM; - case Positions.RIGHT: - return Positions.TOP; +/** + * Maps axis position to new position or opposite of new position based on old position + * @param position + * @param oldPosition + * @param newPosition + */ +export function mapPositionOpposingOpposite( + position: Position, + oldPosition: Position, + newPosition: Position +) { + if (position === oldPosition) { + return newPosition; } + + return mapPositionOpposite(newPosition); } -export { - makeSerie, - isAxisHorizontal, - countNextAxisNumber, - getUpdatedAxisName, - mapPositionOpposite, - mapPosition, -}; +/** + * Maps axis position to opposite rotation position + * @param position + */ +export function mapPosition(position: Position) { + switch (position) { + case Position.Bottom: + return Position.Left; + case Position.Top: + return Position.Right; + case Position.Left: + return Position.Bottom; + case Position.Right: + return Position.Top; + } +} diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.test.tsx similarity index 96% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.test.tsx index 070433ca03f6d..f3286029db32f 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.test.tsx @@ -19,10 +19,12 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { ValueAxesPanel, ValueAxesPanelProps } from './value_axes_panel'; -import { ValueAxis, SeriesParam } from '../../../types'; -import { Positions } from '../../../utils/collections'; + import { mountWithIntl } from '@kbn/test/jest'; +import { Position } from '@elastic/charts'; + +import { ValueAxis, SeriesParam } from '../../../../types'; +import { ValueAxesPanel, ValueAxesPanelProps } from './value_axes_panel'; import { valueAxis, seriesParam, vis } from './mocks'; describe('ValueAxesPanel component', () => { @@ -47,7 +49,7 @@ describe('ValueAxesPanel component', () => { axisRight = { ...valueAxis, id: 'ValueAxis-2', - position: Positions.RIGHT, + position: Position.Right, }; seriesParamCount = { ...seriesParam }; seriesParamAverage = { @@ -63,7 +65,6 @@ describe('ValueAxesPanel component', () => { seriesParams: [seriesParamCount, seriesParamAverage], valueAxes: [axisLeft, axisRight], vis, - isCategoryAxisHorizontal: false, setParamByIndex, onValueAxisPositionChanged, addValueAxis, diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.tsx similarity index 90% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.tsx index 5f7d33b7f1f47..397704663ff1f 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.tsx @@ -31,13 +31,13 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Vis } from '../../../../../visualizations/public'; -import { SeriesParam, ValueAxis } from '../../../types'; +import { Vis } from '../../../../../../visualizations/public'; + +import { SeriesParam, ValueAxis } from '../../../../types'; import { ValueAxisOptions } from './value_axis_options'; import { SetParamByIndex } from '.'; export interface ValueAxesPanelProps { - isCategoryAxisHorizontal: boolean; addValueAxis: () => ValueAxis; removeValueAxis: (axis: ValueAxis) => void; onValueAxisPositionChanged: (index: number, value: ValueAxis['position']) => void; @@ -64,7 +64,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { const removeButtonTooltip = useMemo( () => - i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.removeButtonTooltip', { + i18n.translate('visTypeXy.controls.pointSeries.valueAxes.removeButtonTooltip', { defaultMessage: 'Remove Y-axis', }), [] @@ -87,7 +87,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { const addButtonTooltip = useMemo( () => - i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.addButtonTooltip', { + i18n.translate('visTypeXy.controls.pointSeries.valueAxes.addButtonTooltip', { defaultMessage: 'Add Y-axis', }), [] @@ -115,7 +115,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) {

@@ -146,7 +146,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { buttonClassName="eui-textTruncate" buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" aria-label={i18n.translate( - 'visTypeVislib.controls.pointSeries.valueAxes.toggleOptionsAriaLabel', + 'visTypeXy.controls.pointSeries.valueAxes.toggleOptionsAriaLabel', { defaultMessage: 'Toggle {axisName} options', values: { axisName: axis.name }, @@ -160,7 +160,6 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { axis={axis} index={index} valueAxis={valueAxes[index]} - isCategoryAxisHorizontal={props.isCategoryAxisHorizontal} onValueAxisPositionChanged={props.onValueAxisPositionChanged} setParamByIndex={props.setParamByIndex} setMultipleValidity={props.setMultipleValidity} diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx similarity index 64% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx index 1977bdba6eadf..0b325198c3fe7 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx @@ -19,21 +19,18 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { ValueAxisOptions, ValueAxisOptionsParams } from './value_axis_options'; -import { ValueAxis } from '../../../types'; -import { TextInputOption } from '../../../../../charts/public'; + +import { Position } from '@elastic/charts'; + +import { TextInputOption } from '../../../../../../charts/public'; + +import { ValueAxis, ScaleType } from '../../../../types'; import { LabelOptions } from './label_options'; -import { ScaleTypes, Positions } from '../../../utils/collections'; +import { ValueAxisOptions, ValueAxisOptionsParams } from './value_axis_options'; import { valueAxis, vis } from './mocks'; const POSITION = 'position'; -interface PositionOption { - text: string; - value: Positions; - disabled: boolean; -} - describe('ValueAxisOptions component', () => { let setParamByIndex: jest.Mock; let onValueAxisPositionChanged: jest.Mock; @@ -52,7 +49,6 @@ describe('ValueAxisOptions component', () => { index: 0, valueAxis, vis, - isCategoryAxisHorizontal: false, setParamByIndex, onValueAxisPositionChanged, setMultipleValidity, @@ -73,50 +69,8 @@ describe('ValueAxisOptions component', () => { expect(comp.find(LabelOptions).exists()).toBeFalsy(); }); - it('should only allow left and right value axis position when category axis is horizontal', () => { - defaultProps.isCategoryAxisHorizontal = true; - const comp = shallow(); - - const options: PositionOption[] = comp.find({ paramName: POSITION }).prop('options'); - - expect(options.length).toBe(4); - options.forEach(({ value, disabled }) => { - switch (value) { - case Positions.LEFT: - case Positions.RIGHT: - expect(disabled).toBeFalsy(); - break; - case Positions.TOP: - case Positions.BOTTOM: - expect(disabled).toBeTruthy(); - break; - } - }); - }); - - it('should only allow top and bottom value axis position when category axis is vertical', () => { - defaultProps.isCategoryAxisHorizontal = false; - const comp = shallow(); - - const options: PositionOption[] = comp.find({ paramName: POSITION }).prop('options'); - - expect(options.length).toBe(4); - options.forEach(({ value, disabled }) => { - switch (value) { - case Positions.LEFT: - case Positions.RIGHT: - expect(disabled).toBeTruthy(); - break; - case Positions.TOP: - case Positions.BOTTOM: - expect(disabled).toBeFalsy(); - break; - } - }); - }); - it('should call onValueAxisPositionChanged when position is changed', () => { - const value = Positions.RIGHT; + const value = Position.Right; const comp = shallow(); comp.find({ paramName: POSITION }).prop('setValue')(POSITION, value); @@ -135,7 +89,7 @@ describe('ValueAxisOptions component', () => { }); it('should call setValueAxis when scale value is changed', () => { - const scaleValue = ScaleTypes.SQUARE_ROOT; + const scaleValue = ScaleType.SquareRoot; const comp = shallow(); comp.find({ paramName: 'type' }).prop('setValue')('type', scaleValue); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx similarity index 76% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx index b42d038267d77..4ab792142e83a 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx @@ -17,17 +17,16 @@ * under the License. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiAccordion, EuiHorizontalRule } from '@elastic/eui'; -import { Vis } from '../../../../../visualizations/public'; -import { ValueAxis } from '../../../types'; -import { Positions } from '../../../utils/collections'; -import { SelectOption, SwitchOption, TextInputOption } from '../../../../../charts/public'; +import { Vis } from '../../../../../../visualizations/public'; +import { SelectOption, SwitchOption, TextInputOption } from '../../../../../../charts/public'; + +import { ValueAxis } from '../../../../types'; import { LabelOptions, SetAxisLabel } from './label_options'; import { CustomExtentsOptions } from './custom_extents_options'; -import { isAxisHorizontal } from './utils'; import { SetParamByIndex } from '.'; export type SetScale = ( @@ -38,7 +37,6 @@ export type SetScale = ( export interface ValueAxisOptionsParams { axis: ValueAxis; index: number; - isCategoryAxisHorizontal: boolean; onValueAxisPositionChanged: (index: number, value: ValueAxis['position']) => void; setParamByIndex: SetParamByIndex; valueAxis: ValueAxis; @@ -46,10 +44,9 @@ export interface ValueAxisOptionsParams { setMultipleValidity: (paramName: string, isValid: boolean) => void; } -function ValueAxisOptions({ +export function ValueAxisOptions({ axis, index, - isCategoryAxisHorizontal, valueAxis, vis, onValueAxisPositionChanged, @@ -105,34 +102,13 @@ function ValueAxisOptions({ [index, onValueAxisPositionChanged] ); - const isPositionDisabled = useCallback( - (position: Positions) => { - if (isCategoryAxisHorizontal) { - return isAxisHorizontal(position); - } - return [Positions.LEFT, Positions.RIGHT].includes(position as any); - }, - [isCategoryAxisHorizontal] - ); - - const positions = useMemo( - () => - vis.type.editorConfig.collections.positions.map( - (position: { text: string; value: Positions }) => ({ - ...position, - disabled: isPositionDisabled(position.value), - }) - ), - [vis.type.editorConfig.collections.positions, isPositionDisabled] - ); - return ( <> ); } - -export { ValueAxisOptions }; diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.test.tsx similarity index 94% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.test.tsx index 7bd5c9efdf29d..c2af7f2ad921b 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.test.tsx @@ -19,9 +19,11 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; + +import { NumberInputOption } from '../../../../../../charts/public'; + +import { ScaleType } from '../../../../types'; import { YExtents, YExtentsProps } from './y_extents'; -import { ScaleTypes } from '../../../utils/collections'; -import { NumberInputOption } from '../../../../../charts/public'; describe('YExtents component', () => { let setMultipleValidity: jest.Mock; @@ -35,7 +37,7 @@ describe('YExtents component', () => { defaultProps = { scale: { - type: ScaleTypes.LINEAR, + type: ScaleType.Linear, }, setMultipleValidity, setScale, @@ -81,7 +83,7 @@ describe('YExtents component', () => { it('should call setMultipleValidity with false when min equals 0 and scale is log', () => { defaultProps.scale.min = 0; defaultProps.scale.max = 1; - defaultProps.scale.type = ScaleTypes.LOG; + defaultProps.scale.type = ScaleType.Log; mount(); expect(setMultipleValidity).toBeCalledWith(Y_EXTENTS, false); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.tsx similarity index 83% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.tsx index 4e23ee5a41554..11d049d4864a7 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.tsx @@ -21,15 +21,15 @@ import React, { useEffect, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Scale } from '../../../types'; -import { ScaleTypes } from '../../../utils/collections'; -import { NumberInputOption } from '../../../../../charts/public'; +import { NumberInputOption } from '../../../../../../charts/public'; + +import { Scale, ScaleType } from '../../../../types'; import { SetScale } from './value_axis_options'; -const rangeError = i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.minErrorMessage', { +const rangeError = i18n.translate('visTypeXy.controls.pointSeries.valueAxes.minErrorMessage', { defaultMessage: 'Min should be less than Max.', }); -const minError = i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.minNeededScaleText', { +const minError = i18n.translate('visTypeXy.controls.pointSeries.valueAxes.minNeededScaleText', { defaultMessage: 'Min must exceed 0 when a log scale is selected.', }); @@ -59,7 +59,7 @@ function YExtents({ scale, setScale, setMultipleValidity }: YExtentsProps) { errors.push(rangeError); } - if (type === ScaleTypes.LOG && (isNullOrUndefined(min) || min <= 0)) { + if (type === ScaleType.Log && (isNullOrUndefined(min) || min <= 0)) { errors.push(minError); } @@ -86,7 +86,7 @@ function YExtents({ scale, setScale, setMultipleValidity }: YExtentsProps) { ) { + const { stateParams, setValue, vis, aggs } = props; + + const hasLineChart = stateParams.seriesParams.some( + ({ type, data: { id: paramId } }) => + (type === ChartType.Line || type === ChartType.Area) && + aggs.aggs.find(({ id }) => id === paramId)?.enabled + ); + + return ( + <> + + + {hasLineChart && ( + + )} + + ); +} diff --git a/src/plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/grid_panel.tsx similarity index 59% rename from src/plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/point_series/grid_panel.tsx index 0126dce37c9f2..c6ad52f7112c9 100644 --- a/src/plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/grid_panel.tsx @@ -16,25 +16,37 @@ * specific language governing permissions and limitations * under the License. */ + import React, { useMemo, useEffect, useCallback } from 'react'; -import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; + +import { SelectOption, SwitchOption } from '../../../../../../charts/public'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { SelectOption, SwitchOption } from '../../../../../charts/public'; -import { BasicVislibParams, ValueAxis } from '../../../types'; +import { VisParams, ValueAxis } from '../../../../types'; +import { ValidationVisOptionsProps } from '../../common'; -function GridPanel({ stateParams, setValue, hasHistogramAgg }: VisOptionsProps) { +type GridPanelOptions = ValidationVisOptionsProps< + VisParams, + { + showElasticChartsOptions: boolean; + } +>; + +function GridPanel({ stateParams, setValue, hasHistogramAgg, extraProps }: GridPanelOptions) { const setGrid = useCallback( - ( - paramName: T, - value: BasicVislibParams['grid'][T] - ) => setValue('grid', { ...stateParams.grid, [paramName]: value }), + (paramName: T, value: VisParams['grid'][T]) => + setValue('grid', { ...stateParams.grid, [paramName]: value }), [stateParams.grid, setValue] ); + const disableCategoryGridLines = useMemo( + () => !extraProps?.showElasticChartsOptions && hasHistogramAgg, + [extraProps?.showElasticChartsOptions, hasHistogramAgg] + ); + const options = useMemo( () => [ ...stateParams.valueAxes.map(({ id, name }: ValueAxis) => ({ @@ -42,7 +54,7 @@ function GridPanel({ stateParams, setValue, hasHistogramAgg }: VisOptionsProps { - if (hasHistogramAgg) { + if (disableCategoryGridLines) { setGrid('categoryLines', false); } - }, [hasHistogramAgg, setGrid]); + }, [disableCategoryGridLines, setGrid]); return (

@@ -71,19 +83,16 @@ function GridPanel({ stateParams, setValue, hasHistogramAgg }: VisOptionsProps
diff --git a/src/plugins/vis_type_vislib/public/components/options/point_series/index.ts b/src/plugins/vis_type_xy/public/editor/components/options/point_series/index.ts similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/point_series/index.ts rename to src/plugins/vis_type_xy/public/editor/components/options/point_series/index.ts diff --git a/src/plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx similarity index 64% rename from src/plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx index 18073d2218877..283fc28aed46e 100644 --- a/src/plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx @@ -16,25 +16,41 @@ * specific language governing permissions and limitations * under the License. */ + import React, { useMemo } from 'react'; import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { uniq } from 'lodash'; -import { ValidationVisOptionsProps } from '../../common'; -import { BasicOptions, SwitchOption } from '../../../../../charts/public'; +import { BasicOptions, SwitchOption } from '../../../../../../charts/public'; +import { BUCKET_TYPES } from '../../../../../../data/public'; + +import { VisParams } from '../../../../types'; import { GridPanel } from './grid_panel'; import { ThresholdPanel } from './threshold_panel'; -import { BasicVislibParams } from '../../../types'; -import { ChartTypes } from '../../../utils/collections'; - -function PointSeriesOptions(props: ValidationVisOptionsProps) { - const { stateParams, setValue, vis } = props; +import { ChartType } from '../../../../../common'; +import { ValidationVisOptionsProps } from '../../common'; +import { ElasticChartsOptions } from './elastic_charts_options'; - const currentChartTypes = useMemo(() => uniq(stateParams.seriesParams.map(({ type }) => type)), [ - stateParams.seriesParams, - ]); +export function PointSeriesOptions( + props: ValidationVisOptionsProps< + VisParams, + { + // TODO: Remove when vis_type_vislib is removed + // https://github.com/elastic/kibana/issues/56143 + showElasticChartsOptions: boolean; + } + > +) { + const { stateParams, setValue, vis, aggs } = props; + const hasBarChart = useMemo( + () => + stateParams.seriesParams.some( + ({ type, data: { id: paramId } }) => + type === ChartType.Histogram && aggs.aggs.find(({ id }) => id === paramId)?.enabled + ), + [stateParams.seriesParams, aggs.aggs] + ); return ( <> @@ -42,7 +58,7 @@ function PointSeriesOptions(props: ValidationVisOptionsProps)

@@ -52,10 +68,11 @@ function PointSeriesOptions(props: ValidationVisOptionsProps) {vis.data.aggs!.aggs.some( - (agg) => agg.schema === 'segment' && agg.type.name === 'date_histogram' + (agg) => agg.schema === 'segment' && agg.type.name === BUCKET_TYPES.DATE_HISTOGRAM ) ? ( ) /> ) : ( ) /> )} - {currentChartTypes.includes(ChartTypes.HISTOGRAM) && ( + {hasBarChart && ( ) } /> )} + + {props.extraProps?.showElasticChartsOptions && } @@ -98,5 +118,3 @@ function PointSeriesOptions(props: ValidationVisOptionsProps) ); } - -export { PointSeriesOptions }; diff --git a/src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/threshold_panel.tsx similarity index 77% rename from src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/point_series/threshold_panel.tsx index 964bb7d569b08..ec21a386a5679 100644 --- a/src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/threshold_panel.tsx @@ -17,39 +17,41 @@ * under the License. */ import React, { useCallback } from 'react'; -import { EuiPanel, EuiTitle, EuiColorPicker, EuiFormRow, EuiSpacer } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPanel, EuiTitle, EuiColorPicker, EuiFormRow, EuiSpacer } from '@elastic/eui'; -import { ValidationVisOptionsProps } from '../../common'; import { SelectOption, SwitchOption, RequiredNumberInputOption, -} from '../../../../../charts/public'; -import { BasicVislibParams } from '../../../types'; +} from '../../../../../../charts/public'; + +import { ValidationVisOptionsProps } from '../../common'; +import { VisParams } from '../../../../types'; function ThresholdPanel({ stateParams, setValue, setMultipleValidity, vis, -}: ValidationVisOptionsProps) { +}: ValidationVisOptionsProps) { const setThresholdLine = useCallback( - ( + ( paramName: T, - value: BasicVislibParams['thresholdLine'][T] + value: VisParams['thresholdLine'][T] ) => setValue('thresholdLine', { ...stateParams.thresholdLine, [paramName]: value }), [stateParams.thresholdLine, setValue] ); const setThresholdLineColor = useCallback( - (value: BasicVislibParams['thresholdLine']['color']) => setThresholdLine('color', value), + (value: VisParams['thresholdLine']['color']) => setThresholdLine('color', value), [setThresholdLine] ); const setThresholdLineValidity = useCallback( - (paramName: keyof BasicVislibParams['thresholdLine'], isValid: boolean) => + (paramName: keyof VisParams['thresholdLine'], isValid: boolean) => setMultipleValidity(`thresholdLine__${paramName}`, isValid), [setMultipleValidity] ); @@ -59,7 +61,7 @@ function ThresholdPanel({

@@ -67,7 +69,7 @@ function ThresholdPanel({ import('./components/data_view')); +import { i18n } from '@kbn/i18n'; +import { Position } from '@elastic/charts'; -export const getDataViewDescription = (): InspectorViewDescription => ({ - title: i18n.translate('inspector.data.dataTitle', { - defaultMessage: 'Data', - }), - order: 10, - help: i18n.translate('inspector.data.dataDescriptionTooltip', { - defaultMessage: 'View the data behind the visualization', - }), - shouldShow(adapters: Adapters) { - return Boolean(adapters.data); +export const getPositions = () => [ + { + text: i18n.translate('visTypeXy.legendPositions.topText', { + defaultMessage: 'Top', + }), + value: Position.Top, + }, + { + text: i18n.translate('visTypeXy.legendPositions.leftText', { + defaultMessage: 'Left', + }), + value: Position.Left, + }, + { + text: i18n.translate('visTypeXy.legendPositions.rightText', { + defaultMessage: 'Right', + }), + value: Position.Right, + }, + { + text: i18n.translate('visTypeXy.legendPositions.bottomText', { + defaultMessage: 'Bottom', + }), + value: Position.Bottom, }, - component: DataViewComponent, -}); +]; diff --git a/src/plugins/vis_type_xy/public/editor/scale_types.ts b/src/plugins/vis_type_xy/public/editor/scale_types.ts new file mode 100644 index 0000000000000..c115b04ead5fd --- /dev/null +++ b/src/plugins/vis_type_xy/public/editor/scale_types.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; + +import { ScaleType } from '../types'; + +export const getScaleTypes = () => [ + { + text: i18n.translate('visTypeXy.scaleTypes.linearText', { + defaultMessage: 'Linear', + }), + value: ScaleType.Linear, + }, + { + text: i18n.translate('visTypeXy.scaleTypes.logText', { + defaultMessage: 'Log', + }), + value: ScaleType.Log, + }, + { + text: i18n.translate('visTypeXy.scaleTypes.squareRootText', { + defaultMessage: 'Square root', + }), + value: ScaleType.SquareRoot, + }, +]; diff --git a/src/plugins/vis_type_xy/public/index.ts b/src/plugins/vis_type_xy/public/index.ts index 9af75ce9059e9..0739ad77a245b 100644 --- a/src/plugins/vis_type_xy/public/index.ts +++ b/src/plugins/vis_type_xy/public/index.ts @@ -17,11 +17,35 @@ * under the License. */ -import { PluginInitializerContext } from '../../../core/public'; import { VisTypeXyPlugin as Plugin } from './plugin'; export { VisTypeXyPluginSetup } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); +// TODO: Remove when vis_type_vislib is removed +// https://github.com/elastic/kibana/issues/56143 +export { + CategoryAxis, + ThresholdLine, + ValueAxis, + Grid, + SeriesParam, + Dimension, + Dimensions, + ScaleType, + AxisType, + HistogramParams, + DateHistogramParams, +} from './types'; +export type { ValidationVisOptionsProps } from './editor/components/common/validation_wrapper'; +export { TruncateLabelsOption } from './editor/components/common/truncate_labels'; +export { getPositions } from './editor/positions'; +export { getScaleTypes } from './editor/scale_types'; +export { xyVisTypes } from './vis_types'; +export { getAggId } from './config/get_agg_id'; + +// Export common types +export * from '../common'; + +export function plugin() { + return new Plugin(); } diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index 667018c1e6e30..c8812b07e5949 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -17,25 +17,30 @@ * under the License. */ -import { - CoreSetup, - CoreStart, - Plugin, - IUiSettingsClient, - PluginInitializerContext, -} from 'kibana/public'; - +import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; import { ChartsPluginSetup } from '../../charts/public'; +import { DataPublicPluginStart } from '../../data/public'; -export interface VisTypeXyDependencies { - uiSettings: IUiSettingsClient; - charts: ChartsPluginSetup; -} +import { createVisTypeXyVisFn } from './xy_vis_fn'; +import { + setDataActions, + setFormatService, + setThemeService, + setColorsService, + setTimefilter, + setUISettings, + setDocLinks, +} from './services'; +import { visTypesDefinitions } from './vis_types'; +import { CHARTS_LIBRARY } from '../common'; +import { xyVisRenderer } from './vis_renderer'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface VisTypeXyPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface VisTypeXyPluginStart {} /** @internal */ export interface VisTypeXyPluginSetupDependencies { @@ -48,40 +53,43 @@ export interface VisTypeXyPluginSetupDependencies { export interface VisTypeXyPluginStartDependencies { expressions: ReturnType; visualizations: VisualizationsStart; + data: DataPublicPluginStart; } -type VisTypeXyCoreSetup = CoreSetup; +type VisTypeXyCoreSetup = CoreSetup; /** @internal */ -export class VisTypeXyPlugin implements Plugin { - constructor(public initializerContext: PluginInitializerContext) {} - +export class VisTypeXyPlugin + implements + Plugin< + VisTypeXyPluginSetup, + VisTypeXyPluginStart, + VisTypeXyPluginSetupDependencies, + VisTypeXyPluginStartDependencies + > { public async setup( core: VisTypeXyCoreSetup, { expressions, visualizations, charts }: VisTypeXyPluginSetupDependencies ) { - // eslint-disable-next-line no-console - console.warn( - 'The visTypeXy plugin is enabled\n\n', - 'This may negatively alter existing vislib visualization configurations if saved.' - ); - const visualizationDependencies: Readonly = { - uiSettings: core.uiSettings, - charts, - }; - - const visTypeDefinitions: any[] = []; - const visFunctions: any = []; + if (core.uiSettings.get(CHARTS_LIBRARY, false)) { + setUISettings(core.uiSettings); + setThemeService(charts.theme); + setColorsService(charts.legacyColors); - visFunctions.forEach((fn: any) => expressions.registerFunction(fn)); - visTypeDefinitions.forEach((vis: any) => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); + [createVisTypeXyVisFn].forEach(expressions.registerFunction); + expressions.registerRenderer(xyVisRenderer); + visTypesDefinitions.forEach(visualizations.createBaseVisualization); + } return {}; } - public start(core: CoreStart, deps: VisTypeXyPluginStartDependencies) { - // nothing to do here + public start(core: CoreStart, { data }: VisTypeXyPluginStartDependencies) { + setFormatService(data.fieldFormats); + setDataActions(data.actions); + setTimefilter(data.query.timefilter.timefilter); + setDocLinks(core.docLinks); + + return {}; } } diff --git a/src/plugins/vis_type_xy/public/services.ts b/src/plugins/vis_type_xy/public/services.ts new file mode 100644 index 0000000000000..5a72759ecff6c --- /dev/null +++ b/src/plugins/vis_type_xy/public/services.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, DocLinksStart } from '../../../core/public'; +import { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { ChartsPluginSetup } from '../../charts/public'; + +export const [getUISettings, setUISettings] = createGetterSetter( + 'xy core.uiSettings' +); + +export const [getDataActions, setDataActions] = createGetterSetter< + DataPublicPluginStart['actions'] +>('xy data.actions'); + +export const [getFormatService, setFormatService] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('xy data.fieldFormats'); + +export const [getTimefilter, setTimefilter] = createGetterSetter< + DataPublicPluginStart['query']['timefilter']['timefilter'] +>('xy data.query.timefilter.timefilter'); + +export const [getThemeService, setThemeService] = createGetterSetter( + 'xy charts.theme' +); + +export const [getColorsService, setColorsService] = createGetterSetter< + ChartsPluginSetup['legacyColors'] +>('xy charts.color'); + +export const [getDocLinks, setDocLinks] = createGetterSetter('DocLinks'); diff --git a/src/plugins/vis_type_xy/public/to_ast.test.ts b/src/plugins/vis_type_xy/public/to_ast.test.ts new file mode 100644 index 0000000000000..678a9faaec585 --- /dev/null +++ b/src/plugins/vis_type_xy/public/to_ast.test.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Vis } from '../../visualizations/public'; +import { buildExpression } from '../../expressions/public'; +import { sampleAreaVis } from '../../vis_type_vislib/public/sample_vis.test.mocks'; + +import { toExpressionAst } from './to_ast'; +import { VisParams } from './types'; + +jest.mock('../../expressions/public', () => ({ + ...(jest.requireActual('../../expressions/public') as any), + buildExpression: jest.fn().mockImplementation(() => ({ + toAst: () => ({ + type: 'expression', + chain: [], + }), + })), +})); + +jest.mock('./to_ast_esaggs', () => ({ + getEsaggsFn: jest.fn(), +})); + +describe('xy vis toExpressionAst function', () => { + let vis: Vis; + + const params = { + timefilter: {}, + timeRange: {}, + abortSignal: {}, + } as any; + + beforeEach(() => { + vis = sampleAreaVis as any; + }); + + it('should match basic snapshot', () => { + toExpressionAst(vis, params); + const [, builtExpression] = (buildExpression as jest.Mock).mock.calls[0][0]; + + expect(builtExpression).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_type_xy/public/to_ast.ts b/src/plugins/vis_type_xy/public/to_ast.ts new file mode 100644 index 0000000000000..c93dbe46dca0e --- /dev/null +++ b/src/plugins/vis_type_xy/public/to_ast.ts @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +import { VisToExpressionAst, getVisSchemas } from '../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { BUCKET_TYPES } from '../../data/public'; + +import { DateHistogramParams, Dimensions, HistogramParams, VisParams } from './types'; +import { visName, VisTypeXyExpressionFunctionDefinition } from './xy_vis_fn'; +import { XyVisType } from '../common'; +import { getEsaggsFn } from './to_ast_esaggs'; + +export const toExpressionAst: VisToExpressionAst = async (vis, params) => { + const schemas = getVisSchemas(vis, params); + const dimensions: Dimensions = { + x: schemas.segment ? schemas.segment[0] : null, + y: schemas.metric, + z: schemas.radius, + width: schemas.width, + series: schemas.group, + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }; + + const responseAggs = vis.data.aggs?.getResponseAggs().filter(({ enabled }) => enabled) ?? []; + + if (dimensions.x) { + const xAgg = responseAggs[dimensions.x.accessor] as any; + if (xAgg.type.name === BUCKET_TYPES.DATE_HISTOGRAM) { + (dimensions.x.params as DateHistogramParams).date = true; + const { esUnit, esValue } = xAgg.buckets.getInterval(); + (dimensions.x.params as DateHistogramParams).intervalESUnit = esUnit; + (dimensions.x.params as DateHistogramParams).intervalESValue = esValue; + (dimensions.x.params as DateHistogramParams).interval = moment + .duration(esValue, esUnit) + .asMilliseconds(); + (dimensions.x.params as DateHistogramParams).format = xAgg.buckets.getScaledDateFormat(); + } else if (xAgg.type.name === BUCKET_TYPES.HISTOGRAM) { + const intervalParam = xAgg.type.paramByName('interval'); + const output = { params: {} as any }; + await intervalParam.modifyAggConfigOnSearchRequestStart(xAgg, vis.data.searchSource, { + abortSignal: params.abortSignal, + }); + intervalParam.write(xAgg, output); + (dimensions.x.params as HistogramParams).interval = output.params.interval; + } + } + + const visConfig = { ...vis.params }; + + (dimensions.y || []).forEach((yDimension) => { + const yAgg = responseAggs[yDimension.accessor]; + const seriesParam = (visConfig.seriesParams || []).find( + (param: any) => param.data.id === yAgg.id + ); + if (seriesParam) { + const usedValueAxis = (visConfig.valueAxes || []).find( + (valueAxis: any) => valueAxis.id === seriesParam.valueAxis + ); + if (usedValueAxis?.scale.mode === 'percentage') { + yDimension.format = { id: 'percent' }; + } + } + }); + + visConfig.dimensions = dimensions; + + const visTypeXy = buildExpressionFunction(visName, { + type: vis.type.name as XyVisType, + visConfig: JSON.stringify(visConfig), + }); + + const ast = buildExpression([getEsaggsFn(vis), visTypeXy]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_type_xy/public/to_ast_esaggs.ts b/src/plugins/vis_type_xy/public/to_ast_esaggs.ts new file mode 100644 index 0000000000000..da8d11ac8340c --- /dev/null +++ b/src/plugins/vis_type_xy/public/to_ast_esaggs.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Vis } from '../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { + EsaggsExpressionFunctionDefinition, + IndexPatternLoadExpressionFunctionDefinition, +} from '../../data/public'; + +import { VisParams } from './types'; + +/** + * Get esaggs expressions function + * TODO: replace this with vis.data.aggs!.toExpressionAst(); + * https://github.com/elastic/kibana/issues/61768 + * @param vis + */ +export function getEsaggsFn(vis: Vis) { + return buildExpressionFunction('esaggs', { + index: buildExpression([ + buildExpressionFunction('indexPatternLoad', { + id: vis.data.indexPattern!.id!, + }), + ]), + metricsAtAllLevels: vis.isHierarchical(), + partialRows: false, + aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), + }); +} diff --git a/src/plugins/vis_type_xy/public/types/config.ts b/src/plugins/vis_type_xy/public/types/config.ts new file mode 100644 index 0000000000000..ec73c0f6e3fc0 --- /dev/null +++ b/src/plugins/vis_type_xy/public/types/config.ts @@ -0,0 +1,130 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + AxisSpec, + CustomTooltip, + Fit, + GridLineStyle, + Position, + Rotation, + SeriesScales, + TickFormatter, + TooltipProps, + TooltipValueFormatter, + YDomainRange, +} from '@elastic/charts'; + +import { Dimension, Scale, ThresholdLine } from './param'; + +export interface Column { + id: string | null; + name: string; +} + +export interface Aspect { + accessor: Column['id']; + aggType: string | null; + aggId: string | null; + column?: Dimension['accessor']; + title: Column['name']; + format?: Dimension['format']; + formatter?: TickFormatter; + params: Dimension['params']; +} + +export interface Aspects { + x: Aspect; + y: Aspect[]; + z?: Aspect; + series?: Aspect[]; +} + +export interface AxisGrid { + show?: boolean; + styles?: GridLineStyle; +} + +export interface TickOptions { + show?: boolean; + size?: number; + count?: number; + padding?: number; + formatter?: TickFormatter; + labelFormatter?: TickFormatter; + rotation?: number; + showDuplicates?: boolean; + integersOnly?: boolean; + showOverlappingTicks?: boolean; + showOverlappingLabels?: boolean; +} + +export type YScaleType = SeriesScales['yScaleType']; +export type XScaleType = SeriesScales['xScaleType']; + +export type ScaleConfig = Omit & { + type?: S; +}; + +export interface AxisConfig { + id: string; + groupId?: string; + position: Position; + ticks?: TickOptions; + show: boolean; + style: AxisSpec['style']; + scale: ScaleConfig; + domain?: YDomainRange; + title?: string; + grid?: AxisGrid; + integersOnly: boolean; +} + +export interface LegendOptions { + show: boolean; + position?: Position; +} + +export type ThresholdLineConfig = Omit & { + dash?: number[]; + groupId?: string; +}; + +export type TooltipConfig = Omit & { + detailedTooltip?: (headerFormatter?: TooltipValueFormatter) => CustomTooltip; +}; + +export interface VisConfig { + legend: LegendOptions; + tooltip: TooltipConfig; + xAxis: AxisConfig; + yAxes: Array>; + aspects: Aspects; + rotation: Rotation; + thresholdLine: ThresholdLineConfig; + orderBucketsBySum?: boolean; + showCurrentTime: boolean; + isTimeChart: boolean; + markSizeRatio: number; + showValueLabel: boolean; + enableHistogramMode: boolean; + fittingFunction?: Exclude; + detailedTooltip?: boolean; + isVislibVis?: boolean; +} diff --git a/src/plugins/vis_type_xy/public/types/constants.ts b/src/plugins/vis_type_xy/public/types/constants.ts new file mode 100644 index 0000000000000..f92c56e43413e --- /dev/null +++ b/src/plugins/vis_type_xy/public/types/constants.ts @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { $Values } from '@kbn/utility-types'; + +export const ChartMode = Object.freeze({ + Normal: 'normal' as const, + Stacked: 'stacked' as const, +}); +export type ChartMode = $Values; + +export const InterpolationMode = Object.freeze({ + Linear: 'linear' as const, + Cardinal: 'cardinal' as const, + StepAfter: 'step-after' as const, +}); +export type InterpolationMode = $Values; + +export const AxisType = Object.freeze({ + Category: 'category' as const, + Value: 'value' as const, +}); +export type AxisType = $Values; + +export const ScaleType = Object.freeze({ + Linear: 'linear' as const, + Log: 'log' as const, + SquareRoot: 'square root' as const, +}); +export type ScaleType = $Values; + +export const AxisMode = Object.freeze({ + Normal: 'normal' as const, + Percentage: 'percentage' as const, + Wiggle: 'wiggle' as const, + Silhouette: 'silhouette' as const, +}); +export type AxisMode = $Values; + +export const ThresholdLineStyle = Object.freeze({ + Full: 'full' as const, + Dashed: 'dashed' as const, + DotDashed: 'dot-dashed' as const, +}); +export type ThresholdLineStyle = $Values; + +export const ColorMode = Object.freeze({ + Background: 'Background' as const, + Labels: 'Labels' as const, + None: 'None' as const, +}); +export type ColorMode = $Values; diff --git a/src/plugins/vis_type_xy/public/types/index.ts b/src/plugins/vis_type_xy/public/types/index.ts new file mode 100644 index 0000000000000..791373def2018 --- /dev/null +++ b/src/plugins/vis_type_xy/public/types/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './constants'; +export * from './config'; +export * from './param'; +export * from './vis_type'; diff --git a/src/plugins/vis_type_xy/public/types/param.ts b/src/plugins/vis_type_xy/public/types/param.ts new file mode 100644 index 0000000000000..c8cd020dec03c --- /dev/null +++ b/src/plugins/vis_type_xy/public/types/param.ts @@ -0,0 +1,160 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Fit, Position } from '@elastic/charts'; + +import { Style, Labels } from '../../../charts/public'; +import { SchemaConfig } from '../../../visualizations/public'; + +import { ChartType } from '../../common'; +import { + ChartMode, + AxisMode, + AxisType, + InterpolationMode, + ScaleType, + ThresholdLineStyle, +} from './constants'; + +export interface Scale { + boundsMargin?: number | ''; + defaultYExtents?: boolean; + max?: number | null; + min?: number | null; + mode?: AxisMode; + setYExtents?: boolean; + type: ScaleType; +} + +export interface CategoryAxis { + id: string; + labels: Labels; + position: Position; + scale: Scale; + show: boolean; + title: { + text?: string; + }; + type: AxisType; + /** + * Used only for heatmap, here for consistent types when used in vis_type_vislib + * + * remove with vis_type_vislib + * https://github.com/elastic/kibana/issues/56143 + */ + style: Partial