From e6ea7a8d394e5c2f9dea7ac3676fc605e884bc02 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 13 Jul 2022 16:34:22 +0200 Subject: [PATCH 01/92] [UnifiedFieldList] Bootstrap a new unifiedFieldList plugin --- .github/CODEOWNERS | 1 + src/plugins/discover/kibana.json | 3 +- .../components/sidebar/discover_field.tsx | 2 + src/plugins/discover/tsconfig.json | 1 + src/plugins/unified_field_list/README.md | 9 ++++ .../unified_field_list/common/index.ts | 9 ++++ src/plugins/unified_field_list/jest.config.js | 16 +++++++ src/plugins/unified_field_list/kibana.json | 15 +++++++ .../components/field_stats/field_stats.tsx | 45 +++++++++++++++++++ .../public/components/field_stats/index.tsx | 9 ++++ .../unified_field_list/public/index.ts | 18 ++++++++ .../unified_field_list/public/plugin.ts | 25 +++++++++++ .../unified_field_list/public/types.ts | 13 ++++++ .../unified_field_list/server/index.ts | 19 ++++++++ .../unified_field_list/server/plugin.ts | 39 ++++++++++++++++ .../unified_field_list/server/routes/index.ts | 25 +++++++++++ .../unified_field_list/server/types.ts | 12 +++++ src/plugins/unified_field_list/tsconfig.json | 19 ++++++++ tsconfig.base.json | 2 + 19 files changed, 281 insertions(+), 1 deletion(-) create mode 100755 src/plugins/unified_field_list/README.md create mode 100755 src/plugins/unified_field_list/common/index.ts create mode 100644 src/plugins/unified_field_list/jest.config.js create mode 100755 src/plugins/unified_field_list/kibana.json create mode 100755 src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx create mode 100755 src/plugins/unified_field_list/public/components/field_stats/index.tsx create mode 100755 src/plugins/unified_field_list/public/index.ts create mode 100755 src/plugins/unified_field_list/public/plugin.ts create mode 100755 src/plugins/unified_field_list/public/types.ts create mode 100755 src/plugins/unified_field_list/server/index.ts create mode 100755 src/plugins/unified_field_list/server/plugin.ts create mode 100755 src/plugins/unified_field_list/server/routes/index.ts create mode 100755 src/plugins/unified_field_list/server/types.ts create mode 100644 src/plugins/unified_field_list/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7377ea3ffe353..4cc9d808724c9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,6 +14,7 @@ /test/functional/apps/discover/ @elastic/kibana-data-discovery /x-pack/plugins/graph/ @elastic/kibana-data-discovery /x-pack/test/functional/apps/graph @elastic/kibana-data-discovery +/src/plugins/unified_field_list/ @elastic/kibana-data-discovery # Vis Editors /x-pack/plugins/lens/ @elastic/kibana-vis-editors diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index cb40433b73fa1..4cd977aa45a04 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -14,7 +14,8 @@ "uiActions", "savedObjects", "dataViewFieldEditor", - "dataViewEditor" + "dataViewEditor", + "unifiedFieldList" ], "optionalPlugins": ["home", "share", "usageCollection", "spaces", "triggersActionsUi"], "requiredBundles": ["kibanaUtils", "kibanaReact", "dataViews", "unifiedSearch"], diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 33fc01abb5150..7f3e968e7c5c4 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -26,6 +26,7 @@ import { UiCounterMetricType } from '@kbn/analytics'; import classNames from 'classnames'; import { FieldButton, FieldIcon } from '@kbn/react-field'; import type { DataViewField, DataView } from '@kbn/data-views-plugin/public'; +import { FieldStats } from '@kbn/unified-field-list-plugin/public'; import { getFieldCapabilities } from '../../../../utils/get_field_capabilities'; import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon'; import { DiscoverFieldDetails } from './discover_field_details'; @@ -375,6 +376,7 @@ function DiscoverFieldComponent({ <> {showFieldStats && ( <> +
{i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 9915680ada26e..f662af0736e67 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -26,6 +26,7 @@ { "path": "../field_formats/tsconfig.json" }, { "path": "../data_views/tsconfig.json" }, { "path": "../unified_search/tsconfig.json" }, + { "path": "../unified_field_list/tsconfig.json" }, { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, { "path": "../data_view_editor/tsconfig.json" }, { "path": "../../../x-pack/plugins/triggers_actions_ui/tsconfig.json" } diff --git a/src/plugins/unified_field_list/README.md b/src/plugins/unified_field_list/README.md new file mode 100755 index 0000000000000..86e3ec9700b9b --- /dev/null +++ b/src/plugins/unified_field_list/README.md @@ -0,0 +1,9 @@ +# unifiedFieldList + +A Kibana plugin + +--- + +## Development + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/src/plugins/unified_field_list/common/index.ts b/src/plugins/unified_field_list/common/index.ts new file mode 100755 index 0000000000000..747db7a56bbae --- /dev/null +++ b/src/plugins/unified_field_list/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const PLUGIN_ID = 'unifiedFieldList'; diff --git a/src/plugins/unified_field_list/jest.config.js b/src/plugins/unified_field_list/jest.config.js new file mode 100644 index 0000000000000..a1782360c93b9 --- /dev/null +++ b/src/plugins/unified_field_list/jest.config.js @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/unified_field_list'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/unified_field_list', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/unified_field_list/public/**/*.{ts,tsx}'], +}; diff --git a/src/plugins/unified_field_list/kibana.json b/src/plugins/unified_field_list/kibana.json new file mode 100755 index 0000000000000..84ffcf7064311 --- /dev/null +++ b/src/plugins/unified_field_list/kibana.json @@ -0,0 +1,15 @@ +{ + "id": "unifiedFieldList", + "version": "1.0.0", + "kibanaVersion": "kibana", + "owner": { + "name": "Data Discovery", + "githubTeam": "kibana-data-discovery" + }, + "description": "Contains functionality for the field list which can be integrated into apps sidebar", + "server": true, + "ui": true, + "requiredPlugins": [], + "optionalPlugins": [], + "requiredBundles": ["kibanaReact"] +} diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx new file mode 100755 index 0000000000000..5e4ecafeaf69c --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { CoreStart } from '@kbn/core/public'; +import { EuiButton, EuiText } from '@elastic/eui'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface FieldStatsProps {} + +export const FieldStats: React.FC = () => { + const { services } = useKibana<{ http: CoreStart['http'] }>(); + const { http } = services; + // Use React hooks to manage state. + const [timestamp, setTimestamp] = useState(); + + const onClickHandler = () => { + // Use the core http service to make a response to the server API. + http?.get('/api/unified_field_list/example').then((res) => { + setTimestamp((res as unknown as { time: string }).time); + }); + }; + + return ( + +

+ + + + +

+
+ ); +}; diff --git a/src/plugins/unified_field_list/public/components/field_stats/index.tsx b/src/plugins/unified_field_list/public/components/field_stats/index.tsx new file mode 100755 index 0000000000000..fb4d09f68b779 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_stats/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { FieldStatsProps, FieldStats } from './field_stats'; diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts new file mode 100755 index 0000000000000..38fef2998c75c --- /dev/null +++ b/src/plugins/unified_field_list/public/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UnifiedFieldListPlugin } from './plugin'; + +export { FieldStats, FieldStatsProps } from './components/field_stats'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin() { + return new UnifiedFieldListPlugin(); +} +export { UnifiedFieldListPluginSetup, UnifiedFieldListPluginStart } from './types'; diff --git a/src/plugins/unified_field_list/public/plugin.ts b/src/plugins/unified_field_list/public/plugin.ts new file mode 100755 index 0000000000000..009b73dd1c575 --- /dev/null +++ b/src/plugins/unified_field_list/public/plugin.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import { UnifiedFieldListPluginSetup, UnifiedFieldListPluginStart } from './types'; + +export class UnifiedFieldListPlugin + implements Plugin +{ + public setup(core: CoreSetup): UnifiedFieldListPluginSetup { + // Return methods that should be available to other plugins + return {}; + } + + public start(core: CoreStart): UnifiedFieldListPluginStart { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/unified_field_list/public/types.ts b/src/plugins/unified_field_list/public/types.ts new file mode 100755 index 0000000000000..feb24509cdee5 --- /dev/null +++ b/src/plugins/unified_field_list/public/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UnifiedFieldListPluginSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UnifiedFieldListPluginStart {} diff --git a/src/plugins/unified_field_list/server/index.ts b/src/plugins/unified_field_list/server/index.ts new file mode 100755 index 0000000000000..4f3385c1ea7d3 --- /dev/null +++ b/src/plugins/unified_field_list/server/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginInitializerContext } from '@kbn/core/server'; +import { UnifiedFieldListPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new UnifiedFieldListPlugin(initializerContext); +} + +export { UnifiedFieldListPluginSetup, UnifiedFieldListPluginStart } from './types'; diff --git a/src/plugins/unified_field_list/server/plugin.ts b/src/plugins/unified_field_list/server/plugin.ts new file mode 100755 index 0000000000000..28565262eddbc --- /dev/null +++ b/src/plugins/unified_field_list/server/plugin.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; + +import { UnifiedFieldListPluginSetup, UnifiedFieldListPluginStart } from './types'; +import { defineRoutes } from './routes'; + +export class UnifiedFieldListPlugin + implements Plugin +{ + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('unifiedFieldList: Setup'); + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('unifiedFieldList: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/unified_field_list/server/routes/index.ts b/src/plugins/unified_field_list/server/routes/index.ts new file mode 100755 index 0000000000000..8108eef74ac60 --- /dev/null +++ b/src/plugins/unified_field_list/server/routes/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IRouter } from '@kbn/core/server'; + +export function defineRoutes(router: IRouter) { + router.get( + { + path: '/api/unified_field_list/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/src/plugins/unified_field_list/server/types.ts b/src/plugins/unified_field_list/server/types.ts new file mode 100755 index 0000000000000..2f36ffbad814a --- /dev/null +++ b/src/plugins/unified_field_list/server/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UnifiedFieldListPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UnifiedFieldListPluginStart {} diff --git a/src/plugins/unified_field_list/tsconfig.json b/src/plugins/unified_field_list/tsconfig.json new file mode 100644 index 0000000000000..65166e5788e83 --- /dev/null +++ b/src/plugins/unified_field_list/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "../../typings/**/*", + "common/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" } + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 9946503830c70..2388b932cfef7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -159,6 +159,8 @@ "@kbn/ui-actions-enhanced-plugin/*": ["src/plugins/ui_actions_enhanced/*"], "@kbn/ui-actions-plugin": ["src/plugins/ui_actions"], "@kbn/ui-actions-plugin/*": ["src/plugins/ui_actions/*"], + "@kbn/unified-field-list-plugin": ["src/plugins/unified_field_list"], + "@kbn/unified-field-list-plugin/*": ["src/plugins/unified_field_list/*"], "@kbn/unified-search-plugin": ["src/plugins/unified_search"], "@kbn/unified-search-plugin/*": ["src/plugins/unified_search/*"], "@kbn/url-forwarding-plugin": ["src/plugins/url_forwarding"], From 4fe9db0c557fa8ae137b44d19d1e4ec2a83ef97a Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 13 Jul 2022 20:00:19 +0200 Subject: [PATCH 02/92] [UnifiedFieldList] Move backend API for field stats from Lens to UnifiedFieldList plugin --- .github/CODEOWNERS | 1 + .../unified_field_list/common/constants.ts | 10 ++++++++ .../unified_field_list/common/types/index.ts | 14 +++++++++++ .../unified_field_list/common/types/stats.ts | 5 ++-- src/plugins/unified_field_list/kibana.json | 2 +- .../unified_field_list/public/index.ts | 7 ++++++ .../unified_field_list/server/index.ts | 2 +- .../unified_field_list/server/plugin.ts | 18 ++++++++------- .../server/routes/field_stats.ts | 23 ++++++++++--------- .../unified_field_list/server/routes/index.ts | 10 ++++++-- .../unified_field_list/server/types.ts | 14 +++++++++-- src/plugins/unified_field_list/tsconfig.json | 4 +++- x-pack/plugins/lens/common/index.ts | 1 - x-pack/plugins/lens/kibana.json | 3 ++- .../field_item.test.tsx | 7 ++++-- .../indexpattern_datasource/field_item.tsx | 10 ++++++-- .../operations/definitions/terms/helpers.ts | 5 ++-- x-pack/plugins/lens/server/routes/index.ts | 2 -- x-pack/plugins/lens/tsconfig.json | 3 ++- 19 files changed, 102 insertions(+), 39 deletions(-) create mode 100644 src/plugins/unified_field_list/common/constants.ts create mode 100755 src/plugins/unified_field_list/common/types/index.ts rename x-pack/plugins/lens/common/api.ts => src/plugins/unified_field_list/common/types/stats.ts (84%) rename {x-pack/plugins/lens => src/plugins/unified_field_list}/server/routes/field_stats.ts (93%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4cc9d808724c9..eee40ea064021 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -42,6 +42,7 @@ /packages/kbn-tinymath/ @elastic/kibana-vis-editors /x-pack/test/functional/apps/lens @elastic/kibana-vis-editors /test/functional/apps/visualize/ @elastic/kibana-vis-editors +/src/plugins/unified_field_list/ @elastic/kibana-vis-editors # Application Services /examples/bfetch_explorer/ @elastic/kibana-app-services diff --git a/src/plugins/unified_field_list/common/constants.ts b/src/plugins/unified_field_list/common/constants.ts new file mode 100644 index 0000000000000..ef8a01b2705ff --- /dev/null +++ b/src/plugins/unified_field_list/common/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const BASE_API_PATH = '/api/unified_field_list'; +export const FIELD_STATS_API_PATH = `${BASE_API_PATH}/field_stats`; diff --git a/src/plugins/unified_field_list/common/types/index.ts b/src/plugins/unified_field_list/common/types/index.ts new file mode 100755 index 0000000000000..78dbc548406c0 --- /dev/null +++ b/src/plugins/unified_field_list/common/types/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { + FieldStatsResponse, + NumberStatsResult, + TopValuesResult, + BucketedAggregation, +} from './stats'; diff --git a/x-pack/plugins/lens/common/api.ts b/src/plugins/unified_field_list/common/types/stats.ts similarity index 84% rename from x-pack/plugins/lens/common/api.ts rename to src/plugins/unified_field_list/common/types/stats.ts index 026f540cdb67b..71d75db1aaaa3 100644 --- a/x-pack/plugins/lens/common/api.ts +++ b/src/plugins/unified_field_list/common/types/stats.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ export interface BucketedAggregation { diff --git a/src/plugins/unified_field_list/kibana.json b/src/plugins/unified_field_list/kibana.json index 84ffcf7064311..c0d1f9899e071 100755 --- a/src/plugins/unified_field_list/kibana.json +++ b/src/plugins/unified_field_list/kibana.json @@ -9,7 +9,7 @@ "description": "Contains functionality for the field list which can be integrated into apps sidebar", "server": true, "ui": true, - "requiredPlugins": [], + "requiredPlugins": ["dataViews"], "optionalPlugins": [], "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts index 38fef2998c75c..1d0102cba9d4b 100755 --- a/src/plugins/unified_field_list/public/index.ts +++ b/src/plugins/unified_field_list/public/index.ts @@ -8,7 +8,14 @@ import { UnifiedFieldListPlugin } from './plugin'; +export type { + FieldStatsResponse, + BucketedAggregation, + NumberStatsResult, + TopValuesResult, +} from '../common/types'; export { FieldStats, FieldStatsProps } from './components/field_stats'; +export { BASE_API_PATH, FIELD_STATS_API_PATH } from '../common/constants'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. diff --git a/src/plugins/unified_field_list/server/index.ts b/src/plugins/unified_field_list/server/index.ts index 4f3385c1ea7d3..0b753d1636823 100755 --- a/src/plugins/unified_field_list/server/index.ts +++ b/src/plugins/unified_field_list/server/index.ts @@ -16,4 +16,4 @@ export function plugin(initializerContext: PluginInitializerContext) { return new UnifiedFieldListPlugin(initializerContext); } -export { UnifiedFieldListPluginSetup, UnifiedFieldListPluginStart } from './types'; +export { UnifiedFieldListServerPluginSetup, UnifiedFieldListServerPluginStart } from './types'; diff --git a/src/plugins/unified_field_list/server/plugin.ts b/src/plugins/unified_field_list/server/plugin.ts index 28565262eddbc..2853afcf3d6fd 100755 --- a/src/plugins/unified_field_list/server/plugin.ts +++ b/src/plugins/unified_field_list/server/plugin.ts @@ -7,12 +7,16 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; - -import { UnifiedFieldListPluginSetup, UnifiedFieldListPluginStart } from './types'; +import { + UnifiedFieldListServerPluginSetup, + UnifiedFieldListServerPluginStart, + PluginStart, + PluginSetup, +} from './types'; import { defineRoutes } from './routes'; export class UnifiedFieldListPlugin - implements Plugin + implements Plugin { private readonly logger: Logger; @@ -20,17 +24,15 @@ export class UnifiedFieldListPlugin this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, plugins: PluginSetup) { this.logger.debug('unifiedFieldList: Setup'); - const router = core.http.createRouter(); - // Register server side APIs - defineRoutes(router); + defineRoutes(core); return {}; } - public start(core: CoreStart) { + public start(core: CoreStart, plugins: PluginStart) { this.logger.debug('unifiedFieldList: Started'); return {}; } diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/src/plugins/unified_field_list/server/routes/field_stats.ts similarity index 93% rename from x-pack/plugins/lens/server/routes/field_stats.ts rename to src/plugins/unified_field_list/server/routes/field_stats.ts index 35a15ea44be67..14a549116bd6e 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/src/plugins/unified_field_list/server/routes/field_stats.ts @@ -1,9 +1,11 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ + import { errors } from '@elastic/elasticsearch'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import DateMath from '@kbn/datemath'; @@ -12,25 +14,24 @@ import { CoreSetup } from '@kbn/core/server'; import type { DataViewField } from '@kbn/data-views-plugin/common'; import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; -import { FieldStatsResponse, BASE_API_URL } from '../../common'; -import { PluginStartContract } from '../plugin'; +import { FIELD_STATS_API_PATH } from '../../common/constants'; +import type { FieldStatsResponse } from '../../common/types'; +import type { PluginStart } from '../types'; const SHARD_SIZE = 5000; -export async function initFieldsRoute(setup: CoreSetup) { +export async function initFieldStatsRoute(setup: CoreSetup) { const router = setup.http.createRouter(); router.post( { - path: `${BASE_API_URL}/index_stats/{indexPatternId}/field`, + path: FIELD_STATS_API_PATH, validate: { - params: schema.object({ - indexPatternId: schema.string(), - }), body: schema.object( { dslQuery: schema.object({}, { unknowns: 'allow' }), fromDate: schema.string(), toDate: schema.string(), + dataViewId: schema.string(), fieldName: schema.string(), size: schema.maybe(schema.number()), }, @@ -40,7 +41,7 @@ export async function initFieldsRoute(setup: CoreSetup) { }, async (context, req, res) => { const requestClient = (await context.core).elasticsearch.client.asCurrentUser; - const { fromDate, toDate, fieldName, dslQuery, size } = req.body; + const { fromDate, toDate, fieldName, dslQuery, size, dataViewId } = req.body; const [{ savedObjects, elasticsearch }, { dataViews }] = await setup.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(req); @@ -51,7 +52,7 @@ export async function initFieldsRoute(setup: CoreSetup) { ); try { - const indexPattern = await indexPatternsService.get(req.params.indexPatternId); + const indexPattern = await indexPatternsService.get(dataViewId); const timeFieldName = indexPattern.timeFieldName; const field = indexPattern.fields.find((f) => f.name === fieldName); diff --git a/src/plugins/unified_field_list/server/routes/index.ts b/src/plugins/unified_field_list/server/routes/index.ts index 8108eef74ac60..96adaa0fb7591 100755 --- a/src/plugins/unified_field_list/server/routes/index.ts +++ b/src/plugins/unified_field_list/server/routes/index.ts @@ -6,9 +6,15 @@ * Side Public License, v 1. */ -import { IRouter } from '@kbn/core/server'; +import { CoreSetup } from '@kbn/core/server'; +import { PluginStart } from '../types'; +import { initFieldStatsRoute } from './field_stats'; -export function defineRoutes(router: IRouter) { +export function defineRoutes(setup: CoreSetup) { + initFieldStatsRoute(setup); + + // TODO: remove this temporary code + const router = setup.http.createRouter(); router.get( { path: '/api/unified_field_list/example', diff --git a/src/plugins/unified_field_list/server/types.ts b/src/plugins/unified_field_list/server/types.ts index 2f36ffbad814a..56cd69a01881e 100755 --- a/src/plugins/unified_field_list/server/types.ts +++ b/src/plugins/unified_field_list/server/types.ts @@ -6,7 +6,17 @@ * Side Public License, v 1. */ +import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; + // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface UnifiedFieldListPluginSetup {} +export interface UnifiedFieldListServerPluginSetup {} + // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface UnifiedFieldListPluginStart {} +export interface UnifiedFieldListServerPluginStart {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginSetup {} + +export interface PluginStart { + dataViews: DataViewsServerPluginStart; +} diff --git a/src/plugins/unified_field_list/tsconfig.json b/src/plugins/unified_field_list/tsconfig.json index 65166e5788e83..e74e3d26f3e28 100644 --- a/src/plugins/unified_field_list/tsconfig.json +++ b/src/plugins/unified_field_list/tsconfig.json @@ -14,6 +14,8 @@ ], "references": [ { "path": "../../core/tsconfig.json" }, - { "path": "../kibana_react/tsconfig.json" } + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../data_views/tsconfig.json" } ] } diff --git a/x-pack/plugins/lens/common/index.ts b/x-pack/plugins/lens/common/index.ts index e0600bd18afc1..45dd41422b018 100644 --- a/x-pack/plugins/lens/common/index.ts +++ b/x-pack/plugins/lens/common/index.ts @@ -8,7 +8,6 @@ // TODO: https://github.com/elastic/kibana/issues/110891 /* eslint-disable @kbn/eslint/no_export_all */ -export * from './api'; export * from './constants'; export * from './types'; diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index adf791e8d2f48..09c96db95ae3c 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -25,7 +25,8 @@ "expressionGauge", "expressionHeatmap", "eventAnnotation", - "unifiedSearch" + "unifiedSearch", + "unifiedFieldList" ], "optionalPlugins": [ "expressionXY", diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 11d3d9c6a7871..9dc49f8681733 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -18,6 +18,7 @@ import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { documentField } from './document_field'; import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { FIELD_STATS_API_PATH } from '@kbn/unified-field-list-plugin/public'; import { DOCUMENT_FIELD_NAME } from '../../common'; const chartsThemeService = chartPluginMock.createSetupContract().theme; @@ -184,7 +185,7 @@ describe('IndexPattern Field Item', () => { clickField(wrapper, 'bytes'); - expect(core.http.post).toHaveBeenCalledWith(`/api/lens/index_stats/1/field`, { + expect(core.http.post).toHaveBeenCalledWith(FIELD_STATS_API_PATH, { body: JSON.stringify({ dslQuery: { bool: { @@ -197,6 +198,7 @@ describe('IndexPattern Field Item', () => { fromDate: 'now-7d', toDate: 'now', fieldName: 'bytes', + dataViewId: '1', }), }); @@ -251,7 +253,7 @@ describe('IndexPattern Field Item', () => { clickField(wrapper, 'bytes'); expect(core.http.post).toHaveBeenCalledTimes(2); - expect(core.http.post).toHaveBeenLastCalledWith(`/api/lens/index_stats/1/field`, { + expect(core.http.post).toHaveBeenLastCalledWith(FIELD_STATS_API_PATH, { body: JSON.stringify({ dslQuery: { bool: { @@ -274,6 +276,7 @@ describe('IndexPattern Field Item', () => { fromDate: 'now-14d', toDate: 'now-7d', fieldName: 'bytes', + dataViewId: '1', }), }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 27c774ed2963e..a3ff80463fd6c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -43,9 +43,14 @@ import { Filter, buildEsQuery, Query } from '@kbn/es-query'; import { KBN_FIELD_TYPES, ES_FIELD_TYPES, getEsQueryConfig } from '@kbn/data-plugin/public'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { + BucketedAggregation, + FieldStatsResponse, + FIELD_STATS_API_PATH, +} from '@kbn/unified-field-list-plugin/public'; import { DragDrop, DragDropIdentifier } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; -import { BucketedAggregation, DOCUMENT_FIELD_NAME, FieldStatsResponse } from '../../common'; +import { DOCUMENT_FIELD_NAME } from '../../common'; import { IndexPattern, IndexPatternField, DraggedField } from './types'; import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { trackUiEvent } from '../lens_ui_telemetry'; @@ -160,12 +165,13 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { setState((s) => ({ ...s, isLoading: true })); core.http - .post>(`/api/lens/index_stats/${indexPattern.id}/field`, { + .post>(FIELD_STATS_API_PATH, { body: JSON.stringify({ dslQuery: buildEsQuery(indexPattern, query, filters, getEsQueryConfig(core.uiSettings)), fromDate: dateRange.fromDate, toDate: dateRange.toDate, fieldName: field.name, + dataViewId: indexPattern.id, }), }) .then((results) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts index a3d749f4308cf..f047bdbe7ba56 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts @@ -10,11 +10,11 @@ import { uniq } from 'lodash'; import type { CoreStart } from '@kbn/core/public'; import { buildEsQuery } from '@kbn/es-query'; import { getEsQueryConfig } from '@kbn/data-plugin/public'; +import { FieldStatsResponse, FIELD_STATS_API_PATH } from '@kbn/unified-field-list-plugin/public'; import { GenericIndexPatternColumn, operationDefinitionMap } from '..'; import { defaultLabel } from '../filters'; import { isReferenced } from '../../layer_helpers'; -import type { FieldStatsResponse } from '../../../../../common'; import type { FrameDatasourceAPI } from '../../../../types'; import type { FiltersIndexPatternColumn } from '..'; import type { TermsIndexPatternColumn } from './types'; @@ -133,9 +133,10 @@ export function getDisallowedTermsMessage( if (!activeDataFieldNameMatch || currentTerms.length === 0) { if (fieldNames.length === 1) { const response: FieldStatsResponse = await core.http.post( - `/api/lens/index_stats/${indexPattern.id}/field`, + FIELD_STATS_API_PATH, { body: JSON.stringify({ + dataViewId: indexPattern.id, fieldName: fieldNames[0], dslQuery: buildEsQuery( indexPattern, diff --git a/x-pack/plugins/lens/server/routes/index.ts b/x-pack/plugins/lens/server/routes/index.ts index d040812c27264..0d08c53ff8f0f 100644 --- a/x-pack/plugins/lens/server/routes/index.ts +++ b/x-pack/plugins/lens/server/routes/index.ts @@ -8,11 +8,9 @@ import { CoreSetup, Logger } from '@kbn/core/server'; import { PluginStartContract } from '../plugin'; import { existingFieldsRoute } from './existing_fields'; -import { initFieldsRoute } from './field_stats'; import { initLensUsageRoute } from './telemetry'; export function setupRoutes(setup: CoreSetup, logger: Logger) { existingFieldsRoute(setup, logger); - initFieldsRoute(setup); initLensUsageRoute(setup); } diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index df55e06710599..4e7728a041da3 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -37,6 +37,7 @@ { "path": "../../../src/plugins/data_view_editor/tsconfig.json"}, { "path": "../../../src/plugins/event_annotation/tsconfig.json"}, { "path": "../../../src/plugins/chart_expressions/expression_xy/tsconfig.json"}, - { "path": "../../../src/plugins/unified_search/tsconfig.json" } + { "path": "../../../src/plugins/unified_search/tsconfig.json" }, + { "path": "../../../src/plugins/unified_field_list/tsconfig.json" } ] } From 68405a5a4f4559fe0e364f4c5926a2557fcf417a Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 13 Jul 2022 18:19:47 +0000 Subject: [PATCH 03/92] [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs' --- docs/developer/plugin-list.asciidoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 041c0cee57359..f20140ada1c8d 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -299,6 +299,10 @@ In general this plugin provides: |Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. +|{kib-repo}blob/{branch}/src/plugins/unified_field_list/README.md[unifiedFieldList] +|A Kibana plugin + + |{kib-repo}blob/{branch}/src/plugins/unified_search/README.md[unifiedSearch] |Contains all the components of Kibana's unified search experience. Specifically: From d46c7a13c0a88a2ac9a4d02d0da7e4cab6713fc7 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 14 Jul 2022 17:59:19 +0200 Subject: [PATCH 04/92] [Discover] Address CI checks --- packages/kbn-optimizer/limits.yml | 1 + .../public/components/field_stats/index.tsx | 3 ++- src/plugins/unified_field_list/public/index.ts | 2 +- src/plugins/unified_field_list/server/index.ts | 7 ++++++- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 0dc1e1ee4675e..c36b2a04523a4 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -125,6 +125,7 @@ pageLoadAssetSize: cloudSecurityPosture: 19109 visTypeGauge: 24113 unifiedSearch: 71059 + unifiedFieldList: 16515 data: 454087 eventAnnotation: 19334 screenshotting: 22870 diff --git a/src/plugins/unified_field_list/public/components/field_stats/index.tsx b/src/plugins/unified_field_list/public/components/field_stats/index.tsx index fb4d09f68b779..8d93deaffe169 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/index.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/index.tsx @@ -6,4 +6,5 @@ * Side Public License, v 1. */ -export { FieldStatsProps, FieldStats } from './field_stats'; +export type { FieldStatsProps } from './field_stats'; +export { FieldStats } from './field_stats'; diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts index 1d0102cba9d4b..0064a3cfb737b 100755 --- a/src/plugins/unified_field_list/public/index.ts +++ b/src/plugins/unified_field_list/public/index.ts @@ -22,4 +22,4 @@ export { BASE_API_PATH, FIELD_STATS_API_PATH } from '../common/constants'; export function plugin() { return new UnifiedFieldListPlugin(); } -export { UnifiedFieldListPluginSetup, UnifiedFieldListPluginStart } from './types'; +export type { UnifiedFieldListPluginSetup, UnifiedFieldListPluginStart } from './types'; diff --git a/src/plugins/unified_field_list/server/index.ts b/src/plugins/unified_field_list/server/index.ts index 0b753d1636823..039ea0488b533 100755 --- a/src/plugins/unified_field_list/server/index.ts +++ b/src/plugins/unified_field_list/server/index.ts @@ -16,4 +16,9 @@ export function plugin(initializerContext: PluginInitializerContext) { return new UnifiedFieldListPlugin(initializerContext); } -export { UnifiedFieldListServerPluginSetup, UnifiedFieldListServerPluginStart } from './types'; +export type { + UnifiedFieldListServerPluginSetup, + UnifiedFieldListServerPluginStart, + PluginSetup, + PluginStart, +} from './types'; From f74243b86e418d0a25ede9e47b0bd89d080258eb Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 14 Jul 2022 22:01:21 +0200 Subject: [PATCH 05/92] [UnifiedFieldList] Move field stats UI from Lens to UnifiedFieldList plugin --- .../components/sidebar/discover_field.tsx | 4 +- src/plugins/unified_field_list/kibana.json | 2 +- .../components/field_stats/field_stats.tsx | 501 ++++++++++++++++-- .../field_stats/field_stats_from_sample.tsx | 45 ++ .../public/components/field_stats/index.tsx | 3 + .../hooks/use_unified_field_list_services.tsx | 14 + .../unified_field_list/public/index.ts | 3 +- .../unified_field_list/public/types.ts | 15 + src/plugins/unified_field_list/tsconfig.json | 4 +- .../indexpattern_datasource/field_item.tsx | 463 +--------------- .../indexpattern_datasource/indexpattern.tsx | 32 +- 11 files changed, 595 insertions(+), 491 deletions(-) create mode 100755 src/plugins/unified_field_list/public/components/field_stats/field_stats_from_sample.tsx create mode 100755 src/plugins/unified_field_list/public/hooks/use_unified_field_list_services.tsx diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 7f3e968e7c5c4..4d4b85b9aa712 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -26,7 +26,7 @@ import { UiCounterMetricType } from '@kbn/analytics'; import classNames from 'classnames'; import { FieldButton, FieldIcon } from '@kbn/react-field'; import type { DataViewField, DataView } from '@kbn/data-views-plugin/public'; -import { FieldStats } from '@kbn/unified-field-list-plugin/public'; +import { FieldStatsFromSample } from '@kbn/unified-field-list-plugin/public'; import { getFieldCapabilities } from '../../../../utils/get_field_capabilities'; import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon'; import { DiscoverFieldDetails } from './discover_field_details'; @@ -376,7 +376,7 @@ function DiscoverFieldComponent({ <> {showFieldStats && ( <> - +
{i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { diff --git a/src/plugins/unified_field_list/kibana.json b/src/plugins/unified_field_list/kibana.json index c0d1f9899e071..e2fd80fe79ad7 100755 --- a/src/plugins/unified_field_list/kibana.json +++ b/src/plugins/unified_field_list/kibana.json @@ -9,7 +9,7 @@ "description": "Contains functionality for the field list which can be integrated into apps sidebar", "server": true, "ui": true, - "requiredPlugins": ["dataViews"], + "requiredPlugins": ["dataViews", "data", "fieldFormats", "charts"], "optionalPlugins": [], "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 5e4ecafeaf69c..5bdb2fe368f39 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -6,40 +6,471 @@ * Side Public License, v 1. */ -import React, { useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { CoreStart } from '@kbn/core/public'; -import { EuiButton, EuiText } from '@elastic/eui'; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface FieldStatsProps {} - -export const FieldStats: React.FC = () => { - const { services } = useKibana<{ http: CoreStart['http'] }>(); - const { http } = services; - // Use React hooks to manage state. - const [timestamp, setTimestamp] = useState(); - - const onClickHandler = () => { - // Use the core http service to make a response to the server API. - http?.get('/api/unified_field_list/example').then((res) => { - setTimestamp((res as unknown as { time: string }).time); +import React, { useEffect, useState } from 'react'; +import { + DataView, + DataViewField, + ES_FIELD_TYPES, + getEsQueryConfig, + KBN_FIELD_TYPES, +} from '@kbn/data-plugin/common'; +import DateMath from '@kbn/datemath'; +import { + EuiButtonGroup, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiProgress, + EuiSpacer, + EuiText, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; +import { + Axis, + Chart, + HistogramBarSeries, + niceTimeFormatter, + Position, + ScaleType, + Settings, + TooltipType, +} from '@elastic/charts'; +import { i18n } from '@kbn/i18n'; +import { buildEsQuery, Query, Filter } from '@kbn/es-query'; +import type { BucketedAggregation, FieldStatsResponse } from '../../../common/types'; +import { FIELD_STATS_API_PATH } from '../../../common/constants'; +import { useUnifiedFieldListServices } from '../../hooks/use_unified_field_list_services'; + +interface State { + isLoading: boolean; + totalDocuments?: number; + sampledDocuments?: number; + sampledValues?: number; + histogram?: BucketedAggregation; + topValues?: BucketedAggregation; +} + +export interface FieldStatsProps { + query: Query; + filters: Filter[]; + fromDate: string; + toDate: string; + dataViewOrDataViewId: DataView | string; + field: DataViewField; + testSubject: string; +} + +// TODO: catch errors during rendering + +export const FieldStats: React.FC = ({ + query, + filters, + fromDate, + toDate, + dataViewOrDataViewId, + field, + testSubject, +}) => { + const services = useUnifiedFieldListServices(); + const { http, fieldFormats, uiSettings, charts, dataViews } = services; + const [state, setState] = useState({ + isLoading: false, + }); + const [dataView, setDataView] = useState(null); + + async function fetchData() { + // Range types don't have any useful stats we can show + if ( + state.isLoading || + field.type === 'document' || + field.type.includes('range') || + field.type === 'geo_point' || + field.type === 'geo_shape' + ) { + return; + } + + const loadedDataView = + typeof dataViewOrDataViewId === 'string' + ? await dataViews.get(dataViewOrDataViewId) + : dataViewOrDataViewId; + + setDataView(loadedDataView); + setState((s) => ({ ...s, isLoading: true })); + + http + .post>(FIELD_STATS_API_PATH, { + body: JSON.stringify({ + dslQuery: buildEsQuery(loadedDataView, query, filters, getEsQueryConfig(uiSettings)), + fromDate, + toDate, + fieldName: field.name, + dataViewId: loadedDataView.id, + }), + }) + .then((results) => { + setState((s) => ({ + ...s, + isLoading: false, + totalDocuments: results.totalDocuments, + sampledDocuments: results.sampledDocuments, + sampledValues: results.sampledValues, + histogram: results.histogram, + topValues: results.topValues, + })); + }) + .catch(() => { + setState((s) => ({ ...s, isLoading: false })); + }); + } + + useEffect(() => { + fetchData(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const chartTheme = charts.theme.useChartsTheme(); + const chartBaseTheme = charts.theme.useChartsBaseTheme(); + + const { isLoading, histogram, topValues, sampledValues, sampledDocuments, totalDocuments } = + state; + + let histogramDefault = !!state.histogram; + const fromDateParsed = DateMath.parse(fromDate); + const toDateParsed = DateMath.parse(toDate); + + const totalValuesCount = + topValues && topValues.buckets.reduce((prev, bucket) => bucket.count + prev, 0); + const otherCount = sampledValues && totalValuesCount ? sampledValues - totalValuesCount : 0; + + if ( + totalValuesCount && + histogram && + histogram.buckets.length && + topValues && + topValues.buckets.length + ) { + // Default to histogram when top values are less than 10% of total + histogramDefault = otherCount / totalValuesCount > 0.9; + } + + const [showingHistogram, setShowingHistogram] = useState(histogramDefault); + + if (isLoading || !dataView) { + return ; + } + + const formatter = dataView.getFormatterForField(field); + let title = <>; + + if (field.type.includes('range')) { + // TODO: new localization keys + return ( + <> + + {i18n.translate('xpack.lens.indexPattern.fieldStatsLimited', { + defaultMessage: `Summary information is not available for range type fields.`, + })} + + + ); + } + + if (field.type === 'murmur3') { + return ( + <> + + {i18n.translate('xpack.lens.indexPattern.fieldStatsMurmur3Limited', { + defaultMessage: `Summary information is not available for murmur3 fields.`, + })} + + + ); + } + + if (field.type === 'geo_point' || field.type === 'geo_shape') { + return <>{/* TODO allow to add a custom view (Visualize)?*/}; + } + + if ( + (!histogram || histogram.buckets.length === 0) && + (!topValues || topValues.buckets.length === 0) + ) { + const isUsingSampling = services.uiSettings.get('lens:useFieldExistenceSampling'); // TODO: what to do with this setting? + return ( + <> + + {isUsingSampling + ? i18n.translate('xpack.lens.indexPattern.fieldStatsSamplingNoData', { + defaultMessage: + 'Lens is unable to create visualizations with this field because it does not contain data in the first 500 documents that match your filters. To create a visualization, drag and drop a different field.', + }) + : i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { + defaultMessage: + 'Lens is unable to create visualizations with this field because it does not contain data. To create a visualization, drag and drop a different field.', + })} + + + ); + } + + if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { + title = ( + { + setShowingHistogram(optionId === 'histogram'); + }} + idSelected={showingHistogram ? 'histogram' : 'topValues'} + /> + ); + } else if (field.type === 'date') { + title = ( + +
+ {i18n.translate('xpack.lens.indexPattern.fieldTimeDistributionLabel', { + defaultMessage: 'Time distribution', + })} +
+
+ ); + } else if (topValues && topValues.buckets.length) { + title = ( + +
+ {i18n.translate('xpack.lens.indexPattern.fieldTopValuesLabel', { + defaultMessage: 'Top values', + })} +
+
+ ); + } + + function combineWithTitleAndFooter(el: React.ReactElement) { + return ( + <> + {title ? title : <>} + + + + {el} + + + + {totalDocuments ? ( + + {sampledDocuments && ( + <> + {i18n.translate('xpack.lens.indexPattern.percentageOfLabel', { + defaultMessage: '{percentage}% of', + values: { + percentage: Math.round((sampledDocuments / totalDocuments) * 100), + }, + })} + + )}{' '} + + {fieldFormats + .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) + .convert(totalDocuments)} + {' '} + {i18n.translate('xpack.lens.indexPattern.ofDocumentsLabel', { + defaultMessage: 'documents', + })} + + ) : ( + <> + )} + + ); + } + + if (histogram && histogram.buckets.length) { + const specId = i18n.translate('xpack.lens.indexPattern.fieldStatsCountLabel', { + defaultMessage: 'Count', }); - }; - - return ( - -

- - - - -

-
- ); + + if (field.type === 'date') { + return combineWithTitleAndFooter( + + + + + + + + ); + } + + if (showingHistogram || !topValues || !topValues.buckets.length) { + return combineWithTitleAndFooter( + + + + formatter.convert(d)} + /> + + + + ); + } + } + + if (topValues && topValues.buckets.length) { + const digitsRequired = topValues.buckets.some( + (topValue) => !Number.isInteger(topValue.count / sampledValues!) + ); + return combineWithTitleAndFooter( +
+ {topValues.buckets.map((topValue) => { + const formatted = formatter.convert(topValue.key); + return ( + // TODO: move styles next to this file +
+ + + {formatted === '' ? ( + + + {i18n.translate('xpack.lens.indexPattern.fieldPanelEmptyStringValue', { + defaultMessage: 'Empty string', + })} + + + ) : ( + + + {formatted} + + + )} + + + + {(Math.round((topValue.count / sampledValues!) * 1000) / 10).toFixed( + digitsRequired ? 1 : 0 + )} + % + + + + +
+ ); + })} + {otherCount ? ( + <> + + + + {i18n.translate('xpack.lens.indexPattern.otherDocsLabel', { + defaultMessage: 'Other', + })} + + + + + + {(Math.round((otherCount / sampledValues!) * 1000) / 10).toFixed( + digitsRequired ? 1 : 0 + )} + % + + + + + + + ) : ( + <> + )} +
+ ); + } + + return <>; }; diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_sample.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_sample.tsx new file mode 100755 index 0000000000000..2e0170093f88e --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_sample.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { CoreStart } from '@kbn/core/public'; +import { EuiButton, EuiText } from '@elastic/eui'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface FieldStatsFromSampleProps {} + +export const FieldStatsFromSample: React.FC = () => { + const { services } = useKibana<{ http: CoreStart['http'] }>(); + const { http } = services; + // Use React hooks to manage state. + const [timestamp, setTimestamp] = useState(); + + const onClickHandler = () => { + // Use the core http service to make a response to the server API. + http?.get('/api/unified_field_list/example').then((res) => { + setTimestamp((res as unknown as { time: string }).time); + }); + }; + + return ( + +

+ + + + +

+
+ ); +}; diff --git a/src/plugins/unified_field_list/public/components/field_stats/index.tsx b/src/plugins/unified_field_list/public/components/field_stats/index.tsx index 8d93deaffe169..efe2ae40e98db 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/index.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/index.tsx @@ -8,3 +8,6 @@ export type { FieldStatsProps } from './field_stats'; export { FieldStats } from './field_stats'; + +export type { FieldStatsFromSampleProps } from './field_stats_from_sample'; +export { FieldStatsFromSample } from './field_stats_from_sample'; diff --git a/src/plugins/unified_field_list/public/hooks/use_unified_field_list_services.tsx b/src/plugins/unified_field_list/public/hooks/use_unified_field_list_services.tsx new file mode 100755 index 0000000000000..e372e13908c64 --- /dev/null +++ b/src/plugins/unified_field_list/public/hooks/use_unified_field_list_services.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { UnifiedFieldListServices } from '../types'; + +export const useUnifiedFieldListServices = (): UnifiedFieldListServices => { + return useKibana().services; +}; diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts index 0064a3cfb737b..85055a48e0ebf 100755 --- a/src/plugins/unified_field_list/public/index.ts +++ b/src/plugins/unified_field_list/public/index.ts @@ -14,7 +14,8 @@ export type { NumberStatsResult, TopValuesResult, } from '../common/types'; -export { FieldStats, FieldStatsProps } from './components/field_stats'; +export type { FieldStatsProps, FieldStatsFromSampleProps } from './components/field_stats'; +export { FieldStats, FieldStatsFromSample } from './components/field_stats'; export { BASE_API_PATH, FIELD_STATS_API_PATH } from '../common/constants'; // This exports static code and TypeScript types, diff --git a/src/plugins/unified_field_list/public/types.ts b/src/plugins/unified_field_list/public/types.ts index feb24509cdee5..b7ea064952c75 100755 --- a/src/plugins/unified_field_list/public/types.ts +++ b/src/plugins/unified_field_list/public/types.ts @@ -6,8 +6,23 @@ * Side Public License, v 1. */ +import { HttpStart, IUiSettingsClient } from '@kbn/core/public'; +import { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface UnifiedFieldListPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface UnifiedFieldListPluginStart {} + +export interface UnifiedFieldListServices { + http: HttpStart; + uiSettings: IUiSettingsClient; + dataViews: DataViewsContract; + data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; + charts: ChartsPluginSetup; +} diff --git a/src/plugins/unified_field_list/tsconfig.json b/src/plugins/unified_field_list/tsconfig.json index e74e3d26f3e28..221729fbd2b71 100644 --- a/src/plugins/unified_field_list/tsconfig.json +++ b/src/plugins/unified_field_list/tsconfig.json @@ -16,6 +16,8 @@ { "path": "../../core/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, - { "path": "../data_views/tsconfig.json" } + { "path": "../data_views/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../charts/tsconfig.json" } ] } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index a3ff80463fd6c..03233ddb964d5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -8,46 +8,27 @@ import './field_item.scss'; import React, { useCallback, useState, useMemo } from 'react'; -import DateMath from '@kbn/datemath'; import { - EuiButtonGroup, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiIconTip, - EuiLoadingSpinner, EuiPopover, - EuiPopoverFooter, EuiPopoverTitle, - EuiProgress, EuiSpacer, EuiText, EuiTitle, EuiToolTip, } from '@elastic/eui'; -import { - Axis, - HistogramBarSeries, - Chart, - niceTimeFormatter, - Position, - ScaleType, - Settings, - TooltipType, -} from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { FieldButton } from '@kbn/react-field'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { EuiHighlight } from '@elastic/eui'; -import { Filter, buildEsQuery, Query } from '@kbn/es-query'; -import { KBN_FIELD_TYPES, ES_FIELD_TYPES, getEsQueryConfig } from '@kbn/data-plugin/public'; +import { Filter, Query } from '@kbn/es-query'; +import { DataViewField } from '@kbn/data-views-plugin/common'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { - BucketedAggregation, - FieldStatsResponse, - FIELD_STATS_API_PATH, -} from '@kbn/unified-field-list-plugin/public'; +import { FieldStats } from '@kbn/unified-field-list-plugin/public'; import { DragDrop, DragDropIdentifier } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; import { DOCUMENT_FIELD_NAME } from '../../common'; @@ -80,15 +61,6 @@ export interface FieldItemProps { uiActions: UiActionsStart; } -interface State { - isLoading: boolean; - totalDocuments?: number; - sampledDocuments?: number; - sampledValues?: number; - histogram?: BucketedAggregation; - topValues?: BucketedAggregation; -} - function wrapOnDot(str?: string) { // u200B is a non-width white-space character, which allows // the browser to efficiently word-wrap right after the dot @@ -98,14 +70,10 @@ function wrapOnDot(str?: string) { export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { const { - core, field, indexPattern, highlight, exists, - query, - dateRange, - filters, hideDetails, itemIndex, groupIndex, @@ -146,55 +114,10 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { [dropOntoWorkspace, setOpen] ); - const [state, setState] = useState({ - isLoading: false, - }); - - function fetchData() { - // Range types don't have any useful stats we can show - if ( - state.isLoading || - field.type === 'document' || - field.type.includes('range') || - field.type === 'geo_point' || - field.type === 'geo_shape' - ) { - return; - } - - setState((s) => ({ ...s, isLoading: true })); - - core.http - .post>(FIELD_STATS_API_PATH, { - body: JSON.stringify({ - dslQuery: buildEsQuery(indexPattern, query, filters, getEsQueryConfig(core.uiSettings)), - fromDate: dateRange.fromDate, - toDate: dateRange.toDate, - fieldName: field.name, - dataViewId: indexPattern.id, - }), - }) - .then((results) => { - setState((s) => ({ - ...s, - isLoading: false, - totalDocuments: results.totalDocuments, - sampledDocuments: results.sampledDocuments, - sampledValues: results.sampledValues, - histogram: results.histogram, - topValues: results.topValues, - })); - }) - .catch(() => { - setState((s) => ({ ...s, isLoading: false })); - }); - } - function togglePopover() { setOpen(!infoIsOpen); if (!infoIsOpen) { trackUiEvent('indexpattern_field_info_click'); - fetchData(); } } @@ -290,7 +213,6 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { initialFocus=".lnsFieldItem__fieldPanel" > bucket.count + prev, 0); - const otherCount = sampledValues && totalValuesCount ? sampledValues - totalValuesCount : 0; - - if ( - totalValuesCount && - histogram && - histogram.buckets.length && - topValues && - topValues.buckets.length - ) { - // Default to histogram when top values are less than 10% of total - histogramDefault = otherCount / totalValuesCount > 0.9; - } - - const [showingHistogram, setShowingHistogram] = useState(histogramDefault); - const panelHeader = ( string }; - if (indexPattern.fieldFormatMap && indexPattern.fieldFormatMap[field.name]) { - const FormatType = fieldFormats.getType(indexPattern.fieldFormatMap[field.name].id); - if (FormatType) { - formatter = new FormatType( - indexPattern.fieldFormatMap[field.name].params, - core.uiSettings.get.bind(core.uiSettings) - ); - } else { - formatter = { convert: (data: unknown) => JSON.stringify(data) }; - } - } else { - formatter = fieldFormats.getDefaultInstance( - field.type as KBN_FIELD_TYPES, - field.esTypes as ES_FIELD_TYPES[] - ); - } - - const fromDate = DateMath.parse(dateRange.fromDate); - const toDate = DateMath.parse(dateRange.toDate); - - let title = <>; - - if (props.isLoading) { - return ; - } else if (field.type.includes('range')) { - return ( - <> - {panelHeader} - - - {i18n.translate('xpack.lens.indexPattern.fieldStatsLimited', { - defaultMessage: `Summary information is not available for range type fields.`, - })} - - - ); - } else if (field.type === 'murmur3') { - return ( - <> - {panelHeader} - - - {i18n.translate('xpack.lens.indexPattern.fieldStatsMurmur3Limited', { - defaultMessage: `Summary information is not available for murmur3 fields.`, - })} - - - ); - } else if (field.type === 'geo_point' || field.type === 'geo_shape') { - return ( + const stats = + field.type === 'geo_point' || field.type === 'geo_shape' ? ( <> - {panelHeader} - {getVisualizeGeoFieldMessage(field.type)} @@ -499,287 +345,24 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { fieldName={field.name} /> - ); - } else if ( - (!props.histogram || props.histogram.buckets.length === 0) && - (!props.topValues || props.topValues.buckets.length === 0) - ) { - const isUsingSampling = core.uiSettings.get('lens:useFieldExistenceSampling'); - return ( - <> - {panelHeader} - - - {isUsingSampling - ? i18n.translate('xpack.lens.indexPattern.fieldStatsSamplingNoData', { - defaultMessage: - 'Lens is unable to create visualizations with this field because it does not contain data in the first 500 documents that match your filters. To create a visualization, drag and drop a different field.', - }) - : i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { - defaultMessage: - 'Lens is unable to create visualizations with this field because it does not contain data. To create a visualization, drag and drop a different field.', - })} - - - ); - } - - if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { - title = ( - { - setShowingHistogram(optionId === 'histogram'); - }} - idSelected={showingHistogram ? 'histogram' : 'topValues'} + ) : ( + ); - } else if (field.type === 'date') { - title = ( - -
- {i18n.translate('xpack.lens.indexPattern.fieldTimeDistributionLabel', { - defaultMessage: 'Time distribution', - })} -
-
- ); - } else if (topValues && topValues.buckets.length) { - title = ( - -
- {i18n.translate('xpack.lens.indexPattern.fieldTopValuesLabel', { - defaultMessage: 'Top values', - })} -
-
- ); - } - - function wrapInPopover(el: React.ReactElement) { - return ( - <> - {panelHeader} - - {title ? title : <>} - - - {el} - - {props.totalDocuments ? ( - - - {props.sampledDocuments && ( - <> - {i18n.translate('xpack.lens.indexPattern.percentageOfLabel', { - defaultMessage: '{percentage}% of', - values: { - percentage: Math.round((props.sampledDocuments / props.totalDocuments) * 100), - }, - })} - - )}{' '} - - {fieldFormats - .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) - .convert(props.totalDocuments)} - {' '} - {i18n.translate('xpack.lens.indexPattern.ofDocumentsLabel', { - defaultMessage: 'documents', - })} - - - ) : ( - <> - )} - - ); - } - - if (histogram && histogram.buckets.length) { - const specId = i18n.translate('xpack.lens.indexPattern.fieldStatsCountLabel', { - defaultMessage: 'Count', - }); - - if (field.type === 'date') { - return wrapInPopover( - - - - - - - - ); - } else if (showingHistogram || !topValues || !topValues.buckets.length) { - return wrapInPopover( - - - - formatter.convert(d)} - /> - - - - ); - } - } - - if (props.topValues && props.topValues.buckets.length) { - const digitsRequired = props.topValues.buckets.some( - (topValue) => !Number.isInteger(topValue.count / props.sampledValues!) - ); - return wrapInPopover( -
- {props.topValues.buckets.map((topValue) => { - const formatted = formatter.convert(topValue.key); - return ( -
- - - {formatted === '' ? ( - - - {i18n.translate('xpack.lens.indexPattern.fieldPanelEmptyStringValue', { - defaultMessage: 'Empty string', - })} - - - ) : ( - - - {formatted} - - - )} - - - - {(Math.round((topValue.count / props.sampledValues!) * 1000) / 10).toFixed( - digitsRequired ? 1 : 0 - )} - % - - - - - -
- ); - })} - {otherCount ? ( - <> - - - - {i18n.translate('xpack.lens.indexPattern.otherDocsLabel', { - defaultMessage: 'Other', - })} - - - - - - {(Math.round((otherCount / props.sampledValues!) * 1000) / 10).toFixed( - digitsRequired ? 1 : 0 - )} - % - - - - - - - ) : ( - <> - )} -
- ); - } - return <>; + return ( + <> + {panelHeader} + {stats} + + ); } const DragToWorkspaceButton = ({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index fe6d6adef39f4..2ef26fd8365ce 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -267,17 +267,27 @@ export function getIndexPatternDatasource({ render( - + + + , domElement From 968454a2be7fa504fa82cbcce5067d0bd14264de Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 18 Jul 2022 15:57:59 +0200 Subject: [PATCH 06/92] [Discover] Integrate FieldStats into Discover field popover --- .../components/sidebar/discover_field.tsx | 50 +++++++++++++------ .../components/sidebar/discover_sidebar.tsx | 4 ++ src/plugins/discover/public/build_services.ts | 2 + 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 4d4b85b9aa712..b3652ce3bc326 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -26,13 +26,15 @@ import { UiCounterMetricType } from '@kbn/analytics'; import classNames from 'classnames'; import { FieldButton, FieldIcon } from '@kbn/react-field'; import type { DataViewField, DataView } from '@kbn/data-views-plugin/public'; -import { FieldStatsFromSample } from '@kbn/unified-field-list-plugin/public'; +import { FieldStats } from '@kbn/unified-field-list-plugin/public'; import { getFieldCapabilities } from '../../../../utils/get_field_capabilities'; import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon'; -import { DiscoverFieldDetails } from './discover_field_details'; +// import { DiscoverFieldDetails } from './discover_field_details'; import { FieldDetails } from './types'; import { getFieldTypeName } from '../../../../utils/get_field_type_name'; import { DiscoverFieldVisualize } from './discover_field_visualize'; +import type { AppState } from '../../services/discover_state'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; function wrapOnDot(str?: string) { // u200B is a non-width white-space character, which allows @@ -263,6 +265,11 @@ export interface DiscoverFieldProps { * Optionally show or hide field stats in the popover */ showFieldStats?: boolean; + + /** + * Discover App State + */ + state: AppState; } function DiscoverFieldComponent({ @@ -279,7 +286,10 @@ function DiscoverFieldComponent({ onEditField, onDeleteField, showFieldStats, + state, }: DiscoverFieldProps) { + const services = useDiscoverServices(); + const { data } = services; const [infoIsOpen, setOpen] = useState(false); const toggleDisplay = useCallback( @@ -372,24 +382,34 @@ function DiscoverFieldComponent({ const renderPopover = () => { const details = getDetails(field); + const dateRange = data.query.timefilter.timefilter.getTime(); + return ( <> {showFieldStats && ( <> - - -
- {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { - defaultMessage: 'Top 5 values', - })} -
-
- + {/* */} + {/*
*/} + {/* {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', {*/} + {/* defaultMessage: 'Top 5 values',*/} + {/* })}*/} + {/*
*/} + {/*
*/} + {/* */} )} @@ -404,7 +424,7 @@ function DiscoverFieldComponent({ )} {(showFieldStats || multiFields) && } - (null); @@ -404,6 +405,7 @@ export function DiscoverSidebarComponent({ onEditField={editField} onDeleteField={deleteField} showFieldStats={showFieldStats} + state={state} /> ); @@ -464,6 +466,7 @@ export function DiscoverSidebarComponent({ onEditField={editField} onDeleteField={deleteField} showFieldStats={showFieldStats} + state={state} /> ); @@ -493,6 +496,7 @@ export function DiscoverSidebarComponent({ onEditField={editField} onDeleteField={deleteField} showFieldStats={showFieldStats} + state={state} /> ); diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index d77bc5dde2660..400b3ae00fe43 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -81,6 +81,7 @@ export interface DiscoverServices { spaces?: SpacesApi; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; locator: DiscoverAppLocator; + charts: ChartsPluginStart; } export const buildServices = memoize(function ( @@ -125,5 +126,6 @@ export const buildServices = memoize(function ( dataViewEditor: plugins.dataViewEditor, triggersActionsUi: plugins.triggersActionsUi, locator, + charts: plugins.charts, }; }); From f14fe4c7e14bcc1d57041d5068a8557cb3f24003 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 18 Jul 2022 16:13:29 +0200 Subject: [PATCH 07/92] [Discover] Show both views side to side --- .../components/sidebar/discover_field.tsx | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index b3652ce3bc326..2d08f7f11f14b 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -20,6 +20,7 @@ import { EuiFlexItem, EuiSpacer, EuiHorizontalRule, + EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UiCounterMetricType } from '@kbn/analytics'; @@ -29,7 +30,7 @@ import type { DataViewField, DataView } from '@kbn/data-views-plugin/public'; import { FieldStats } from '@kbn/unified-field-list-plugin/public'; import { getFieldCapabilities } from '../../../../utils/get_field_capabilities'; import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon'; -// import { DiscoverFieldDetails } from './discover_field_details'; +import { DiscoverFieldDetails } from './discover_field_details'; import { FieldDetails } from './types'; import { getFieldTypeName } from '../../../../utils/get_field_type_name'; import { DiscoverFieldVisualize } from './discover_field_visualize'; @@ -388,28 +389,38 @@ function DiscoverFieldComponent({ <> {showFieldStats && ( <> + + {'Stats as in Lens:'} + + + {/* TODO: remove previous field stats view when we finish FieldStats component and add addFilter buttons to it */} + + + {'Current Discover stats:'} + + + +
+ {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { + defaultMessage: 'Top 5 values', + })} +
+
+ - {/* */} - {/*
*/} - {/* {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', {*/} - {/* defaultMessage: 'Top 5 values',*/} - {/* })}*/} - {/*
*/} - {/*
*/} - {/* */} )} From 116098b8716010d6ba42b2ca42e01d6bde838265 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 18 Jul 2022 16:41:20 +0200 Subject: [PATCH 08/92] [Discover] Allow for some customization --- .../components/sidebar/discover_field.tsx | 5 +++ .../components/field_stats/field_stats.tsx | 24 ++++++++---- .../indexpattern_datasource/field_item.tsx | 39 ++++++++++--------- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 2d08f7f11f14b..42d03ee8d734a 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -401,6 +401,11 @@ function DiscoverFieldComponent({ dataViewOrDataViewId={indexPattern} field={multiFields ? multiFields[0].field : field} // TODO: how to handle multifields? testSubject="dscFieldListPanel" + overrideContent={(currentField) => { + return ( + {`TODO: add a custom "not available" message for ${currentField.type} field`} + ); + }} /> {/* TODO: remove previous field stats view when we finish FieldStats component and add addFilter buttons to it */} diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 5bdb2fe368f39..2cf63039fd6d0 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -59,6 +59,7 @@ export interface FieldStatsProps { dataViewOrDataViewId: DataView | string; field: DataViewField; testSubject: string; + overrideContent?: (field: DataViewField) => JSX.Element | null; } // TODO: catch errors during rendering @@ -71,6 +72,7 @@ export const FieldStats: React.FC = ({ dataViewOrDataViewId, field, testSubject, + overrideContent, }) => { const services = useUnifiedFieldListServices(); const { http, fieldFormats, uiSettings, charts, dataViews } = services; @@ -80,6 +82,13 @@ export const FieldStats: React.FC = ({ const [dataView, setDataView] = useState(null); async function fetchData() { + const loadedDataView = + typeof dataViewOrDataViewId === 'string' + ? await dataViews.get(dataViewOrDataViewId) + : dataViewOrDataViewId; + + setDataView(loadedDataView); + // Range types don't have any useful stats we can show if ( state.isLoading || @@ -91,12 +100,6 @@ export const FieldStats: React.FC = ({ return; } - const loadedDataView = - typeof dataViewOrDataViewId === 'string' - ? await dataViews.get(dataViewOrDataViewId) - : dataViewOrDataViewId; - - setDataView(loadedDataView); setState((s) => ({ ...s, isLoading: true })); http @@ -118,6 +121,7 @@ export const FieldStats: React.FC = ({ sampledValues: results.sampledValues, histogram: results.histogram, topValues: results.topValues, + dataView: loadedDataView, })); }) .catch(() => { @@ -156,10 +160,14 @@ export const FieldStats: React.FC = ({ const [showingHistogram, setShowingHistogram] = useState(histogramDefault); - if (isLoading || !dataView) { + if (isLoading) { return ; } + if (!dataView) { + return null; + } + const formatter = dataView.getFormatterForField(field); let title = <>; @@ -189,7 +197,7 @@ export const FieldStats: React.FC = ({ } if (field.type === 'geo_point' || field.type === 'geo_shape') { - return <>{/* TODO allow to add a custom view (Visualize)?*/}; + return overrideContent?.(field) || null; } if ( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 03233ddb964d5..a3245fac18628 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -333,19 +333,9 @@ function FieldItemPopoverContents(props: FieldItemProps) { return panelHeader; } - const stats = - field.type === 'geo_point' || field.type === 'geo_shape' ? ( - <> - {getVisualizeGeoFieldMessage(field.type)} - - - - - ) : ( + return ( + <> + {panelHeader} - ); + overrideContent={(currentField) => { + if (currentField.type === 'geo_point' || currentField.type === 'geo_shape') { + return ( + <> + {getVisualizeGeoFieldMessage(currentField.type)} - return ( - <> - {panelHeader} - {stats} + + + + ); + } + return null; + }} + /> ); } From 3243220cb1e495e2848ff3c4f5b8ecdaba7d7451 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 19 Jul 2022 12:38:28 +0200 Subject: [PATCH 09/92] [Discover] Allow for more customization --- .../components/sidebar/discover_field.tsx | 10 ++++++-- .../components/field_stats/field_stats.tsx | 24 +++++-------------- .../indexpattern_datasource/field_item.tsx | 23 +++++++++++++++++- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 42d03ee8d734a..745ad74038504 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -401,9 +401,15 @@ function DiscoverFieldComponent({ dataViewOrDataViewId={indexPattern} field={multiFields ? multiFields[0].field : field} // TODO: how to handle multifields? testSubject="dscFieldListPanel" - overrideContent={(currentField) => { + overrideContent={(currentField, params) => { + if (params?.noDataFound) { + return ( + {`TODO: add a custom "no data available" message for ${currentField.type} field`} + ); + } + return ( - {`TODO: add a custom "not available" message for ${currentField.type} field`} + {`TODO: add a custom "stats are not available" message for ${currentField.type} field`} ); }} /> diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 2cf63039fd6d0..36d87d69227b1 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -59,7 +59,10 @@ export interface FieldStatsProps { dataViewOrDataViewId: DataView | string; field: DataViewField; testSubject: string; - overrideContent?: (field: DataViewField) => JSX.Element | null; + overrideContent?: ( + field: DataViewField, + params?: { noDataFound?: boolean } + ) => JSX.Element | null; } // TODO: catch errors during rendering @@ -204,22 +207,7 @@ export const FieldStats: React.FC = ({ (!histogram || histogram.buckets.length === 0) && (!topValues || topValues.buckets.length === 0) ) { - const isUsingSampling = services.uiSettings.get('lens:useFieldExistenceSampling'); // TODO: what to do with this setting? - return ( - <> - - {isUsingSampling - ? i18n.translate('xpack.lens.indexPattern.fieldStatsSamplingNoData', { - defaultMessage: - 'Lens is unable to create visualizations with this field because it does not contain data in the first 500 documents that match your filters. To create a visualization, drag and drop a different field.', - }) - : i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { - defaultMessage: - 'Lens is unable to create visualizations with this field because it does not contain data. To create a visualization, drag and drop a different field.', - })} - - - ); + return overrideContent?.(field, { noDataFound: true }) || null; } if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { @@ -480,5 +468,5 @@ export const FieldStats: React.FC = ({ ); } - return <>; + return null; }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index a3245fac18628..641636be90922 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -316,6 +316,7 @@ function FieldItemPopoverContents(props: FieldItemProps) { hasSuggestionForField, hideDetails, uiActions, + core, } = props; const panelHeader = ( @@ -344,7 +345,7 @@ function FieldItemPopoverContents(props: FieldItemProps) { dataViewOrDataViewId={indexPattern.id} field={field as DataViewField} testSubject="lnsFieldListPanel" - overrideContent={(currentField) => { + overrideContent={(currentField, params) => { if (currentField.type === 'geo_point' || currentField.type === 'geo_shape') { return ( <> @@ -359,6 +360,26 @@ function FieldItemPopoverContents(props: FieldItemProps) { ); } + + if (params?.noDataFound) { + const isUsingSampling = core.uiSettings.get('lens:useFieldExistenceSampling'); + return ( + <> + + {isUsingSampling + ? i18n.translate('xpack.lens.indexPattern.fieldStatsSamplingNoData', { + defaultMessage: + 'Lens is unable to create visualizations with this field because it does not contain data in the first 500 documents that match your filters. To create a visualization, drag and drop a different field.', + }) + : i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { + defaultMessage: + 'Lens is unable to create visualizations with this field because it does not contain data. To create a visualization, drag and drop a different field.', + })} + + + ); + } + return null; }} /> From b7e77a367dedd34a39417d281f9eaaa2f5d35419 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 19 Jul 2022 12:41:24 +0200 Subject: [PATCH 10/92] [UnifiedFieldList] Remove temporary code --- .../field_stats/field_stats_from_sample.tsx | 34 ++----------------- .../unified_field_list/server/routes/index.ts | 16 --------- 2 files changed, 3 insertions(+), 47 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_sample.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_sample.tsx index 2e0170093f88e..1bd87178bec15 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_sample.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_sample.tsx @@ -6,40 +6,12 @@ * Side Public License, v 1. */ -import React, { useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { CoreStart } from '@kbn/core/public'; -import { EuiButton, EuiText } from '@elastic/eui'; +import React from 'react'; +import { EuiText } from '@elastic/eui'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface FieldStatsFromSampleProps {} export const FieldStatsFromSample: React.FC = () => { - const { services } = useKibana<{ http: CoreStart['http'] }>(); - const { http } = services; - // Use React hooks to manage state. - const [timestamp, setTimestamp] = useState(); - - const onClickHandler = () => { - // Use the core http service to make a response to the server API. - http?.get('/api/unified_field_list/example').then((res) => { - setTimestamp((res as unknown as { time: string }).time); - }); - }; - - return ( - -

- - - - -

-
- ); + return {'TODO: move current field stats from Discover to this component'}; }; diff --git a/src/plugins/unified_field_list/server/routes/index.ts b/src/plugins/unified_field_list/server/routes/index.ts index 96adaa0fb7591..3558e93e4a780 100755 --- a/src/plugins/unified_field_list/server/routes/index.ts +++ b/src/plugins/unified_field_list/server/routes/index.ts @@ -12,20 +12,4 @@ import { initFieldStatsRoute } from './field_stats'; export function defineRoutes(setup: CoreSetup) { initFieldStatsRoute(setup); - - // TODO: remove this temporary code - const router = setup.http.createRouter(); - router.get( - { - path: '/api/unified_field_list/example', - validate: false, - }, - async (context, request, response) => { - return response.ok({ - body: { - time: new Date().toISOString(), - }, - }); - } - ); } From 4bac5915bd8b924ddda40bb7d71f7eb7086ed875 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 19 Jul 2022 16:22:10 +0200 Subject: [PATCH 11/92] [UnifiedFieldList] Extract styles --- .../components/field_stats/field_stats.scss | 16 ++++++++++++++++ .../components/field_stats/field_stats.tsx | 10 +++++----- .../indexpattern_datasource/field_item.scss | 17 ----------------- 3 files changed, 21 insertions(+), 22 deletions(-) create mode 100644 src/plugins/unified_field_list/public/components/field_stats/field_stats.scss diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.scss b/src/plugins/unified_field_list/public/components/field_stats/field_stats.scss new file mode 100644 index 0000000000000..3c96c78935d4e --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.scss @@ -0,0 +1,16 @@ +.unifiedFieldList__fieldStats__topValue { + margin-bottom: $euiSizeS; + + &:last-of-type { + margin-bottom: 0; + } +} + +.unifiedFieldList__fieldStats__topValueProgress { + background-color: $euiColorLightestShade; + + // sass-lint:disable-block no-vendor-prefixes + &::-webkit-progress-bar { + background-color: $euiColorLightestShade; + } +} diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 36d87d69227b1..f70f943447dda 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -41,6 +41,7 @@ import { buildEsQuery, Query, Filter } from '@kbn/es-query'; import type { BucketedAggregation, FieldStatsResponse } from '../../../common/types'; import { FIELD_STATS_API_PATH } from '../../../common/constants'; import { useUnifiedFieldListServices } from '../../hooks/use_unified_field_list_services'; +import './field_stats.scss'; interface State { isLoading: boolean; @@ -213,7 +214,6 @@ export const FieldStats: React.FC = ({ if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { title = ( = ({ {topValues.buckets.map((topValue) => { const formatted = formatter.convert(topValue.key); return ( - // TODO: move styles next to this file -
+ // TODO: convert styles to `css` prop +
= ({ = ({ Date: Wed, 3 Aug 2022 13:00:33 +0200 Subject: [PATCH 12/92] [UnifiedFieldList] Fix after merge --- .../components/sidebar/discover_field.tsx | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 4136d0ca19810..26d59a4d5f02e 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -409,7 +409,7 @@ function DiscoverFieldComponent({ const renderPopover = () => { const details = getDetails(field); - const dateRange = data.query.timefilter.timefilter.getTime(); + const dateRange = data?.query?.timefilter.timefilter.getTime(); return ( <> @@ -419,26 +419,28 @@ function DiscoverFieldComponent({ {'Stats as in Lens:'} - { - if (params?.noDataFound) { + {Boolean(dateRange) && ( + { + if (params?.noDataFound) { + return ( + {`TODO: add a custom "no data available" message for ${currentField.type} field`} + ); + } + return ( - {`TODO: add a custom "no data available" message for ${currentField.type} field`} + {`TODO: add a custom "stats are not available" message for ${currentField.type} field`} ); - } - - return ( - {`TODO: add a custom "stats are not available" message for ${currentField.type} field`} - ); - }} - /> + }} + /> + )} {/* TODO: remove previous field stats view when we finish FieldStats component and add addFilter buttons to it */} From 5c5f4d254fea62786582786351e58c5628ff21aa Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 3 Aug 2022 13:09:47 +0200 Subject: [PATCH 13/92] [UnifiedFieldList] Extend i18n --- .i18nrc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.i18nrc.json b/.i18nrc.json index 08f2ff151b4c2..f6bbf035fd71d 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -91,7 +91,8 @@ "visTypeVislib": "src/plugins/vis_types/vislib", "visTypeXy": "src/plugins/vis_types/xy", "visualizations": "src/plugins/visualizations", - "unifiedSearch": "src/plugins/unified_search" + "unifiedSearch": "src/plugins/unified_search", + "unifiedFieldList": "src/plugins/unified_field_list" }, "translations": [] } From 67f0568442f023789a06d097198508eb69676e84 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 3 Aug 2022 17:54:04 +0200 Subject: [PATCH 14/92] [UnifiedFieldList] Migrate stats API from server to public --- .../services/field_stats}/field_stats.ts | 220 ++++++++---------- .../services/field_stats}/index.ts | 8 +- src/plugins/unified_field_list/kibana.json | 2 +- .../components/field_stats/field_stats.tsx | 56 +++-- .../unified_field_list/public/index.ts | 2 + .../unified_field_list/server/index.ts | 24 -- .../unified_field_list/server/plugin.ts | 41 ---- .../unified_field_list/server/types.ts | 22 -- 8 files changed, 126 insertions(+), 249 deletions(-) rename src/plugins/unified_field_list/{server/routes => common/services/field_stats}/field_stats.ts (58%) rename src/plugins/unified_field_list/{server/routes => common/services/field_stats}/index.ts (60%) delete mode 100755 src/plugins/unified_field_list/server/index.ts delete mode 100755 src/plugins/unified_field_list/server/plugin.ts delete mode 100755 src/plugins/unified_field_list/server/types.ts diff --git a/src/plugins/unified_field_list/server/routes/field_stats.ts b/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts similarity index 58% rename from src/plugins/unified_field_list/server/routes/field_stats.ts rename to src/plugins/unified_field_list/common/services/field_stats/field_stats.ts index 14a549116bd6e..16a1c807a208a 100644 --- a/src/plugins/unified_field_list/server/routes/field_stats.ts +++ b/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts @@ -6,145 +6,111 @@ * Side Public License, v 1. */ -import { errors } from '@elastic/elasticsearch'; +import { lastValueFrom } from 'rxjs'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import DateMath from '@kbn/datemath'; -import { schema } from '@kbn/config-schema'; -import { CoreSetup } from '@kbn/core/server'; -import type { DataViewField } from '@kbn/data-views-plugin/common'; -import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; +import type { DataViewField, DataView } from '@kbn/data-views-plugin/common'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; -import { FIELD_STATS_API_PATH } from '../../common/constants'; -import type { FieldStatsResponse } from '../../common/types'; -import type { PluginStart } from '../types'; +import type { BoolQuery } from '@kbn/es-query'; +import type { FieldStatsResponse } from '../../types'; const SHARD_SIZE = 5000; +const DEFAULT_TOP_VALUES_SIZE = 10; + +interface FetchFieldStatsParams { + data: DataPublicPluginStart; + dataView: DataView; + field: DataViewField; + fromDate: string; + toDate: string; + dslQuery: { bool: BoolQuery } | {}; + size?: number; +} -export async function initFieldStatsRoute(setup: CoreSetup) { - const router = setup.http.createRouter(); - router.post( - { - path: FIELD_STATS_API_PATH, - validate: { - body: schema.object( +export const fetchFieldStats = async ({ + data, + dataView, + field, + fromDate, + toDate, + dslQuery, + size, +}: FetchFieldStatsParams): Promise> => { + try { + const timeFieldName = dataView.timeFieldName; + const filter = timeFieldName + ? [ { - dslQuery: schema.object({}, { unknowns: 'allow' }), - fromDate: schema.string(), - toDate: schema.string(), - dataViewId: schema.string(), - fieldName: schema.string(), - size: schema.maybe(schema.number()), - }, - { unknowns: 'allow' } - ), - }, - }, - async (context, req, res) => { - const requestClient = (await context.core).elasticsearch.client.asCurrentUser; - const { fromDate, toDate, fieldName, dslQuery, size, dataViewId } = req.body; - - const [{ savedObjects, elasticsearch }, { dataViews }] = await setup.getStartServices(); - const savedObjectsClient = savedObjects.getScopedClient(req); - const esClient = elasticsearch.client.asScoped(req).asCurrentUser; - const indexPatternsService = await dataViews.dataViewsServiceFactory( - savedObjectsClient, - esClient - ); - - try { - const indexPattern = await indexPatternsService.get(dataViewId); - - const timeFieldName = indexPattern.timeFieldName; - const field = indexPattern.fields.find((f) => f.name === fieldName); - - if (!field) { - throw new Error(`Field {fieldName} not found in data view ${indexPattern.title}`); - } - - const filter = timeFieldName - ? [ - { - range: { - [timeFieldName]: { - gte: fromDate, - lte: toDate, - }, - }, + range: { + [timeFieldName]: { + gte: fromDate, + lte: toDate, }, - dslQuery, - ] - : [dslQuery]; - - const query = { - bool: { - filter, + }, }, - }; + dslQuery, + ] + : [dslQuery]; - const runtimeMappings = indexPattern.getRuntimeMappings(); + const query = { + bool: { + filter, + }, + }; - const search = async (aggs: Record) => { - const result = await requestClient.search({ - index: indexPattern.title, - track_total_hits: true, + const runtimeMappings = dataView.getRuntimeMappings(); + + const search = async ( + aggs: Record + ): Promise> => { + const result = await lastValueFrom( + data.search.search({ + params: { + index: dataView.title, body: { query, aggs, runtime_mappings: runtimeMappings, }, + track_total_hits: true, size: 0, - }); - return result; - }; + }, + }) + ); + return result.rawResponse; + }; - if (field.type.includes('range')) { - return res.ok({ body: {} }); - } - - if (field.type === 'histogram') { - return res.ok({ - body: await getNumberHistogram(search, field, false), - }); - } else if (field.type === 'number') { - return res.ok({ - body: await getNumberHistogram(search, field), - }); - } else if (field.type === 'date') { - return res.ok({ - body: await getDateHistogram(search, field, { fromDate, toDate }), - }); - } - - return res.ok({ - body: await getStringSamples(search, field, size), - }); - } catch (e) { - if (e instanceof SavedObjectNotFound) { - return res.notFound(); - } - if (e instanceof errors.ResponseError && e.statusCode === 404) { - return res.notFound(); - } - if (e.isBoom) { - if (e.output.statusCode === 404) { - return res.notFound(); - } - throw new Error(e.output.message); - } else { - throw e; - } - } + if (field.type.includes('range')) { + return {}; + } + + if (field.type === 'histogram') { + return await getNumberHistogram(search, field, false); } - ); -} + + if (field.type === 'number') { + return await getNumberHistogram(search, field); + } + + if (field.type === 'date') { + return await getDateHistogram(search, field, { fromDate, toDate }); + } + + return await getStringSamples(search, field, size); + } catch (error) { + // console.error(error); + throw new Error('Could not aggregate data'); + } +}; export async function getNumberHistogram( aggSearchWithBody: ( aggs: Record - ) => Promise, + ) => Promise>, field: DataViewField, useTopHits = true -): Promise { +): Promise> { const fieldRef = getFieldRef(field); const baseAggs = { @@ -168,7 +134,7 @@ export async function getNumberHistogram( aggs: { ...baseAggs, top_values: { - terms: { ...fieldRef, size: 10 }, + terms: { ...fieldRef, size: DEFAULT_TOP_VALUES_SIZE }, }, }, }, @@ -177,8 +143,8 @@ export async function getNumberHistogram( const minMaxResult = (await aggSearchWithBody( useTopHits ? searchWithHits : searchWithoutHits )) as - | ESSearchResponse - | ESSearchResponse; + | ESSearchResponse + | ESSearchResponse; const minValue = minMaxResult.aggregations!.sample.min_value.value; const maxValue = minMaxResult.aggregations!.sample.max_value.value; @@ -204,7 +170,7 @@ export async function getNumberHistogram( if (histogramInterval === 0) { return { - totalDocuments: minMaxResult.hits.total.value, + totalDocuments: getHitsTotal(minMaxResult), sampledValues: minMaxResult.aggregations!.sample.sample_count.value!, sampledDocuments: minMaxResult.aggregations!.sample.doc_count, topValues: topValuesBuckets, @@ -212,7 +178,7 @@ export async function getNumberHistogram( ? { buckets: [] } : { // Insert a fake bucket for a single-value histogram - buckets: [{ count: minMaxResult.aggregations!.sample.doc_count, key: minValue }], + buckets: [{ count: minMaxResult.aggregations!.sample.doc_count, key: minValue! }], }, }; } @@ -236,7 +202,7 @@ export async function getNumberHistogram( >; return { - totalDocuments: minMaxResult.hits.total.value, + totalDocuments: getHitsTotal(minMaxResult), sampledDocuments: minMaxResult.aggregations!.sample.doc_count, sampledValues: minMaxResult.aggregations!.sample.sample_count.value!, histogram: { @@ -252,8 +218,8 @@ export async function getNumberHistogram( export async function getStringSamples( aggSearchWithBody: (aggs: Record) => unknown, field: DataViewField, - size = 10 -): Promise { + size = DEFAULT_TOP_VALUES_SIZE +): Promise> { const fieldRef = getFieldRef(field); const topValuesBody = { @@ -276,7 +242,7 @@ export async function getStringSamples( >; return { - totalDocuments: topValuesResult.hits.total.value, + totalDocuments: getHitsTotal(topValuesResult), sampledDocuments: topValuesResult.aggregations!.sample.doc_count, sampledValues: topValuesResult.aggregations!.sample.sample_count.value!, topValues: { @@ -293,7 +259,7 @@ export async function getDateHistogram( aggSearchWithBody: (aggs: Record) => unknown, field: DataViewField, range: { fromDate: string; toDate: string } -): Promise { +): Promise> { const fromDate = DateMath.parse(range.fromDate); const toDate = DateMath.parse(range.toDate); if (!fromDate) { @@ -323,7 +289,7 @@ export async function getDateHistogram( >; return { - totalDocuments: results.hits.total.value, + totalDocuments: getHitsTotal(results), histogram: { buckets: results.aggregations!.histo.buckets.map((bucket) => ({ count: bucket.doc_count, @@ -343,3 +309,7 @@ function getFieldRef(field: DataViewField) { } : { field: field.name }; } + +const getHitsTotal = (body: estypes.SearchResponse): number => { + return (body.hits.total as estypes.SearchTotalHits).value ?? body.hits.total ?? 0; +}; diff --git a/src/plugins/unified_field_list/server/routes/index.ts b/src/plugins/unified_field_list/common/services/field_stats/index.ts similarity index 60% rename from src/plugins/unified_field_list/server/routes/index.ts rename to src/plugins/unified_field_list/common/services/field_stats/index.ts index 3558e93e4a780..7065fac3dea30 100755 --- a/src/plugins/unified_field_list/server/routes/index.ts +++ b/src/plugins/unified_field_list/common/services/field_stats/index.ts @@ -6,10 +6,4 @@ * Side Public License, v 1. */ -import { CoreSetup } from '@kbn/core/server'; -import { PluginStart } from '../types'; -import { initFieldStatsRoute } from './field_stats'; - -export function defineRoutes(setup: CoreSetup) { - initFieldStatsRoute(setup); -} +export { fetchFieldStats } from './field_stats'; diff --git a/src/plugins/unified_field_list/kibana.json b/src/plugins/unified_field_list/kibana.json index e2fd80fe79ad7..58a9212c90e39 100755 --- a/src/plugins/unified_field_list/kibana.json +++ b/src/plugins/unified_field_list/kibana.json @@ -7,7 +7,7 @@ "githubTeam": "kibana-data-discovery" }, "description": "Contains functionality for the field list which can be integrated into apps sidebar", - "server": true, + "server": false, "ui": true, "requiredPlugins": ["dataViews", "data", "fieldFormats", "charts"], "optionalPlugins": [], diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index f70f943447dda..367b9b84782e8 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -38,8 +38,8 @@ import { } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { buildEsQuery, Query, Filter } from '@kbn/es-query'; -import type { BucketedAggregation, FieldStatsResponse } from '../../../common/types'; -import { FIELD_STATS_API_PATH } from '../../../common/constants'; +import type { BucketedAggregation } from '../../../common/types'; +import { fetchFieldStats } from '../../../common/services/field_stats'; import { useUnifiedFieldListServices } from '../../hooks/use_unified_field_list_services'; import './field_stats.scss'; @@ -79,7 +79,7 @@ export const FieldStats: React.FC = ({ overrideContent, }) => { const services = useUnifiedFieldListServices(); - const { http, fieldFormats, uiSettings, charts, dataViews } = services; + const { fieldFormats, uiSettings, charts, dataViews, data } = services; const [state, setState] = useState({ isLoading: false, }); @@ -104,33 +104,31 @@ export const FieldStats: React.FC = ({ return; } - setState((s) => ({ ...s, isLoading: true })); - - http - .post>(FIELD_STATS_API_PATH, { - body: JSON.stringify({ - dslQuery: buildEsQuery(loadedDataView, query, filters, getEsQueryConfig(uiSettings)), - fromDate, - toDate, - fieldName: field.name, - dataViewId: loadedDataView.id, - }), - }) - .then((results) => { - setState((s) => ({ - ...s, - isLoading: false, - totalDocuments: results.totalDocuments, - sampledDocuments: results.sampledDocuments, - sampledValues: results.sampledValues, - histogram: results.histogram, - topValues: results.topValues, - dataView: loadedDataView, - })); - }) - .catch(() => { - setState((s) => ({ ...s, isLoading: false })); + try { + setState((s) => ({ ...s, isLoading: true })); + + const results = await fetchFieldStats({ + data, + dataView: loadedDataView, + field, + fromDate, + toDate, + dslQuery: buildEsQuery(loadedDataView, query, filters, getEsQueryConfig(uiSettings)), + // TODO: pass abortSignal on unmount }); + + setState((s) => ({ + ...s, + isLoading: false, + totalDocuments: results.totalDocuments, + sampledDocuments: results.sampledDocuments, + sampledValues: results.sampledValues, + histogram: results.histogram, + topValues: results.topValues, + })); + } catch (e) { + setState((s) => ({ ...s, isLoading: false })); + } } useEffect(() => { diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts index 85055a48e0ebf..e6ebf55bf7e74 100755 --- a/src/plugins/unified_field_list/public/index.ts +++ b/src/plugins/unified_field_list/public/index.ts @@ -24,3 +24,5 @@ export function plugin() { return new UnifiedFieldListPlugin(); } export type { UnifiedFieldListPluginSetup, UnifiedFieldListPluginStart } from './types'; + +export { fetchFieldStats } from '../common/services/field_stats'; diff --git a/src/plugins/unified_field_list/server/index.ts b/src/plugins/unified_field_list/server/index.ts deleted file mode 100755 index 039ea0488b533..0000000000000 --- a/src/plugins/unified_field_list/server/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { PluginInitializerContext } from '@kbn/core/server'; -import { UnifiedFieldListPlugin } from './plugin'; - -// This exports static code and TypeScript types, -// as well as, Kibana Platform `plugin()` initializer. - -export function plugin(initializerContext: PluginInitializerContext) { - return new UnifiedFieldListPlugin(initializerContext); -} - -export type { - UnifiedFieldListServerPluginSetup, - UnifiedFieldListServerPluginStart, - PluginSetup, - PluginStart, -} from './types'; diff --git a/src/plugins/unified_field_list/server/plugin.ts b/src/plugins/unified_field_list/server/plugin.ts deleted file mode 100755 index 2853afcf3d6fd..0000000000000 --- a/src/plugins/unified_field_list/server/plugin.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; -import { - UnifiedFieldListServerPluginSetup, - UnifiedFieldListServerPluginStart, - PluginStart, - PluginSetup, -} from './types'; -import { defineRoutes } from './routes'; - -export class UnifiedFieldListPlugin - implements Plugin -{ - private readonly logger: Logger; - - constructor(initializerContext: PluginInitializerContext) { - this.logger = initializerContext.logger.get(); - } - - public setup(core: CoreSetup, plugins: PluginSetup) { - this.logger.debug('unifiedFieldList: Setup'); - - defineRoutes(core); - - return {}; - } - - public start(core: CoreStart, plugins: PluginStart) { - this.logger.debug('unifiedFieldList: Started'); - return {}; - } - - public stop() {} -} diff --git a/src/plugins/unified_field_list/server/types.ts b/src/plugins/unified_field_list/server/types.ts deleted file mode 100755 index 56cd69a01881e..0000000000000 --- a/src/plugins/unified_field_list/server/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface UnifiedFieldListServerPluginSetup {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface UnifiedFieldListServerPluginStart {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PluginSetup {} - -export interface PluginStart { - dataViews: DataViewsServerPluginStart; -} From 3e093436cf25faca350869b642abc25c924f43a4 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 4 Aug 2022 09:29:22 +0200 Subject: [PATCH 15/92] [UnifiedFieldList] Update types --- .../main/components/sidebar/discover_field.test.tsx | 1 + .../public/components/field_stats/field_stats.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx index 45129d171cb33..87f301c662df4 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx @@ -56,6 +56,7 @@ function getComponent({ onRemoveField: jest.fn(), showDetails, selected, + state: {}, }; const services = { history: () => ({ diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 367b9b84782e8..843ad78f4c42f 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -37,7 +37,7 @@ import { TooltipType, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { buildEsQuery, Query, Filter } from '@kbn/es-query'; +import { buildEsQuery, Query, Filter, AggregateQuery } from '@kbn/es-query'; import type { BucketedAggregation } from '../../../common/types'; import { fetchFieldStats } from '../../../common/services/field_stats'; import { useUnifiedFieldListServices } from '../../hooks/use_unified_field_list_services'; @@ -53,7 +53,7 @@ interface State { } export interface FieldStatsProps { - query: Query; + query: Query | AggregateQuery; filters: Filter[]; fromDate: string; toDate: string; From ffb95204dd6a96479f9212e1cf5a01adc905acd4 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 4 Aug 2022 18:54:59 +0200 Subject: [PATCH 16/92] [UnifiedFieldList] Update Lens tests --- .../unified_field_list/common/constants.ts | 10 - .../services/field_stats/field_stats.ts | 17 +- .../components/field_stats/field_stats.tsx | 36 +++- .../unified_field_list/public/index.ts | 1 - .../field_item.test.tsx | 189 +++++++++++------- .../indexpattern_datasource/field_item.tsx | 14 +- .../indexpattern_datasource/indexpattern.tsx | 3 +- .../operations/definitions/index.ts | 1 + .../definitions/terms/helpers.test.ts | 46 ++--- .../operations/definitions/terms/helpers.ts | 44 ++-- .../definitions/terms/terms.test.tsx | 48 ++--- .../operations/layer_helpers.test.ts | 18 +- .../operations/layer_helpers.ts | 6 +- 13 files changed, 251 insertions(+), 182 deletions(-) delete mode 100644 src/plugins/unified_field_list/common/constants.ts diff --git a/src/plugins/unified_field_list/common/constants.ts b/src/plugins/unified_field_list/common/constants.ts deleted file mode 100644 index ef8a01b2705ff..0000000000000 --- a/src/plugins/unified_field_list/common/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export const BASE_API_PATH = '/api/unified_field_list'; -export const FIELD_STATS_API_PATH = `${BASE_API_PATH}/field_stats`; diff --git a/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts b/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts index 16a1c807a208a..8ef4a6e2f889b 100644 --- a/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts +++ b/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts @@ -9,10 +9,10 @@ import { lastValueFrom } from 'rxjs'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import DateMath from '@kbn/datemath'; -import type { DataViewField, DataView } from '@kbn/data-views-plugin/common'; +import type { DataView } from '@kbn/data-views-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; -import type { BoolQuery } from '@kbn/es-query'; +import type { BoolQuery, DataViewFieldBase } from '@kbn/es-query'; import type { FieldStatsResponse } from '../../types'; const SHARD_SIZE = 5000; @@ -21,7 +21,7 @@ const DEFAULT_TOP_VALUES_SIZE = 10; interface FetchFieldStatsParams { data: DataPublicPluginStart; dataView: DataView; - field: DataViewField; + field: DataViewFieldBase; fromDate: string; toDate: string; dslQuery: { bool: BoolQuery } | {}; @@ -38,6 +38,9 @@ export const fetchFieldStats = async ({ size, }: FetchFieldStatsParams): Promise> => { try { + if (!dataView?.id || !field?.type) { + return {}; + } const timeFieldName = dataView.timeFieldName; const filter = timeFieldName ? [ @@ -108,7 +111,7 @@ export async function getNumberHistogram( aggSearchWithBody: ( aggs: Record ) => Promise>, - field: DataViewField, + field: DataViewFieldBase, useTopHits = true ): Promise> { const fieldRef = getFieldRef(field); @@ -217,7 +220,7 @@ export async function getNumberHistogram( export async function getStringSamples( aggSearchWithBody: (aggs: Record) => unknown, - field: DataViewField, + field: DataViewFieldBase, size = DEFAULT_TOP_VALUES_SIZE ): Promise> { const fieldRef = getFieldRef(field); @@ -257,7 +260,7 @@ export async function getStringSamples( // This one is not sampled so that it returns the full date range export async function getDateHistogram( aggSearchWithBody: (aggs: Record) => unknown, - field: DataViewField, + field: DataViewFieldBase, range: { fromDate: string; toDate: string } ): Promise> { const fromDate = DateMath.parse(range.fromDate); @@ -299,7 +302,7 @@ export async function getDateHistogram( }; } -function getFieldRef(field: DataViewField) { +function getFieldRef(field: DataViewFieldBase) { return field.scripted ? { script: { diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 843ad78f4c42f..7897240a7a334 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -66,9 +66,7 @@ export interface FieldStatsProps { ) => JSX.Element | null; } -// TODO: catch errors during rendering - -export const FieldStats: React.FC = ({ +const FieldStatsComponent: React.FC = ({ query, filters, fromDate, @@ -127,6 +125,7 @@ export const FieldStats: React.FC = ({ topValues: results.topValues, })); } catch (e) { + // console.error(e); setState((s) => ({ ...s, isLoading: false })); } } @@ -468,3 +467,34 @@ export const FieldStats: React.FC = ({ return null; }; + +class ErrorBoundary extends React.Component<{}, { hasError: boolean }> { + constructor(props: FieldStatsProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + return { hasError: true }; + } + + // componentDidCatch(error, errorInfo) { + // console.log(error, errorInfo); + // } + + render() { + if (this.state.hasError) { + return null; + } + + return this.props.children; + } +} + +export const FieldStats: React.FC = (props) => { + return ( + + + + ); +}; diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts index e6ebf55bf7e74..f5865ce66bd39 100755 --- a/src/plugins/unified_field_list/public/index.ts +++ b/src/plugins/unified_field_list/public/index.ts @@ -16,7 +16,6 @@ export type { } from '../common/types'; export type { FieldStatsProps, FieldStatsFromSampleProps } from './components/field_stats'; export { FieldStats, FieldStatsFromSample } from './components/field_stats'; -export { BASE_API_PATH, FIELD_STATS_API_PATH } from '../common/constants'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 9dc49f8681733..f3c0cbc6c333a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -18,19 +18,45 @@ import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { documentField } from './document_field'; import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { FIELD_STATS_API_PATH } from '@kbn/unified-field-list-plugin/public'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { fetchFieldStats } from '@kbn/unified-field-list-plugin/common/services/field_stats'; import { DOCUMENT_FIELD_NAME } from '../../common'; +jest.mock('@kbn/unified-field-list-plugin/common/services/field_stats', () => ({ + fetchFieldStats: jest.fn().mockResolvedValue({}), +})); + const chartsThemeService = chartPluginMock.createSetupContract().theme; -function clickField(wrapper: ReactWrapper, field: string) { - wrapper.find(`[data-test-subj="lnsFieldListPanelField-${field}"] button`).simulate('click'); -} +const clickField = async (wrapper: ReactWrapper, field: string) => { + await act(async () => { + wrapper.find(`[data-test-subj="lnsFieldListPanelField-${field}"] button`).simulate('click'); + }); +}; + +const mockedServices = { + data: dataPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), + charts: chartPluginMock.createSetupContract(), + uiSettings: coreMock.createStart().uiSettings, +}; + +const InnerFieldItemWrapper: React.FC = (props) => { + return ( + + + + ); +}; describe('IndexPattern Field Item', () => { let defaultProps: FieldItemProps; let indexPattern: IndexPattern; - let core: ReturnType; + let dataView: DataView; beforeEach(() => { indexPattern = { @@ -84,8 +110,6 @@ describe('IndexPattern Field Item', () => { ], } as IndexPattern; - core = coreMock.createSetup(); - core.http.post.mockClear(); defaultProps = { indexPattern, fieldFormats: { @@ -94,7 +118,7 @@ describe('IndexPattern Field Item', () => { convert: jest.fn((s: unknown) => JSON.stringify(s)), })), } as unknown as FieldFormatsStart, - core, + core: coreMock.createStart(), highlight: '', dateRange: { fromDate: 'now-7d', @@ -117,10 +141,19 @@ describe('IndexPattern Field Item', () => { hasSuggestionForField: () => false, uiActions: uiActionsPluginMock.createStartContract(), }; + + dataView = { + ...indexPattern, + getFormatterForField: defaultProps.fieldFormats.getDefaultInstance, + } as unknown as DataView; + + (mockedServices.dataViews.get as jest.Mock).mockImplementation(() => { + return Promise.resolve(dataView); + }); }); it('should display displayName of a field', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); // Using .toContain over .toEqual because this element includes text from // which can't be seen, but shows in the text content @@ -129,15 +162,12 @@ describe('IndexPattern Field Item', () => { ); }); - it('should render edit field button if callback is set', () => { - core.http.post.mockImplementation(() => { - return new Promise(() => {}); - }); + it('should render edit field button if callback is set', async () => { const editFieldSpy = jest.fn(); const wrapper = mountWithIntl( - + ); - clickField(wrapper, 'bytes'); + await clickField(wrapper, 'bytes'); wrapper.update(); const popoverContent = wrapper.find(EuiPopover).prop('children'); act(() => { @@ -149,20 +179,17 @@ describe('IndexPattern Field Item', () => { expect(editFieldSpy).toHaveBeenCalledWith('bytes'); }); - it('should not render edit field button for document field', () => { - core.http.post.mockImplementation(() => { - return new Promise(() => {}); - }); + it('should not render edit field button for document field', async () => { const editFieldSpy = jest.fn(); const wrapper = mountWithIntl( - ); - clickField(wrapper, documentField.name); + await clickField(wrapper, documentField.name); wrapper.update(); const popoverContent = wrapper.find(EuiPopover).prop('children'); expect( @@ -175,33 +202,34 @@ describe('IndexPattern Field Item', () => { it('should request field stats every time the button is clicked', async () => { let resolveFunction: (arg: unknown) => void; - core.http.post.mockImplementation(() => { + (fetchFieldStats as jest.Mock).mockImplementation(() => { return new Promise((resolve) => { resolveFunction = resolve; }); }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); - clickField(wrapper, 'bytes'); + await clickField(wrapper, 'bytes'); - expect(core.http.post).toHaveBeenCalledWith(FIELD_STATS_API_PATH, { - body: JSON.stringify({ - dslQuery: { - bool: { - must: [], - filter: [], - should: [], - must_not: [], - }, + expect(fetchFieldStats).toHaveBeenCalledWith({ + data: mockedServices.data, + dataView, + dslQuery: { + bool: { + must: [], + filter: [], + should: [], + must_not: [], }, - fromDate: 'now-7d', - toDate: 'now', - fieldName: 'bytes', - dataViewId: '1', - }), + }, + fromDate: 'now-7d', + toDate: 'now', + field: defaultProps.field, }); + await wrapper.update(); + expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); @@ -220,12 +248,15 @@ describe('IndexPattern Field Item', () => { }); }); - wrapper.update(); + await wrapper.update(); expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); - clickField(wrapper, 'bytes'); - expect(core.http.post).toHaveBeenCalledTimes(1); + await clickField(wrapper, 'bytes'); + + await wrapper.update(); + + expect(fetchFieldStats).toHaveBeenCalledTimes(1); act(() => { const closePopover = wrapper.find(EuiPopover).prop('closePopover'); @@ -233,6 +264,8 @@ describe('IndexPattern Field Item', () => { closePopover(); }); + await wrapper.update(); + expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(false); act(() => { @@ -250,50 +283,58 @@ describe('IndexPattern Field Item', () => { }); }); - clickField(wrapper, 'bytes'); - - expect(core.http.post).toHaveBeenCalledTimes(2); - expect(core.http.post).toHaveBeenLastCalledWith(FIELD_STATS_API_PATH, { - body: JSON.stringify({ - dslQuery: { - bool: { - must: [], - filter: [ - { - bool: { - should: [{ match_phrase: { 'geo.src': 'US' } }], - minimum_should_match: 1, - }, + await clickField(wrapper, 'bytes'); + + await wrapper.update(); + + expect(fetchFieldStats).toHaveBeenCalledTimes(2); + expect(fetchFieldStats).toHaveBeenLastCalledWith({ + data: mockedServices.data, + dataView, + dslQuery: { + bool: { + must: [], + filter: [ + { + bool: { + should: [{ match_phrase: { 'geo.src': 'US' } }], + minimum_should_match: 1, }, - { - match: { phrase: { 'geo.dest': 'US' } }, - }, - ], - should: [], - must_not: [], - }, + }, + { + match: { phrase: { 'geo.dest': 'US' } }, + }, + ], + should: [], + must_not: [], }, - fromDate: 'now-14d', - toDate: 'now-7d', - fieldName: 'bytes', - dataViewId: '1', - }), + }, + fromDate: 'now-14d', + toDate: 'now-7d', + field: defaultProps.field, }); + + (fetchFieldStats as jest.Mock).mockReset(); + (fetchFieldStats as jest.Mock).mockImplementation(() => Promise.resolve({})); }); it('should not request field stats for document field', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); - clickField(wrapper, DOCUMENT_FIELD_NAME); + await clickField(wrapper, DOCUMENT_FIELD_NAME); - expect(core.http.post).not.toHaveBeenCalled(); + await wrapper.update(); + + expect(fetchFieldStats).not.toHaveBeenCalled(); expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); }); it('should not request field stats for range fields', async () => { const wrapper = mountWithIntl( - { /> ); - await act(async () => { - clickField(wrapper, 'ip_range'); - }); + await clickField(wrapper, 'ip_range'); - expect(core.http.post).not.toHaveBeenCalled(); + expect(fetchFieldStats).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index be83286566651..75857f6d86048 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -208,12 +208,14 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { panelClassName="lnsFieldItem__fieldPanel" initialFocus=".lnsFieldItem__fieldPanel" > - + {infoIsOpen && ( + + )} ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index ed80b7896c214..9b9dd5e44dacc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -625,7 +625,8 @@ export function getIndexPatternDatasource({ state.indexPatterns[layer.indexPatternId], state, layerId, - core + core, + data ) ?? [] ).map((message) => ({ shortMessage: '', // Not displayed currently diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 2cbe6f5e0c827..ebd01fdb9e1cc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -322,6 +322,7 @@ interface BaseOperationDefinitionProps< fixAction?: { label: string; newState: ( + data: DataPublicPluginStart, core: CoreStart, frame: FrameDatasourceAPI, layerId: string diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.test.ts index 3998836074f6b..395a3e070e226 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.test.ts @@ -5,7 +5,8 @@ * 2.0. */ -import type { CoreStart } from '@kbn/core/public'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { coreMock as corePluginMock } from '@kbn/core/public/mocks'; import type { FrameDatasourceAPI } from '../../../../types'; import type { CountIndexPatternColumn } from '..'; import type { TermsIndexPatternColumn } from './types'; @@ -20,29 +21,24 @@ import { ReferenceBasedIndexPatternColumn } from '../column_types'; import type { PercentileRanksIndexPatternColumn } from '../percentile_ranks'; import { MULTI_KEY_VISUAL_SEPARATOR } from './constants'; -const indexPattern = createMockedIndexPattern(); - -const coreMock = { - uiSettings: { - get: () => undefined, - }, - http: { - post: jest.fn(() => - Promise.resolve({ - topValues: { - buckets: [ - { - key: 'A', - }, - { - key: 'B', - }, - ], +jest.mock('@kbn/unified-field-list-plugin/common/services/field_stats', () => ({ + fetchFieldStats: jest.fn().mockResolvedValue({ + topValues: { + buckets: [ + { + key: 'A', }, - }) - ), - }, -} as unknown as CoreStart; + { + key: 'B', + }, + ], + }, + }), +})); + +const indexPattern = createMockedIndexPattern(); +const dataMock = dataPluginMock.createStartContract(); +const coreMock = corePluginMock.createStart(); function getStringBasedOperationColumn( field = 'source', @@ -212,6 +208,7 @@ describe('getDisallowedTermsMessage()', () => { indexPattern )!.fixAction.newState; const newLayer = await fixAction( + dataMock, coreMock, { query: { language: 'kuery', query: 'a: b' }, @@ -259,6 +256,7 @@ describe('getDisallowedTermsMessage()', () => { indexPattern )!.fixAction.newState; const newLayer = await fixAction( + dataMock, coreMock, { query: { language: 'kuery', query: 'a: b' }, @@ -300,6 +298,7 @@ describe('getDisallowedTermsMessage()', () => { indexPattern )!.fixAction.newState; const newLayer = await fixAction( + dataMock, coreMock, { query: { language: 'kuery', query: 'a: b' }, @@ -340,6 +339,7 @@ describe('getDisallowedTermsMessage()', () => { indexPattern )!.fixAction.newState; const newLayer = await fixAction( + dataMock, coreMock, { query: { language: 'kuery', query: 'a: b' }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts index f047bdbe7ba56..9365d2b34db07 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import { uniq } from 'lodash'; import type { CoreStart } from '@kbn/core/public'; import { buildEsQuery } from '@kbn/es-query'; -import { getEsQueryConfig } from '@kbn/data-plugin/public'; -import { FieldStatsResponse, FIELD_STATS_API_PATH } from '@kbn/unified-field-list-plugin/public'; +import { getEsQueryConfig, DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { FieldStatsResponse, fetchFieldStats } from '@kbn/unified-field-list-plugin/public'; import { GenericIndexPatternColumn, operationDefinitionMap } from '..'; import { defaultLabel } from '../filters'; import { isReferenced } from '../../layer_helpers'; @@ -108,7 +108,12 @@ export function getDisallowedTermsMessage( label: i18n.translate('xpack.lens.indexPattern.termsWithMultipleShiftsFixActionLabel', { defaultMessage: 'Use filters', }), - newState: async (core: CoreStart, frame: FrameDatasourceAPI, layerId: string) => { + newState: async ( + data: DataPublicPluginStart, + core: CoreStart, + frame: FrameDatasourceAPI, + layerId: string + ) => { const currentColumn = layer.columns[columnId] as TermsIndexPatternColumn; const fieldNames = [ currentColumn.sourceField, @@ -132,24 +137,21 @@ export function getDisallowedTermsMessage( ); if (!activeDataFieldNameMatch || currentTerms.length === 0) { if (fieldNames.length === 1) { - const response: FieldStatsResponse = await core.http.post( - FIELD_STATS_API_PATH, - { - body: JSON.stringify({ - dataViewId: indexPattern.id, - fieldName: fieldNames[0], - dslQuery: buildEsQuery( - indexPattern, - frame.query, - frame.filters, - getEsQueryConfig(core.uiSettings) - ), - fromDate: frame.dateRange.fromDate, - toDate: frame.dateRange.toDate, - size: currentColumn.params.size, - }), - } - ); + const currentDataView = await data.dataViews.get(indexPattern.id); + const response: FieldStatsResponse = await fetchFieldStats({ + data, + dataView: currentDataView, + field: indexPattern.getFieldByName(fieldNames[0])!, + dslQuery: buildEsQuery( + indexPattern, + frame.query, + frame.filters, + getEsQueryConfig(core.uiSettings) + ), + fromDate: frame.dateRange.fromDate, + toDate: frame.dateRange.toDate, + size: currentColumn.params.size, + }); currentTerms = response.topValues?.buckets.map(({ key }) => String(key)) || []; } } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 424bdfd002522..0edf7c7793c0b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -9,12 +9,8 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow, mount } from 'enzyme'; import { EuiButtonGroup, EuiComboBox, EuiFieldNumber, EuiSelect, EuiSwitch } from '@elastic/eui'; -import type { - IUiSettingsClient, - SavedObjectsClientContract, - HttpSetup, - CoreStart, -} from '@kbn/core/public'; +import { coreMock as corePluginMock } from '@kbn/core/public/mocks'; +import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; @@ -37,6 +33,21 @@ import { ReferenceEditor } from '../../../dimension_panel/reference_editor'; import { cloneDeep } from 'lodash'; import { IncludeExcludeRow } from './include_exclude_options'; +jest.mock('@kbn/unified-field-list-plugin/common/services/field_stats', () => ({ + fetchFieldStats: jest.fn().mockResolvedValue({ + topValues: { + buckets: [ + { + key: 'A', + }, + { + key: 'B', + }, + ], + }, + }), +})); + // mocking random id generator function jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -2719,29 +2730,10 @@ describe('terms', () => { const fixAction = ( typeof errorMessage === 'object' ? errorMessage.fixAction!.newState : undefined )!; - const coreMock = { - uiSettings: { - get: () => undefined, - }, - http: { - post: jest.fn(() => - Promise.resolve({ - topValues: { - buckets: [ - { - key: 'A', - }, - { - key: 'B', - }, - ], - }, - }) - ), - }, - } as unknown as CoreStart; + const dataMock = dataPluginMock.createStartContract(); const newLayer = await fixAction( - coreMock, + dataMock, + corePluginMock.createStart(), { query: { language: 'kuery', query: 'a: b' }, filters: [], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index c89ec6ae02199..f596ff893848d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -38,6 +38,9 @@ import { } from './definitions'; import { TinymathAST } from '@kbn/tinymath'; import { CoreStart } from '@kbn/core/public'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; + +const dataMock = dataPluginMock.createStartContract(); jest.mock('.'); jest.mock('../../id_generator'); @@ -3006,7 +3009,8 @@ describe('state_helpers', () => { indexPattern, {}, '1', - {} + {}, + dataMock ); expect(mock).toHaveBeenCalled(); expect(errors).toHaveLength(1); @@ -3032,7 +3036,8 @@ describe('state_helpers', () => { indexPattern, {} as IndexPatternPrivateState, '1', - {} as CoreStart + {} as CoreStart, + dataMock ); expect(mock).toHaveBeenCalled(); expect(errors).toHaveLength(1); @@ -3067,7 +3072,8 @@ describe('state_helpers', () => { indexPattern, {} as IndexPatternPrivateState, '1', - {} as CoreStart + {} as CoreStart, + dataMock ); expect(notCalledMock).not.toHaveBeenCalled(); expect(mock).toHaveBeenCalledTimes(1); @@ -3103,7 +3109,8 @@ describe('state_helpers', () => { indexPattern, {} as IndexPatternPrivateState, '1', - {} as CoreStart + {} as CoreStart, + dataMock ); expect(savedRef).toHaveBeenCalled(); expect(incompleteRef).not.toHaveBeenCalled(); @@ -3132,7 +3139,8 @@ describe('state_helpers', () => { indexPattern, {} as IndexPatternPrivateState, '1', - {} as CoreStart + {} as CoreStart, + dataMock ); expect(mock).toHaveBeenCalledWith( { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index a1edd6132d22a..1d2339b765fe0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -9,6 +9,7 @@ import { partition, mapValues, pickBy, isArray } from 'lodash'; import { CoreStart } from '@kbn/core/public'; import type { Query } from '@kbn/es-query'; import memoizeOne from 'memoize-one'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { VisualizeEditorLayersContext } from '@kbn/visualizations-plugin/public'; import type { DatasourceFixAction, @@ -1375,7 +1376,8 @@ export function getErrorMessages( indexPattern: IndexPattern, state: IndexPatternPrivateState, layerId: string, - core: CoreStart + core: CoreStart, + data: DataPublicPluginStart ): | Array< | string @@ -1417,7 +1419,7 @@ export function getErrorMessages( ...state, layers: { ...state.layers, - [layerId]: await errorMessage.fixAction!.newState(core, frame, layerId), + [layerId]: await errorMessage.fixAction!.newState(data, core, frame, layerId), }, }), } From a53242d417ad166b8903a1647a26a7b83d3f13f2 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 10 Aug 2022 18:11:53 +0200 Subject: [PATCH 17/92] [UnifiedFieldList] Update Lens tests --- .../operations/definitions/terms/terms.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 0edf7c7793c0b..97289d5b4740d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -9,12 +9,12 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow, mount } from 'enzyme'; import { EuiButtonGroup, EuiComboBox, EuiFieldNumber, EuiSelect, EuiSwitch } from '@elastic/eui'; -import { coreMock as corePluginMock } from '@kbn/core/public/mocks'; import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { coreMock as corePluginMock } from '@kbn/core/public/mocks'; import { createMockedIndexPattern } from '../../../mocks'; import { ValuesInput } from './values_input'; import type { TermsIndexPatternColumn } from '.'; From 1c989a35b89b61ac645f3904920687e311c855d3 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 10 Aug 2022 18:16:56 +0200 Subject: [PATCH 18/92] [UnifiedFieldList] Before merging --- .../operations/definitions/terms/terms.test.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 97289d5b4740d..c68a89358ceac 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -9,7 +9,12 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow, mount } from 'enzyme'; import { EuiButtonGroup, EuiComboBox, EuiFieldNumber, EuiSelect, EuiSwitch } from '@elastic/eui'; -import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; +import type { + IUiSettingsClient, + SavedObjectsClientContract, + HttpSetup, + CoreStart, +} from '@kbn/core/public'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; From e10f286cbf940d6cf8a18f2f7c7cf9be4c1b8185 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 10 Aug 2022 18:18:13 +0200 Subject: [PATCH 19/92] [UnifiedFieldList] After merging --- .../operations/definitions/terms/terms.test.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index a2f030b267a3c..5f63c0e3a14e1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -9,12 +9,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow, mount } from 'enzyme'; import { EuiButtonGroup, EuiComboBox, EuiFieldNumber, EuiSelect, EuiSwitch } from '@elastic/eui'; -import type { - IUiSettingsClient, - SavedObjectsClientContract, - HttpSetup, - CoreStart, -} from '@kbn/core/public'; +import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; From 1f8144a87f1695a5a3546c20171d05dbfd44ddcd Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 11 Aug 2022 10:06:21 +0200 Subject: [PATCH 20/92] [UnifiedFieldList] Refactor localization keys --- .../components/field_stats/field_stats.tsx | 25 +++++++++---------- .../translations/translations/fr-FR.json | 22 ++++++++-------- .../translations/translations/ja-JP.json | 22 ++++++++-------- .../translations/translations/zh-CN.json | 22 ++++++++-------- 4 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 7897240a7a334..a440e8af60912 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -173,11 +173,10 @@ const FieldStatsComponent: React.FC = ({ let title = <>; if (field.type.includes('range')) { - // TODO: new localization keys return ( <> - {i18n.translate('xpack.lens.indexPattern.fieldStatsLimited', { + {i18n.translate('unifiedFieldList.fieldStats.notAvailableForRangeFieldDescription', { defaultMessage: `Summary information is not available for range type fields.`, })} @@ -189,7 +188,7 @@ const FieldStatsComponent: React.FC = ({ return ( <> - {i18n.translate('xpack.lens.indexPattern.fieldStatsMurmur3Limited', { + {i18n.translate('unifiedFieldList.fieldStats.notAvailableForMurmur3FieldDescription', { defaultMessage: `Summary information is not available for murmur3 fields.`, })} @@ -213,18 +212,18 @@ const FieldStatsComponent: React.FC = ({ = ({ title = (
- {i18n.translate('xpack.lens.indexPattern.fieldTimeDistributionLabel', { + {i18n.translate('unifiedFieldList.fieldStats.fieldTimeDistributionLabel', { defaultMessage: 'Time distribution', })}
@@ -250,7 +249,7 @@ const FieldStatsComponent: React.FC = ({ title = (
- {i18n.translate('xpack.lens.indexPattern.fieldTopValuesLabel', { + {i18n.translate('unifiedFieldList.fieldStats.topValuesLabel', { defaultMessage: 'Top values', })}
@@ -273,7 +272,7 @@ const FieldStatsComponent: React.FC = ({ {sampledDocuments && ( <> - {i18n.translate('xpack.lens.indexPattern.percentageOfLabel', { + {i18n.translate('unifiedFieldList.fieldStats.percentageOfLabel', { defaultMessage: '{percentage}% of', values: { percentage: Math.round((sampledDocuments / totalDocuments) * 100), @@ -286,7 +285,7 @@ const FieldStatsComponent: React.FC = ({ .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) .convert(totalDocuments)} {' '} - {i18n.translate('xpack.lens.indexPattern.ofDocumentsLabel', { + {i18n.translate('unifiedFieldList.fieldStats.ofDocumentsLabel', { defaultMessage: 'documents', })} @@ -298,7 +297,7 @@ const FieldStatsComponent: React.FC = ({ } if (histogram && histogram.buckets.length) { - const specId = i18n.translate('xpack.lens.indexPattern.fieldStatsCountLabel', { + const specId = i18n.translate('unifiedFieldList.fieldStats.countLabel', { defaultMessage: 'Count', }); @@ -397,7 +396,7 @@ const FieldStatsComponent: React.FC = ({ {formatted === '' ? ( - {i18n.translate('xpack.lens.indexPattern.fieldPanelEmptyStringValue', { + {i18n.translate('unifiedFieldList.fieldStats.emptyStringValueLabel', { defaultMessage: 'Empty string', })} @@ -434,7 +433,7 @@ const FieldStatsComponent: React.FC = ({ - {i18n.translate('xpack.lens.indexPattern.otherDocsLabel', { + {i18n.translate('unifiedFieldList.fieldStats.otherDocsLabel', { defaultMessage: 'Other', })} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index bd439267fb8a8..53f06b23bc4e0 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -455,25 +455,25 @@ "xpack.lens.indexPattern.existenceErrorLabel": "Impossible de charger les informations de champ", "xpack.lens.indexPattern.existenceTimeoutAriaLabel": "La récupération de l'existence a expiré", "xpack.lens.indexPattern.existenceTimeoutLabel": "Les informations de champ ont pris trop de temps", - "xpack.lens.indexPattern.fieldDistributionLabel": "Distribution", + "unifiedFieldList.fieldStats.fieldDistributionLabel": "Distribution", "xpack.lens.indexPattern.fieldItem.visualizeGeoFieldLinkText": "Visualiser dans Maps", "xpack.lens.indexPattern.fieldItemTooltip": "Effectuez un glisser-déposer pour visualiser.", "xpack.lens.indexPattern.fieldNoOperation": "Le champ {field} ne peut pas être utilisé sans opération", - "xpack.lens.indexPattern.fieldPanelEmptyStringValue": "Chaîne vide", + "unifiedFieldList.fieldStats.emptyStringValueLabel": "Chaîne vide", "xpack.lens.indexPattern.fieldPlaceholder": "Champ", "xpack.lens.indexPattern.fieldsNotFound": "{count, plural, one {Champ} other {Champs}} {missingFields} {count, plural, one {introuvable} other {introuvables}}", "xpack.lens.indexPattern.fieldStatsButtonAriaLabel": "Prévisualiser {fieldName} : {fieldType}", "xpack.lens.indexPattern.fieldStatsButtonEmptyLabel": "Ce champ ne comporte aucune donnée mais vous pouvez toujours effectuer un glisser-déposer pour visualiser.", "xpack.lens.indexPattern.fieldStatsButtonLabel": "Cliquez pour obtenir un aperçu du champ, ou effectuez un glisser-déposer pour visualiser.", - "xpack.lens.indexPattern.fieldStatsCountLabel": "Compte", - "xpack.lens.indexPattern.fieldStatsDisplayToggle": "Basculer soit", - "xpack.lens.indexPattern.fieldStatsLimited": "Le résumé des informations n'est pas disponible pour les champs de type de gamme.", - "xpack.lens.indexPattern.fieldStatsMurmur3Limited": "Le résumé des informations n'est pas disponible pour les champs murmur3.", + "unifiedFieldList.fieldStats.countLabel": "Compte", + "unifiedFieldList.fieldStats.displayToggleLegend": "Basculer soit", + "unifiedFieldList.fieldStats.notAvailableForRangeFieldDescription": "Le résumé des informations n'est pas disponible pour les champs de type de gamme.", + "unifiedFieldList.fieldStats.notAvailableForMurmur3FieldDescription": "Le résumé des informations n'est pas disponible pour les champs murmur3.", "xpack.lens.indexPattern.fieldStatsNoData": "Lens ne peut pas créer de visualisation avec ce champ, car il ne contient pas de données. Pour créer une visualisation, glissez-déposez un autre champ.", "xpack.lens.indexPattern.fieldStatsSamplingNoData": "Lens ne peut pas créer de visualisation avec ce champ, car il ne contient aucune donnée dans les 500 premiers documents correspondant aux filtres. Pour créer une visualisation, glissez-déposez un autre champ.", "xpack.lens.indexPattern.fieldsWrongType": "{count, plural, one {Champ} other {Champs}} {invalidFields} {count, plural, other {de type incorrect}}", - "xpack.lens.indexPattern.fieldTimeDistributionLabel": "Répartition du temps", - "xpack.lens.indexPattern.fieldTopValuesLabel": "Valeurs les plus élevées", + "unifiedFieldList.fieldStats.fieldTimeDistributionLabel": "Répartition du temps", + "unifiedFieldList.fieldStats.topValuesLabel": "Valeurs les plus élevées", "xpack.lens.indexPattern.filterBy.clickToEdit": "Cliquer pour modifier", "xpack.lens.indexPattern.filterBy.emptyFilterQuery": "(vide)", "xpack.lens.indexPattern.filterBy.label": "Filtrer par", @@ -567,9 +567,9 @@ "xpack.lens.indexPattern.noDataViewsLabel": "Aucune vue de données", "xpack.lens.indexPattern.noRealMetricError": "Un calque uniquement doté de valeurs statiques n’affichera pas de résultats ; utilisez au moins un indicateur dynamique.", "xpack.lens.indexPattern.numberFormatLabel": "Nombre", - "xpack.lens.indexPattern.ofDocumentsLabel": "documents", + "unifiedFieldList.fieldStats.ofDocumentsLabel": "documents", "xpack.lens.indexPattern.operationsNotFound": "{operationLength, plural, one {Opération} other {Opérations}} {operationsList} non trouvée(s)", - "xpack.lens.indexPattern.otherDocsLabel": "Autre", + "unifiedFieldList.fieldStats.otherDocsLabel": "Autre", "xpack.lens.indexPattern.overall_metric": "indicateur : nombre", "xpack.lens.indexPattern.overallAverageOf": "Moyenne générale de {name}", "xpack.lens.indexPattern.overallMax": "Max général", @@ -578,7 +578,7 @@ "xpack.lens.indexPattern.overallMinOf": "Min général de {name}", "xpack.lens.indexPattern.overallSum": "Somme générale", "xpack.lens.indexPattern.overallSumOf": "Somme générale de {name}", - "xpack.lens.indexPattern.percentageOfLabel": "{percentage} % de", + "unifiedFieldList.fieldStats.percentageOfLabel": "{percentage} % de", "xpack.lens.indexPattern.percentFormatLabel": "Pour cent", "xpack.lens.indexPattern.percentile": "Centile", "xpack.lens.indexPattern.percentile.errorMessage": "Le centile doit être un entier compris entre 1 et 99", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1ed18a64287a6..61f1fdd59e708 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -456,25 +456,25 @@ "xpack.lens.indexPattern.existenceErrorLabel": "フィールド情報を読み込めません", "xpack.lens.indexPattern.existenceTimeoutAriaLabel": "存在の取り込みがタイムアウトしました", "xpack.lens.indexPattern.existenceTimeoutLabel": "フィールド情報に時間がかかりすぎました", - "xpack.lens.indexPattern.fieldDistributionLabel": "分布", + "unifiedFieldList.fieldStats.fieldDistributionLabel": "分布", "xpack.lens.indexPattern.fieldItem.visualizeGeoFieldLinkText": "Mapsで可視化", "xpack.lens.indexPattern.fieldItemTooltip": "可視化するには、ドラッグアンドドロップします。", "xpack.lens.indexPattern.fieldNoOperation": "フィールド{field}は演算なしで使用できません", - "xpack.lens.indexPattern.fieldPanelEmptyStringValue": "空の文字列", + "unifiedFieldList.fieldStats.emptyStringValueLabel": "空の文字列", "xpack.lens.indexPattern.fieldPlaceholder": "フィールド", "xpack.lens.indexPattern.fieldsNotFound": "{count, plural, other {個のフィールド}} {missingFields} {count, plural, other {が}}見つかりません", "xpack.lens.indexPattern.fieldStatsButtonAriaLabel": "プレビュー{fieldName}:{fieldType}", "xpack.lens.indexPattern.fieldStatsButtonEmptyLabel": "このフィールドにはデータがありませんが、ドラッグアンドドロップで可視化できます。", "xpack.lens.indexPattern.fieldStatsButtonLabel": "フィールドプレビューを表示するには、クリックします。可視化するには、ドラッグアンドドロップします。", - "xpack.lens.indexPattern.fieldStatsCountLabel": "カウント", - "xpack.lens.indexPattern.fieldStatsDisplayToggle": "次のどちらかを切り替えます:", - "xpack.lens.indexPattern.fieldStatsLimited": "範囲タイプフィールドの概要情報がありません。", - "xpack.lens.indexPattern.fieldStatsMurmur3Limited": "murmur3フィールドの概要情報がありません。", + "unifiedFieldList.fieldStats.countLabel": "カウント", + "unifiedFieldList.fieldStats.displayToggleLegend": "次のどちらかを切り替えます:", + "unifiedFieldList.fieldStats.notAvailableForRangeFieldDescription": "範囲タイプフィールドの概要情報がありません。", + "unifiedFieldList.fieldStats.notAvailableForMurmur3FieldDescription": "murmur3フィールドの概要情報がありません。", "xpack.lens.indexPattern.fieldStatsNoData": "Lensはこのフィールドのビジュアライゼーションを作成できません。フィールドにデータがありません。ビジュアライゼーションを作成するには、別のフィールドをドラッグします。", "xpack.lens.indexPattern.fieldStatsSamplingNoData": "Lensはこのフィールドのビジュアライゼーションを作成できません。フィルターと一致する最初の500件のドキュメントではフィールドにデータがありません。ビジュアライゼーションを作成するには、別のフィールドをドラッグします。", "xpack.lens.indexPattern.fieldsWrongType": "{count, plural, other {個のフィールド}} {invalidFields} {count, plural, other {の}}型が正しくありません", - "xpack.lens.indexPattern.fieldTimeDistributionLabel": "時間分布", - "xpack.lens.indexPattern.fieldTopValuesLabel": "トップの値", + "unifiedFieldList.fieldStats.fieldTimeDistributionLabel": "時間分布", + "unifiedFieldList.fieldStats.topValuesLabel": "トップの値", "xpack.lens.indexPattern.filterBy.clickToEdit": "クリックして編集", "xpack.lens.indexPattern.filterBy.emptyFilterQuery": "(空)", "xpack.lens.indexPattern.filterBy.label": "フィルタリング条件", @@ -568,9 +568,9 @@ "xpack.lens.indexPattern.noDataViewsLabel": "データビューがありません", "xpack.lens.indexPattern.noRealMetricError": "静的値のみのレイヤーには結果が表示されません。1つ以上の動的メトリックを使用してください", "xpack.lens.indexPattern.numberFormatLabel": "数字", - "xpack.lens.indexPattern.ofDocumentsLabel": "ドキュメント", + "unifiedFieldList.fieldStats.ofDocumentsLabel": "ドキュメント", "xpack.lens.indexPattern.operationsNotFound": "{operationLength, plural, other {個の演算}} {operationsList}が見つかりました", - "xpack.lens.indexPattern.otherDocsLabel": "その他", + "unifiedFieldList.fieldStats.otherDocsLabel": "その他", "xpack.lens.indexPattern.overall_metric": "メトリック:数値", "xpack.lens.indexPattern.overallAverageOf": "{name}の全体平均値", "xpack.lens.indexPattern.overallMax": "全体最高", @@ -579,7 +579,7 @@ "xpack.lens.indexPattern.overallMinOf": "{name}の全体最小値", "xpack.lens.indexPattern.overallSum": "全体合計", "xpack.lens.indexPattern.overallSumOf": "{name}の全体平方和", - "xpack.lens.indexPattern.percentageOfLabel": "{percentage}% の", + "unifiedFieldList.fieldStats.percentageOfLabel": "{percentage}% の", "xpack.lens.indexPattern.percentFormatLabel": "割合(%)", "xpack.lens.indexPattern.percentile": "パーセンタイル", "xpack.lens.indexPattern.percentile.errorMessage": "パーセンタイルは1~99の範囲の整数でなければなりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 69af29fea4b0a..b3988910914d8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -456,25 +456,25 @@ "xpack.lens.indexPattern.existenceErrorLabel": "无法加载字段信息", "xpack.lens.indexPattern.existenceTimeoutAriaLabel": "现有内容提取超时", "xpack.lens.indexPattern.existenceTimeoutLabel": "字段信息花费时间过久", - "xpack.lens.indexPattern.fieldDistributionLabel": "分布", + "unifiedFieldList.fieldStats.fieldDistributionLabel": "分布", "xpack.lens.indexPattern.fieldItem.visualizeGeoFieldLinkText": "在 Maps 中可视化", "xpack.lens.indexPattern.fieldItemTooltip": "拖放以可视化。", "xpack.lens.indexPattern.fieldNoOperation": "没有运算,无法使用字段 {field}", - "xpack.lens.indexPattern.fieldPanelEmptyStringValue": "空字符串", + "unifiedFieldList.fieldStats.emptyStringValueLabel": "空字符串", "xpack.lens.indexPattern.fieldPlaceholder": "字段", "xpack.lens.indexPattern.fieldsNotFound": "找不到{count, plural, other {字段}} {missingFields} {count, plural, other {}}", "xpack.lens.indexPattern.fieldStatsButtonAriaLabel": "预览 {fieldName}:{fieldType}", "xpack.lens.indexPattern.fieldStatsButtonEmptyLabel": "此字段不包含任何数据,但您仍然可以拖放以进行可视化。", "xpack.lens.indexPattern.fieldStatsButtonLabel": "单击以进行字段预览,或拖放以进行可视化。", - "xpack.lens.indexPattern.fieldStatsCountLabel": "计数", - "xpack.lens.indexPattern.fieldStatsDisplayToggle": "切换", - "xpack.lens.indexPattern.fieldStatsLimited": "摘要信息不适用于范围类型字段。", - "xpack.lens.indexPattern.fieldStatsMurmur3Limited": "摘要信息不适用于 murmur3 字段。", + "unifiedFieldList.fieldStats.countLabel": "计数", + "unifiedFieldList.fieldStats.displayToggleLegend": "切换", + "unifiedFieldList.fieldStats.notAvailableForRangeFieldDescription": "摘要信息不适用于范围类型字段。", + "unifiedFieldList.fieldStats.notAvailableForMurmur3FieldDescription": "摘要信息不适用于 murmur3 字段。", "xpack.lens.indexPattern.fieldStatsNoData": "Lens 无法使用此字段创建可视化,因为其中未包含数据。要创建可视化,请拖放其他字段。", "xpack.lens.indexPattern.fieldStatsSamplingNoData": "Lens 无法使用此字段创建可视化,因为其中未包含与您的筛选匹配的前 500 个文档中的数据。要创建可视化,请拖放其他字段。", "xpack.lens.indexPattern.fieldsWrongType": "{count, plural, other {字段}} {invalidFields} 的类型不正确", - "xpack.lens.indexPattern.fieldTimeDistributionLabel": "时间分布", - "xpack.lens.indexPattern.fieldTopValuesLabel": "排名最前值", + "unifiedFieldList.fieldStats.fieldTimeDistributionLabel": "时间分布", + "unifiedFieldList.fieldStats.topValuesLabel": "排名最前值", "xpack.lens.indexPattern.filterBy.clickToEdit": "单击以编辑", "xpack.lens.indexPattern.filterBy.emptyFilterQuery": "(空)", "xpack.lens.indexPattern.filterBy.label": "筛选依据", @@ -568,9 +568,9 @@ "xpack.lens.indexPattern.noDataViewsLabel": "无数据视图", "xpack.lens.indexPattern.noRealMetricError": "仅包含静态值的图层将不显示结果,请至少使用一个动态指标", "xpack.lens.indexPattern.numberFormatLabel": "数字", - "xpack.lens.indexPattern.ofDocumentsLabel": "文档", + "unifiedFieldList.fieldStats.ofDocumentsLabel": "文档", "xpack.lens.indexPattern.operationsNotFound": "未找到{operationLength, plural, other {运算}} {operationsList}", - "xpack.lens.indexPattern.otherDocsLabel": "其他", + "unifiedFieldList.fieldStats.otherDocsLabel": "其他", "xpack.lens.indexPattern.overall_metric": "指标:数字", "xpack.lens.indexPattern.overallAverageOf": "{name} 的总体平均值", "xpack.lens.indexPattern.overallMax": "总体最大值", @@ -579,7 +579,7 @@ "xpack.lens.indexPattern.overallMinOf": "{name} 的总体最小值", "xpack.lens.indexPattern.overallSum": "总和", "xpack.lens.indexPattern.overallSumOf": "{name} 的总和", - "xpack.lens.indexPattern.percentageOfLabel": "{percentage}% 的", + "unifiedFieldList.fieldStats.percentageOfLabel": "{percentage}% 的", "xpack.lens.indexPattern.percentFormatLabel": "百分比", "xpack.lens.indexPattern.percentile": "百分位数", "xpack.lens.indexPattern.percentile.errorMessage": "百分位数必须是介于 1 到 99 之间的整数", From 5c9379b768b0bfb69ce6a4caef20ecef65fa60fb Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 11 Aug 2022 10:12:06 +0200 Subject: [PATCH 21/92] [UnifiedFieldList] Update types --- .../indexpattern_datasource/operations/definitions/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 5802923ff5553..bd2c8b2199c90 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -552,6 +552,7 @@ interface FieldBasedOperationDefinition Date: Thu, 11 Aug 2022 13:53:36 +0200 Subject: [PATCH 22/92] [UnifiedFieldList] Reintroduce server API for field stats and refactor integration tests --- .../unified_field_list/common/constants.ts | 10 + .../services/field_stats/field_stats.ts | 288 ++-------------- .../services/field_stats/field_stats_utils.ts | 309 ++++++++++++++++++ src/plugins/unified_field_list/kibana.json | 2 +- .../unified_field_list/server/index.ts | 24 ++ .../unified_field_list/server/plugin.ts | 41 +++ .../server/routes/field_stats.ts | 104 ++++++ .../unified_field_list/server/routes/index.ts | 15 + .../unified_field_list/server/types.ts | 22 ++ x-pack/test/api_integration/apis/index.ts | 1 + .../test/api_integration/apis/lens/index.ts | 1 - .../field_stats.ts | 43 ++- .../apis/unified_field_list/index.ts | 14 + 13 files changed, 588 insertions(+), 286 deletions(-) create mode 100644 src/plugins/unified_field_list/common/constants.ts create mode 100644 src/plugins/unified_field_list/common/services/field_stats/field_stats_utils.ts create mode 100644 src/plugins/unified_field_list/server/index.ts create mode 100644 src/plugins/unified_field_list/server/plugin.ts create mode 100644 src/plugins/unified_field_list/server/routes/field_stats.ts create mode 100644 src/plugins/unified_field_list/server/routes/index.ts create mode 100644 src/plugins/unified_field_list/server/types.ts rename x-pack/test/api_integration/apis/{lens => unified_field_list}/field_stats.ts (93%) create mode 100644 x-pack/test/api_integration/apis/unified_field_list/index.ts diff --git a/src/plugins/unified_field_list/common/constants.ts b/src/plugins/unified_field_list/common/constants.ts new file mode 100644 index 0000000000000..ef8a01b2705ff --- /dev/null +++ b/src/plugins/unified_field_list/common/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const BASE_API_PATH = '/api/unified_field_list'; +export const FIELD_STATS_API_PATH = `${BASE_API_PATH}/field_stats`; diff --git a/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts b/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts index 8ef4a6e2f889b..26e91e523620d 100644 --- a/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts +++ b/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts @@ -7,16 +7,11 @@ */ import { lastValueFrom } from 'rxjs'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import DateMath from '@kbn/datemath'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; import type { BoolQuery, DataViewFieldBase } from '@kbn/es-query'; import type { FieldStatsResponse } from '../../types'; - -const SHARD_SIZE = 5000; -const DEFAULT_TOP_VALUES_SIZE = 10; +import { fetchAndCalculateFieldStats, SearchHandler, buildSearchParams } from './field_stats_utils'; interface FetchFieldStatsParams { data: DataPublicPluginStart; @@ -41,278 +36,33 @@ export const fetchFieldStats = async ({ if (!dataView?.id || !field?.type) { return {}; } - const timeFieldName = dataView.timeFieldName; - const filter = timeFieldName - ? [ - { - range: { - [timeFieldName]: { - gte: fromDate, - lte: toDate, - }, - }, - }, - dslQuery, - ] - : [dslQuery]; - - const query = { - bool: { - filter, - }, - }; - const runtimeMappings = dataView.getRuntimeMappings(); - - const search = async ( - aggs: Record - ): Promise> => { + const searchHandler: SearchHandler = async (aggs) => { const result = await lastValueFrom( data.search.search({ - params: { - index: dataView.title, - body: { - query, - aggs, - runtime_mappings: runtimeMappings, - }, - track_total_hits: true, - size: 0, - }, + params: buildSearchParams({ + dataViewPattern: dataView.title, + timeFieldName: dataView.timeFieldName, + fromDate, + toDate, + dslQuery, + runtimeMappings: dataView.getRuntimeMappings(), + aggs, + }), }) ); return result.rawResponse; }; - if (field.type.includes('range')) { - return {}; - } - - if (field.type === 'histogram') { - return await getNumberHistogram(search, field, false); - } - - if (field.type === 'number') { - return await getNumberHistogram(search, field); - } - - if (field.type === 'date') { - return await getDateHistogram(search, field, { fromDate, toDate }); - } - - return await getStringSamples(search, field, size); + return await fetchAndCalculateFieldStats({ + searchHandler, + field, + fromDate, + toDate, + size, + }); } catch (error) { // console.error(error); - throw new Error('Could not aggregate data'); + throw new Error('Could not provide field stats'); } }; - -export async function getNumberHistogram( - aggSearchWithBody: ( - aggs: Record - ) => Promise>, - field: DataViewFieldBase, - useTopHits = true -): Promise> { - const fieldRef = getFieldRef(field); - - const baseAggs = { - min_value: { - min: { field: field.name }, - }, - max_value: { - max: { field: field.name }, - }, - sample_count: { value_count: { ...fieldRef } }, - }; - const searchWithoutHits = { - sample: { - sampler: { shard_size: SHARD_SIZE }, - aggs: { ...baseAggs }, - }, - }; - const searchWithHits = { - sample: { - sampler: { shard_size: SHARD_SIZE }, - aggs: { - ...baseAggs, - top_values: { - terms: { ...fieldRef, size: DEFAULT_TOP_VALUES_SIZE }, - }, - }, - }, - }; - - const minMaxResult = (await aggSearchWithBody( - useTopHits ? searchWithHits : searchWithoutHits - )) as - | ESSearchResponse - | ESSearchResponse; - - const minValue = minMaxResult.aggregations!.sample.min_value.value; - const maxValue = minMaxResult.aggregations!.sample.max_value.value; - const terms = - 'top_values' in minMaxResult.aggregations!.sample - ? minMaxResult.aggregations!.sample.top_values - : { - buckets: [] as Array<{ doc_count: number; key: string | number }>, - }; - - const topValuesBuckets = { - buckets: terms.buckets.map((bucket) => ({ - count: bucket.doc_count, - key: bucket.key, - })), - }; - - let histogramInterval = (maxValue! - minValue!) / 10; - - if (Number.isInteger(minValue!) && Number.isInteger(maxValue!)) { - histogramInterval = Math.ceil(histogramInterval); - } - - if (histogramInterval === 0) { - return { - totalDocuments: getHitsTotal(minMaxResult), - sampledValues: minMaxResult.aggregations!.sample.sample_count.value!, - sampledDocuments: minMaxResult.aggregations!.sample.doc_count, - topValues: topValuesBuckets, - histogram: useTopHits - ? { buckets: [] } - : { - // Insert a fake bucket for a single-value histogram - buckets: [{ count: minMaxResult.aggregations!.sample.doc_count, key: minValue! }], - }, - }; - } - - const histogramBody = { - sample: { - sampler: { shard_size: SHARD_SIZE }, - aggs: { - histo: { - histogram: { - field: field.name, - interval: histogramInterval, - }, - }, - }, - }, - }; - const histogramResult = (await aggSearchWithBody(histogramBody)) as ESSearchResponse< - unknown, - { body: { aggs: typeof histogramBody } } - >; - - return { - totalDocuments: getHitsTotal(minMaxResult), - sampledDocuments: minMaxResult.aggregations!.sample.doc_count, - sampledValues: minMaxResult.aggregations!.sample.sample_count.value!, - histogram: { - buckets: histogramResult.aggregations!.sample.histo.buckets.map((bucket) => ({ - count: bucket.doc_count, - key: bucket.key, - })), - }, - topValues: topValuesBuckets, - }; -} - -export async function getStringSamples( - aggSearchWithBody: (aggs: Record) => unknown, - field: DataViewFieldBase, - size = DEFAULT_TOP_VALUES_SIZE -): Promise> { - const fieldRef = getFieldRef(field); - - const topValuesBody = { - sample: { - sampler: { shard_size: SHARD_SIZE }, - aggs: { - sample_count: { value_count: { ...fieldRef } }, - top_values: { - terms: { - ...fieldRef, - size, - }, - }, - }, - }, - }; - const topValuesResult = (await aggSearchWithBody(topValuesBody)) as ESSearchResponse< - unknown, - { body: { aggs: typeof topValuesBody } } - >; - - return { - totalDocuments: getHitsTotal(topValuesResult), - sampledDocuments: topValuesResult.aggregations!.sample.doc_count, - sampledValues: topValuesResult.aggregations!.sample.sample_count.value!, - topValues: { - buckets: topValuesResult.aggregations!.sample.top_values.buckets.map((bucket) => ({ - count: bucket.doc_count, - key: bucket.key, - })), - }, - }; -} - -// This one is not sampled so that it returns the full date range -export async function getDateHistogram( - aggSearchWithBody: (aggs: Record) => unknown, - field: DataViewFieldBase, - range: { fromDate: string; toDate: string } -): Promise> { - const fromDate = DateMath.parse(range.fromDate); - const toDate = DateMath.parse(range.toDate); - if (!fromDate) { - throw Error('Invalid fromDate value'); - } - if (!toDate) { - throw Error('Invalid toDate value'); - } - - const interval = Math.round((toDate.valueOf() - fromDate.valueOf()) / 10); - if (interval < 1) { - return { - totalDocuments: 0, - histogram: { buckets: [] }, - }; - } - - // TODO: Respect rollup intervals - const fixedInterval = `${interval}ms`; - - const histogramBody = { - histo: { date_histogram: { ...getFieldRef(field), fixed_interval: fixedInterval } }, - }; - const results = (await aggSearchWithBody(histogramBody)) as ESSearchResponse< - unknown, - { body: { aggs: typeof histogramBody } } - >; - - return { - totalDocuments: getHitsTotal(results), - histogram: { - buckets: results.aggregations!.histo.buckets.map((bucket) => ({ - count: bucket.doc_count, - key: bucket.key, - })), - }, - }; -} - -function getFieldRef(field: DataViewFieldBase) { - return field.scripted - ? { - script: { - lang: field.lang!, - source: field.script as string, - }, - } - : { field: field.name }; -} - -const getHitsTotal = (body: estypes.SearchResponse): number => { - return (body.hits.total as estypes.SearchTotalHits).value ?? body.hits.total ?? 0; -}; diff --git a/src/plugins/unified_field_list/common/services/field_stats/field_stats_utils.ts b/src/plugins/unified_field_list/common/services/field_stats/field_stats_utils.ts new file mode 100644 index 0000000000000..154790419434a --- /dev/null +++ b/src/plugins/unified_field_list/common/services/field_stats/field_stats_utils.ts @@ -0,0 +1,309 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import DateMath from '@kbn/datemath'; +import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; +import type { DataViewFieldBase, BoolQuery } from '@kbn/es-query'; +import type { FieldStatsResponse } from '../../types'; + +export type SearchHandler = ( + aggs: Record +) => Promise>; + +const SHARD_SIZE = 5000; +const DEFAULT_TOP_VALUES_SIZE = 10; + +export function buildSearchParams({ + dataViewPattern, + timeFieldName, + fromDate, + toDate, + dslQuery, + runtimeMappings, + aggs, +}: { + dataViewPattern: string; + timeFieldName?: string; + fromDate: string; + toDate: string; + dslQuery: { bool: BoolQuery } | {}; + runtimeMappings: estypes.MappingRuntimeFields; + aggs: Record; +}) { + const filter = timeFieldName + ? [ + { + range: { + [timeFieldName]: { + gte: fromDate, + lte: toDate, + }, + }, + }, + dslQuery, + ] + : [dslQuery]; + + const query = { + bool: { + filter, + }, + }; + + return { + index: dataViewPattern, + body: { + query, + aggs, + runtime_mappings: runtimeMappings, + }, + track_total_hits: true, + size: 0, + }; +} + +export async function fetchAndCalculateFieldStats({ + searchHandler, + field, + fromDate, + toDate, + size, +}: { + searchHandler: SearchHandler; + field: DataViewFieldBase; + fromDate: string; + toDate: string; + size?: number; +}) { + if (field.type.includes('range')) { + return {}; + } + + if (field.type === 'histogram') { + return await getNumberHistogram(searchHandler, field, false); + } + + if (field.type === 'number') { + return await getNumberHistogram(searchHandler, field); + } + + if (field.type === 'date') { + return await getDateHistogram(searchHandler, field, { fromDate, toDate }); + } + + return await getStringSamples(searchHandler, field, size); +} + +export async function getNumberHistogram( + aggSearchWithBody: SearchHandler, + field: DataViewFieldBase, + useTopHits = true +): Promise> { + const fieldRef = getFieldRef(field); + + const baseAggs = { + min_value: { + min: { field: field.name }, + }, + max_value: { + max: { field: field.name }, + }, + sample_count: { value_count: { ...fieldRef } }, + }; + const searchWithoutHits = { + sample: { + sampler: { shard_size: SHARD_SIZE }, + aggs: { ...baseAggs }, + }, + }; + const searchWithHits = { + sample: { + sampler: { shard_size: SHARD_SIZE }, + aggs: { + ...baseAggs, + top_values: { + terms: { ...fieldRef, size: DEFAULT_TOP_VALUES_SIZE }, + }, + }, + }, + }; + + const minMaxResult = (await aggSearchWithBody( + useTopHits ? searchWithHits : searchWithoutHits + )) as + | ESSearchResponse + | ESSearchResponse; + + const minValue = minMaxResult.aggregations!.sample.min_value.value; + const maxValue = minMaxResult.aggregations!.sample.max_value.value; + const terms = + 'top_values' in minMaxResult.aggregations!.sample + ? minMaxResult.aggregations!.sample.top_values + : { + buckets: [] as Array<{ doc_count: number; key: string | number }>, + }; + + const topValuesBuckets = { + buckets: terms.buckets.map((bucket) => ({ + count: bucket.doc_count, + key: bucket.key, + })), + }; + + let histogramInterval = (maxValue! - minValue!) / 10; + + if (Number.isInteger(minValue!) && Number.isInteger(maxValue!)) { + histogramInterval = Math.ceil(histogramInterval); + } + + if (histogramInterval === 0) { + return { + totalDocuments: getHitsTotal(minMaxResult), + sampledValues: minMaxResult.aggregations!.sample.sample_count.value!, + sampledDocuments: minMaxResult.aggregations!.sample.doc_count, + topValues: topValuesBuckets, + histogram: useTopHits + ? { buckets: [] } + : { + // Insert a fake bucket for a single-value histogram + buckets: [{ count: minMaxResult.aggregations!.sample.doc_count, key: minValue! }], + }, + }; + } + + const histogramBody = { + sample: { + sampler: { shard_size: SHARD_SIZE }, + aggs: { + histo: { + histogram: { + field: field.name, + interval: histogramInterval, + }, + }, + }, + }, + }; + const histogramResult = (await aggSearchWithBody(histogramBody)) as ESSearchResponse< + unknown, + { body: { aggs: typeof histogramBody } } + >; + + return { + totalDocuments: getHitsTotal(minMaxResult), + sampledDocuments: minMaxResult.aggregations!.sample.doc_count, + sampledValues: minMaxResult.aggregations!.sample.sample_count.value!, + histogram: { + buckets: histogramResult.aggregations!.sample.histo.buckets.map((bucket) => ({ + count: bucket.doc_count, + key: bucket.key, + })), + }, + topValues: topValuesBuckets, + }; +} + +export async function getStringSamples( + aggSearchWithBody: SearchHandler, + field: DataViewFieldBase, + size = DEFAULT_TOP_VALUES_SIZE +): Promise> { + const fieldRef = getFieldRef(field); + + const topValuesBody = { + sample: { + sampler: { shard_size: SHARD_SIZE }, + aggs: { + sample_count: { value_count: { ...fieldRef } }, + top_values: { + terms: { + ...fieldRef, + size, + }, + }, + }, + }, + }; + const topValuesResult = (await aggSearchWithBody(topValuesBody)) as ESSearchResponse< + unknown, + { body: { aggs: typeof topValuesBody } } + >; + + return { + totalDocuments: getHitsTotal(topValuesResult), + sampledDocuments: topValuesResult.aggregations!.sample.doc_count, + sampledValues: topValuesResult.aggregations!.sample.sample_count.value!, + topValues: { + buckets: topValuesResult.aggregations!.sample.top_values.buckets.map((bucket) => ({ + count: bucket.doc_count, + key: bucket.key, + })), + }, + }; +} + +// This one is not sampled so that it returns the full date range +export async function getDateHistogram( + aggSearchWithBody: SearchHandler, + field: DataViewFieldBase, + range: { fromDate: string; toDate: string } +): Promise> { + const fromDate = DateMath.parse(range.fromDate); + const toDate = DateMath.parse(range.toDate); + if (!fromDate) { + throw Error('Invalid fromDate value'); + } + if (!toDate) { + throw Error('Invalid toDate value'); + } + + const interval = Math.round((toDate.valueOf() - fromDate.valueOf()) / 10); + if (interval < 1) { + return { + totalDocuments: 0, + histogram: { buckets: [] }, + }; + } + + // TODO: Respect rollup intervals + const fixedInterval = `${interval}ms`; + + const histogramBody = { + histo: { date_histogram: { ...getFieldRef(field), fixed_interval: fixedInterval } }, + }; + const results = (await aggSearchWithBody(histogramBody)) as ESSearchResponse< + unknown, + { body: { aggs: typeof histogramBody } } + >; + + return { + totalDocuments: getHitsTotal(results), + histogram: { + buckets: results.aggregations!.histo.buckets.map((bucket) => ({ + count: bucket.doc_count, + key: bucket.key, + })), + }, + }; +} + +function getFieldRef(field: DataViewFieldBase) { + return field.scripted + ? { + script: { + lang: field.lang!, + source: field.script as string, + }, + } + : { field: field.name }; +} + +const getHitsTotal = (body: estypes.SearchResponse): number => { + return (body.hits.total as estypes.SearchTotalHits).value ?? body.hits.total ?? 0; +}; diff --git a/src/plugins/unified_field_list/kibana.json b/src/plugins/unified_field_list/kibana.json index 58a9212c90e39..e2fd80fe79ad7 100755 --- a/src/plugins/unified_field_list/kibana.json +++ b/src/plugins/unified_field_list/kibana.json @@ -7,7 +7,7 @@ "githubTeam": "kibana-data-discovery" }, "description": "Contains functionality for the field list which can be integrated into apps sidebar", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["dataViews", "data", "fieldFormats", "charts"], "optionalPlugins": [], diff --git a/src/plugins/unified_field_list/server/index.ts b/src/plugins/unified_field_list/server/index.ts new file mode 100644 index 0000000000000..039ea0488b533 --- /dev/null +++ b/src/plugins/unified_field_list/server/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginInitializerContext } from '@kbn/core/server'; +import { UnifiedFieldListPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new UnifiedFieldListPlugin(initializerContext); +} + +export type { + UnifiedFieldListServerPluginSetup, + UnifiedFieldListServerPluginStart, + PluginSetup, + PluginStart, +} from './types'; diff --git a/src/plugins/unified_field_list/server/plugin.ts b/src/plugins/unified_field_list/server/plugin.ts new file mode 100644 index 0000000000000..2853afcf3d6fd --- /dev/null +++ b/src/plugins/unified_field_list/server/plugin.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; +import { + UnifiedFieldListServerPluginSetup, + UnifiedFieldListServerPluginStart, + PluginStart, + PluginSetup, +} from './types'; +import { defineRoutes } from './routes'; + +export class UnifiedFieldListPlugin + implements Plugin +{ + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup, plugins: PluginSetup) { + this.logger.debug('unifiedFieldList: Setup'); + + defineRoutes(core); + + return {}; + } + + public start(core: CoreStart, plugins: PluginStart) { + this.logger.debug('unifiedFieldList: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/unified_field_list/server/routes/field_stats.ts b/src/plugins/unified_field_list/server/routes/field_stats.ts new file mode 100644 index 0000000000000..1f348d1914cd4 --- /dev/null +++ b/src/plugins/unified_field_list/server/routes/field_stats.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { errors } from '@elastic/elasticsearch'; +import { schema } from '@kbn/config-schema'; +import { CoreSetup } from '@kbn/core/server'; +import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; +import { FIELD_STATS_API_PATH } from '../../common/constants'; +import type { PluginStart } from '../types'; +import { + fetchAndCalculateFieldStats, + SearchHandler, + buildSearchParams, +} from '../../common/services/field_stats/field_stats_utils'; + +export async function initFieldStatsRoute(setup: CoreSetup) { + const router = setup.http.createRouter(); + router.post( + { + path: FIELD_STATS_API_PATH, + validate: { + body: schema.object( + { + dslQuery: schema.object({}, { unknowns: 'allow' }), + fromDate: schema.string(), + toDate: schema.string(), + dataViewId: schema.string(), + fieldName: schema.string(), + size: schema.maybe(schema.number()), + }, + { unknowns: 'allow' } + ), + }, + }, + async (context, req, res) => { + const requestClient = (await context.core).elasticsearch.client.asCurrentUser; + const { fromDate, toDate, fieldName, dslQuery, size, dataViewId } = req.body; + + const [{ savedObjects, elasticsearch }, { dataViews }] = await setup.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(req); + const esClient = elasticsearch.client.asScoped(req).asCurrentUser; + const indexPatternsService = await dataViews.dataViewsServiceFactory( + savedObjectsClient, + esClient + ); + + try { + const dataView = await indexPatternsService.get(dataViewId); + const field = dataView.fields.find((f) => f.name === fieldName); + + if (!field) { + throw new Error(`Field {fieldName} not found in data view ${dataView.title}`); + } + + const searchHandler: SearchHandler = async (aggs) => { + const result = await requestClient.search( + buildSearchParams({ + dataViewPattern: dataView.title, + timeFieldName: dataView.timeFieldName, + fromDate, + toDate, + dslQuery, + runtimeMappings: dataView.getRuntimeMappings(), + aggs, + }) + ); + return result; + }; + + const stats = await fetchAndCalculateFieldStats({ + searchHandler, + field, + fromDate, + toDate, + size, + }); + + return res.ok({ + body: stats, + }); + } catch (e) { + if (e instanceof SavedObjectNotFound) { + return res.notFound(); + } + if (e instanceof errors.ResponseError && e.statusCode === 404) { + return res.notFound(); + } + if (e.isBoom) { + if (e.output.statusCode === 404) { + return res.notFound(); + } + throw new Error(e.output.message); + } else { + throw e; + } + } + } + ); +} diff --git a/src/plugins/unified_field_list/server/routes/index.ts b/src/plugins/unified_field_list/server/routes/index.ts new file mode 100644 index 0000000000000..3558e93e4a780 --- /dev/null +++ b/src/plugins/unified_field_list/server/routes/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup } from '@kbn/core/server'; +import { PluginStart } from '../types'; +import { initFieldStatsRoute } from './field_stats'; + +export function defineRoutes(setup: CoreSetup) { + initFieldStatsRoute(setup); +} diff --git a/src/plugins/unified_field_list/server/types.ts b/src/plugins/unified_field_list/server/types.ts new file mode 100644 index 0000000000000..56cd69a01881e --- /dev/null +++ b/src/plugins/unified_field_list/server/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UnifiedFieldListServerPluginSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UnifiedFieldListServerPluginStart {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginSetup {} + +export interface PluginStart { + dataViews: DataViewsServerPluginStart; +} diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index 5f99067833afd..e5ae694c1de31 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -25,6 +25,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./maps')); loadTestFile(require.resolve('./security_solution')); loadTestFile(require.resolve('./lens')); + loadTestFile(require.resolve('./unified_field_list')); loadTestFile(require.resolve('./transform')); loadTestFile(require.resolve('./lists')); loadTestFile(require.resolve('./upgrade_assistant')); diff --git a/x-pack/test/api_integration/apis/lens/index.ts b/x-pack/test/api_integration/apis/lens/index.ts index 8d74ad475511f..90774a2db35ed 100644 --- a/x-pack/test/api_integration/apis/lens/index.ts +++ b/x-pack/test/api_integration/apis/lens/index.ts @@ -11,6 +11,5 @@ export default function lensApiIntegrationTests({ loadTestFile }: FtrProviderCon describe('Lens', () => { loadTestFile(require.resolve('./existing_fields')); loadTestFile(require.resolve('./legacy_existing_fields')); - loadTestFile(require.resolve('./field_stats')); }); } diff --git a/x-pack/test/api_integration/apis/lens/field_stats.ts b/x-pack/test/api_integration/apis/unified_field_list/field_stats.ts similarity index 93% rename from x-pack/test/api_integration/apis/lens/field_stats.ts rename to x-pack/test/api_integration/apis/unified_field_list/field_stats.ts index 4d38b54b02252..bf9954a3a23ae 100644 --- a/x-pack/test/api_integration/apis/lens/field_stats.ts +++ b/x-pack/test/api_integration/apis/unified_field_list/field_stats.ts @@ -19,8 +19,9 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const supertest = getService('supertest'); + const API_PATH = '/api/unified_field_list/field_stats'; - describe('index stats apis', () => { + describe('field stats apis', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); }); @@ -41,13 +42,13 @@ export default ({ getService }: FtrProviderContext) => { it('should return a 404 for missing index patterns', async () => { await supertest - .post('/api/lens/index_stats/123/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: '123', dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, - timeFieldName: '@timestamp', fieldName: 'bytes', }) .expect(404); @@ -55,9 +56,10 @@ export default ({ getService }: FtrProviderContext) => { it('should also work without specifying a time field', async () => { const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: 'logstash-2015.09.22', dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, @@ -70,9 +72,10 @@ export default ({ getService }: FtrProviderContext) => { it('should return an auto histogram for numbers and top values', async () => { const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: 'logstash-2015.09.22', dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, @@ -177,9 +180,10 @@ export default ({ getService }: FtrProviderContext) => { it('should return an auto histogram for dates', async () => { const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: 'logstash-2015.09.22', dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, @@ -210,9 +214,10 @@ export default ({ getService }: FtrProviderContext) => { it('should return top values for strings', async () => { const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: 'logstash-2015.09.22', dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, @@ -273,9 +278,10 @@ export default ({ getService }: FtrProviderContext) => { it('should return top values for ip fields', async () => { const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: 'logstash-2015.09.22', dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, @@ -336,9 +342,10 @@ export default ({ getService }: FtrProviderContext) => { it('should return histograms for scripted date fields', async () => { const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: 'logstash-2015.09.22', dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, @@ -361,9 +368,10 @@ export default ({ getService }: FtrProviderContext) => { it('should return top values for scripted string fields', async () => { const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: 'logstash-2015.09.22', dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, @@ -388,9 +396,10 @@ export default ({ getService }: FtrProviderContext) => { it('should return top values for index pattern runtime string fields', async () => { const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: 'logstash-2015.09.22', dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, @@ -415,9 +424,10 @@ export default ({ getService }: FtrProviderContext) => { it('should apply filters and queries', async () => { const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: 'logstash-2015.09.22', dslQuery: { bool: { filter: [{ match: { 'geo.src': 'US' } }], @@ -434,9 +444,10 @@ export default ({ getService }: FtrProviderContext) => { it('should allow filtering on a runtime field other than the field in use', async () => { const { body } = await supertest - .post('/api/lens/index_stats/logstash-2015.09.22/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: 'logstash-2015.09.22', dslQuery: { bool: { filter: [{ exists: { field: 'runtime_string_field' } }], @@ -477,9 +488,10 @@ export default ({ getService }: FtrProviderContext) => { it('should return an auto histogram for precalculated histograms', async () => { const { body } = await supertest - .post('/api/lens/index_stats/histogram-test/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: 'histogram-test', dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, @@ -545,9 +557,10 @@ export default ({ getService }: FtrProviderContext) => { it('should return a single-value histogram when filtering a precalculated histogram', async () => { const { body } = await supertest - .post('/api/lens/index_stats/histogram-test/field') + .post(API_PATH) .set(COMMON_HEADERS) .send({ + dataViewId: 'histogram-test', dslQuery: { match: { 'histogram-title': 'single value' } }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, diff --git a/x-pack/test/api_integration/apis/unified_field_list/index.ts b/x-pack/test/api_integration/apis/unified_field_list/index.ts new file mode 100644 index 0000000000000..9b5771bffd3d2 --- /dev/null +++ b/x-pack/test/api_integration/apis/unified_field_list/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function lensApiIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('UnifiedFieldList', () => { + loadTestFile(require.resolve('./field_stats')); + }); +} From e4dbbcd390e6585800e2b8193fffb246d2fd19be Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 11 Aug 2022 13:56:01 +0200 Subject: [PATCH 23/92] [UnifiedFieldList] Update limits --- packages/kbn-optimizer/limits.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index c23b423ac09cb..82cd25485b765 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -125,7 +125,7 @@ pageLoadAssetSize: cloudSecurityPosture: 19109 visTypeGauge: 24113 unifiedSearch: 71059 - unifiedFieldList: 16515 + unifiedFieldList: 36000 data: 454087 eventAnnotation: 19334 screenshotting: 22870 From 97f5f8224648a1a40ca55af314344054ccaa4ce0 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 11 Aug 2022 15:09:26 +0200 Subject: [PATCH 24/92] [UnifiedFieldList] Rename the component --- ...{field_stats_from_sample.tsx => field_stats_from_hits.tsx} | 4 ++-- .../public/components/field_stats/index.tsx | 4 ++-- src/plugins/unified_field_list/public/index.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/plugins/unified_field_list/public/components/field_stats/{field_stats_from_sample.tsx => field_stats_from_hits.tsx} (82%) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_sample.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_hits.tsx similarity index 82% rename from src/plugins/unified_field_list/public/components/field_stats/field_stats_from_sample.tsx rename to src/plugins/unified_field_list/public/components/field_stats/field_stats_from_hits.tsx index 1bd87178bec15..02622e751e127 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_sample.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_hits.tsx @@ -10,8 +10,8 @@ import React from 'react'; import { EuiText } from '@elastic/eui'; // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface FieldStatsFromSampleProps {} +export interface FieldStatsFromHitsProps {} -export const FieldStatsFromSample: React.FC = () => { +export const FieldStatsFromHits: React.FC = () => { return {'TODO: move current field stats from Discover to this component'}; }; diff --git a/src/plugins/unified_field_list/public/components/field_stats/index.tsx b/src/plugins/unified_field_list/public/components/field_stats/index.tsx index efe2ae40e98db..90313e6860fea 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/index.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/index.tsx @@ -9,5 +9,5 @@ export type { FieldStatsProps } from './field_stats'; export { FieldStats } from './field_stats'; -export type { FieldStatsFromSampleProps } from './field_stats_from_sample'; -export { FieldStatsFromSample } from './field_stats_from_sample'; +export type { FieldStatsFromHitsProps } from './field_stats_from_hits'; +export { FieldStatsFromHits } from './field_stats_from_hits'; diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts index f5865ce66bd39..5068b2f7ee2b2 100755 --- a/src/plugins/unified_field_list/public/index.ts +++ b/src/plugins/unified_field_list/public/index.ts @@ -14,8 +14,8 @@ export type { NumberStatsResult, TopValuesResult, } from '../common/types'; -export type { FieldStatsProps, FieldStatsFromSampleProps } from './components/field_stats'; -export { FieldStats, FieldStatsFromSample } from './components/field_stats'; +export type { FieldStatsProps, FieldStatsFromHitsProps } from './components/field_stats'; +export { FieldStats, FieldStatsFromHits } from './components/field_stats'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. From 0e5a2c9781d38a560d053313f3edd5b6a15afa70 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 11 Aug 2022 15:28:08 +0200 Subject: [PATCH 25/92] [UnifiedFieldList] Improve types --- .../common/services/field_stats/field_stats.ts | 4 ++-- .../services/field_stats/field_stats_utils.ts | 15 ++++++++++++--- .../common/services/field_stats/index.ts | 1 + .../public/components/field_stats/field_stats.tsx | 14 +++++--------- .../public/indexpattern_datasource/field_item.tsx | 2 +- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts b/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts index 26e91e523620d..79e6e8d8d5946 100644 --- a/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts +++ b/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts @@ -9,7 +9,7 @@ import { lastValueFrom } from 'rxjs'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { BoolQuery, DataViewFieldBase } from '@kbn/es-query'; +import type { DataViewFieldBase } from '@kbn/es-query'; import type { FieldStatsResponse } from '../../types'; import { fetchAndCalculateFieldStats, SearchHandler, buildSearchParams } from './field_stats_utils'; @@ -19,7 +19,7 @@ interface FetchFieldStatsParams { field: DataViewFieldBase; fromDate: string; toDate: string; - dslQuery: { bool: BoolQuery } | {}; + dslQuery: object; size?: number; } diff --git a/src/plugins/unified_field_list/common/services/field_stats/field_stats_utils.ts b/src/plugins/unified_field_list/common/services/field_stats/field_stats_utils.ts index 154790419434a..6ef835af5e90c 100644 --- a/src/plugins/unified_field_list/common/services/field_stats/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/services/field_stats/field_stats_utils.ts @@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import DateMath from '@kbn/datemath'; import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; -import type { DataViewFieldBase, BoolQuery } from '@kbn/es-query'; +import type { DataViewFieldBase } from '@kbn/es-query'; import type { FieldStatsResponse } from '../../types'; export type SearchHandler = ( @@ -32,7 +32,7 @@ export function buildSearchParams({ timeFieldName?: string; fromDate: string; toDate: string; - dslQuery: { bool: BoolQuery } | {}; + dslQuery: object; runtimeMappings: estypes.MappingRuntimeFields; aggs: Record; }) { @@ -81,7 +81,7 @@ export async function fetchAndCalculateFieldStats({ toDate: string; size?: number; }) { - if (field.type.includes('range')) { + if (!canProvideFieldStatsForField(field)) { return {}; } @@ -100,6 +100,15 @@ export async function fetchAndCalculateFieldStats({ return await getStringSamples(searchHandler, field, size); } +export function canProvideFieldStatsForField(field: DataViewFieldBase): boolean { + return !( + field.type === 'document' || + field.type.includes('range') || + field.type === 'geo_point' || + field.type === 'geo_shape' + ); +} + export async function getNumberHistogram( aggSearchWithBody: SearchHandler, field: DataViewFieldBase, diff --git a/src/plugins/unified_field_list/common/services/field_stats/index.ts b/src/plugins/unified_field_list/common/services/field_stats/index.ts index 7065fac3dea30..420f7a3312e8d 100755 --- a/src/plugins/unified_field_list/common/services/field_stats/index.ts +++ b/src/plugins/unified_field_list/common/services/field_stats/index.ts @@ -7,3 +7,4 @@ */ export { fetchFieldStats } from './field_stats'; +export { canProvideFieldStatsForField } from './field_stats_utils'; diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index a440e8af60912..b85c8314c6756 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -39,7 +39,10 @@ import { import { i18n } from '@kbn/i18n'; import { buildEsQuery, Query, Filter, AggregateQuery } from '@kbn/es-query'; import type { BucketedAggregation } from '../../../common/types'; -import { fetchFieldStats } from '../../../common/services/field_stats'; +import { + fetchFieldStats, + canProvideFieldStatsForField, +} from '../../../common/services/field_stats'; import { useUnifiedFieldListServices } from '../../hooks/use_unified_field_list_services'; import './field_stats.scss'; @@ -91,14 +94,7 @@ const FieldStatsComponent: React.FC = ({ setDataView(loadedDataView); - // Range types don't have any useful stats we can show - if ( - state.isLoading || - field.type === 'document' || - field.type.includes('range') || - field.type === 'geo_point' || - field.type === 'geo_shape' - ) { + if (state.isLoading || !canProvideFieldStatsForField(field)) { return; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 75857f6d86048..3781826f8c3ce 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -340,7 +340,7 @@ function FieldItemPopoverContents(props: FieldItemProps) { filters={filters} fromDate={dateRange.fromDate} toDate={dateRange.toDate} - dataViewOrDataViewId={indexPattern.id} + dataViewOrDataViewId={indexPattern.id} // TODO: Refactor to pass a variable with DataView type instead field={field as DataViewField} testSubject="lnsFieldListPanel" overrideContent={(currentField, params) => { From c9e3edb0b947946f5f9644cfd9e1058e23f4d053 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 11 Aug 2022 15:57:56 +0200 Subject: [PATCH 26/92] [UnifiedFieldList] Add AbortController --- .../services/field_stats/field_stats.ts | 29 ++++++---- .../components/field_stats/field_stats.tsx | 57 +++++++++++++++---- 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts b/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts index 79e6e8d8d5946..98f45d26dd338 100644 --- a/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts +++ b/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts @@ -21,6 +21,7 @@ interface FetchFieldStatsParams { toDate: string; dslQuery: object; size?: number; + abortController?: AbortController; } export const fetchFieldStats = async ({ @@ -31,6 +32,7 @@ export const fetchFieldStats = async ({ toDate, dslQuery, size, + abortController, }: FetchFieldStatsParams): Promise> => { try { if (!dataView?.id || !field?.type) { @@ -39,17 +41,22 @@ export const fetchFieldStats = async ({ const searchHandler: SearchHandler = async (aggs) => { const result = await lastValueFrom( - data.search.search({ - params: buildSearchParams({ - dataViewPattern: dataView.title, - timeFieldName: dataView.timeFieldName, - fromDate, - toDate, - dslQuery, - runtimeMappings: dataView.getRuntimeMappings(), - aggs, - }), - }) + data.search.search( + { + params: buildSearchParams({ + dataViewPattern: dataView.title, + timeFieldName: dataView.timeFieldName, + fromDate, + toDate, + dslQuery, + runtimeMappings: dataView.getRuntimeMappings(), + aggs, + }), + }, + { + abortSignal: abortController?.signal, + } + ) ); return result.rawResponse; }; diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index b85c8314c6756..c1f5df1fe094c 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { DataView, DataViewField, @@ -81,26 +81,52 @@ const FieldStatsComponent: React.FC = ({ }) => { const services = useUnifiedFieldListServices(); const { fieldFormats, uiSettings, charts, dataViews, data } = services; - const [state, setState] = useState({ + const [state, changeState] = useState({ isLoading: false, }); - const [dataView, setDataView] = useState(null); - - async function fetchData() { - const loadedDataView = - typeof dataViewOrDataViewId === 'string' - ? await dataViews.get(dataViewOrDataViewId) - : dataViewOrDataViewId; + const [dataView, changeDataView] = useState(null); + const abortControllerRef = useRef(null); + const isCanceledRef = useRef(false); + + const setState: typeof changeState = useCallback( + (nextState) => { + if (!isCanceledRef.current) { + changeState(nextState); + } + }, + [changeState, isCanceledRef] + ); - setDataView(loadedDataView); + const setDataView: typeof changeDataView = useCallback( + (nextDataView) => { + if (!isCanceledRef.current) { + changeDataView(nextDataView); + } + }, + [changeDataView, isCanceledRef] + ); - if (state.isLoading || !canProvideFieldStatsForField(field)) { + async function fetchData() { + if (isCanceledRef.current) { return; } try { + const loadedDataView = + typeof dataViewOrDataViewId === 'string' + ? await dataViews.get(dataViewOrDataViewId) + : dataViewOrDataViewId; + + setDataView(loadedDataView); + + if (state.isLoading || !canProvideFieldStatsForField(field)) { + return; + } + setState((s) => ({ ...s, isLoading: true })); + abortControllerRef.current = new AbortController(); + const results = await fetchFieldStats({ data, dataView: loadedDataView, @@ -108,9 +134,11 @@ const FieldStatsComponent: React.FC = ({ fromDate, toDate, dslQuery: buildEsQuery(loadedDataView, query, filters, getEsQueryConfig(uiSettings)), - // TODO: pass abortSignal on unmount + abortController: abortControllerRef.current, }); + abortControllerRef.current = null; + setState((s) => ({ ...s, isLoading: false, @@ -128,6 +156,11 @@ const FieldStatsComponent: React.FC = ({ useEffect(() => { fetchData(); + + return () => { + isCanceledRef.current = true; + abortControllerRef.current?.abort(); + }; }, []); // eslint-disable-line react-hooks/exhaustive-deps const chartTheme = charts.theme.useChartsTheme(); From 48bc2b8572ec6fd892b9ca3a31c045dae73e3b32 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 11 Aug 2022 16:54:54 +0200 Subject: [PATCH 27/92] [UnifiedFieldList] Render counts in PopoverFooter in Lens --- .../components/sidebar/discover_field.tsx | 9 +-- .../components/field_stats/field_stats.tsx | 72 +++++++++++-------- .../indexpattern_datasource/field_item.tsx | 10 +-- 3 files changed, 52 insertions(+), 39 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 26d59a4d5f02e..2ed5e73400b2a 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -410,6 +410,7 @@ function DiscoverFieldComponent({ const renderPopover = () => { const details = getDetails(field); const dateRange = data?.query?.timefilter.timefilter.getTime(); + const fieldForStats = multiFields ? multiFields[0].field : field; // TODO: how to handle multifields? return ( <> @@ -426,17 +427,17 @@ function DiscoverFieldComponent({ fromDate={dateRange.from} toDate={dateRange.to} dataViewOrDataViewId={dataView} - field={multiFields ? multiFields[0].field : field} // TODO: how to handle multifields? + field={fieldForStats} testSubject="dscFieldListPanel" - overrideContent={(currentField, params) => { + overrideMissingContent={(params) => { if (params?.noDataFound) { return ( - {`TODO: add a custom "no data available" message for ${currentField.type} field`} + {`TODO: add a custom "no data available" message for ${fieldForStats.type} field`} ); } return ( - {`TODO: add a custom "stats are not available" message for ${currentField.type} field`} + {`TODO: add a custom "stats are not available" message for ${fieldForStats.type} field`} ); }} /> diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index c1f5df1fe094c..5478ac12f9004 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -63,10 +63,12 @@ export interface FieldStatsProps { dataViewOrDataViewId: DataView | string; field: DataViewField; testSubject: string; - overrideContent?: ( - field: DataViewField, - params?: { noDataFound?: boolean } - ) => JSX.Element | null; + overrideMissingContent?: (params?: { noDataFound?: boolean }) => JSX.Element | null; + overrideFooter?: (params: { + element: JSX.Element; + totalDocuments?: number; + sampledDocuments?: number; + }) => JSX.Element; } const FieldStatsComponent: React.FC = ({ @@ -77,7 +79,8 @@ const FieldStatsComponent: React.FC = ({ dataViewOrDataViewId, field, testSubject, - overrideContent, + overrideMissingContent, + overrideFooter, }) => { const services = useUnifiedFieldListServices(); const { fieldFormats, uiSettings, charts, dataViews, data } = services; @@ -226,14 +229,14 @@ const FieldStatsComponent: React.FC = ({ } if (field.type === 'geo_point' || field.type === 'geo_shape') { - return overrideContent?.(field) || null; + return overrideMissingContent ? overrideMissingContent() : null; } if ( (!histogram || histogram.buckets.length === 0) && (!topValues || topValues.buckets.length === 0) ) { - return overrideContent?.(field, { noDataFound: true }) || null; + return overrideMissingContent ? overrideMissingContent({ noDataFound: true }) : null; } if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { @@ -287,6 +290,31 @@ const FieldStatsComponent: React.FC = ({ } function combineWithTitleAndFooter(el: React.ReactElement) { + const countsElement = totalDocuments ? ( + + {sampledDocuments && ( + <> + {i18n.translate('unifiedFieldList.fieldStats.percentageOfLabel', { + defaultMessage: '{percentage}% of', + values: { + percentage: Math.round((sampledDocuments / totalDocuments) * 100), + }, + })} + + )}{' '} + + {fieldFormats + .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) + .convert(totalDocuments)} + {' '} + {i18n.translate('unifiedFieldList.fieldStats.ofDocumentsLabel', { + defaultMessage: 'documents', + })} + + ) : ( + <> + ); + return ( <> {title ? title : <>} @@ -295,31 +323,13 @@ const FieldStatsComponent: React.FC = ({ {el} - - - {totalDocuments ? ( - - {sampledDocuments && ( - <> - {i18n.translate('unifiedFieldList.fieldStats.percentageOfLabel', { - defaultMessage: '{percentage}% of', - values: { - percentage: Math.round((sampledDocuments / totalDocuments) * 100), - }, - })} - - )}{' '} - - {fieldFormats - .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) - .convert(totalDocuments)} - {' '} - {i18n.translate('unifiedFieldList.fieldStats.ofDocumentsLabel', { - defaultMessage: 'documents', - })} - + {overrideFooter ? ( + overrideFooter?.({ element: countsElement, totalDocuments, sampledDocuments }) ) : ( - <> + <> + + {countsElement} + )} ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 3781826f8c3ce..13126b773965b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -15,6 +15,7 @@ import { EuiIconTip, EuiPopover, EuiPopoverTitle, + EuiPopoverFooter, EuiSpacer, EuiText, EuiTitle, @@ -343,17 +344,18 @@ function FieldItemPopoverContents(props: FieldItemProps) { dataViewOrDataViewId={indexPattern.id} // TODO: Refactor to pass a variable with DataView type instead field={field as DataViewField} testSubject="lnsFieldListPanel" - overrideContent={(currentField, params) => { - if (currentField.type === 'geo_point' || currentField.type === 'geo_shape') { + overrideFooter={({ element }) => {element}} + overrideMissingContent={(params) => { + if (field.type === 'geo_point' || field.type === 'geo_shape') { return ( <> - {getVisualizeGeoFieldMessage(currentField.type)} + {getVisualizeGeoFieldMessage(field.type)} ); From 0e78ac2f68014770161974d27c1755c002f114e3 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 11 Aug 2022 17:03:03 +0200 Subject: [PATCH 28/92] [UnifiedFieldList] Hide new stats from Discover for now --- .../components/sidebar/discover_field.tsx | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 2ed5e73400b2a..2f14164ee95cc 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -411,43 +411,48 @@ function DiscoverFieldComponent({ const details = getDetails(field); const dateRange = data?.query?.timefilter.timefilter.getTime(); const fieldForStats = multiFields ? multiFields[0].field : field; // TODO: how to handle multifields? + const showNewStatsPreviewInDiscover = false; // Toggle this variable to preview new stats locally return ( <> {showFieldStats && ( <> - - {'Stats as in Lens:'} - - - {Boolean(dateRange) && ( - { - if (params?.noDataFound) { - return ( - {`TODO: add a custom "no data available" message for ${fieldForStats.type} field`} - ); - } + {showNewStatsPreviewInDiscover && ( + <> + + {'Stats as in Lens:'} + + + {Boolean(dateRange) && ( + { + if (params?.noDataFound) { + return ( + {`TODO: add a custom "no data available" message for ${fieldForStats.type} field`} + ); + } - return ( - {`TODO: add a custom "stats are not available" message for ${fieldForStats.type} field`} - ); - }} - /> + return ( + {`TODO: add a custom "stats are not available" message for ${fieldForStats.type} field`} + ); + }} + /> + )} + {/* TODO: remove previous field stats view when we finish FieldStats component and add addFilter buttons to it */} + + + {'Current Discover stats:'} + + + )} - {/* TODO: remove previous field stats view when we finish FieldStats component and add addFilter buttons to it */} - - - {'Current Discover stats:'} - -
{i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { From 41baa7abdcd391e6948887b3e994bb7853b12d4c Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 12 Aug 2022 10:58:29 +0200 Subject: [PATCH 29/92] [UnifiedFieldList] Fix tests --- .../public/components/field_stats/field_stats.tsx | 1 + .../public/indexpattern_datasource/field_item.test.tsx | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 5478ac12f9004..fca599d9c1869 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -128,6 +128,7 @@ const FieldStatsComponent: React.FC = ({ setState((s) => ({ ...s, isLoading: true })); + abortControllerRef.current?.abort(); abortControllerRef.current = new AbortController(); const results = await fetchFieldStats({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index f3c0cbc6c333a..1b1ae26fd9a81 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -25,7 +25,7 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import { fetchFieldStats } from '@kbn/unified-field-list-plugin/common/services/field_stats'; import { DOCUMENT_FIELD_NAME } from '../../common'; -jest.mock('@kbn/unified-field-list-plugin/common/services/field_stats', () => ({ +jest.mock('@kbn/unified-field-list-plugin/common/services/field_stats/field_stats', () => ({ fetchFieldStats: jest.fn().mockResolvedValue({}), })); @@ -212,7 +212,10 @@ describe('IndexPattern Field Item', () => { await clickField(wrapper, 'bytes'); + await wrapper.update(); + expect(fetchFieldStats).toHaveBeenCalledWith({ + abortController: new AbortController(), data: mockedServices.data, dataView, dslQuery: { @@ -228,8 +231,6 @@ describe('IndexPattern Field Item', () => { field: defaultProps.field, }); - await wrapper.update(); - expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); @@ -289,6 +290,7 @@ describe('IndexPattern Field Item', () => { expect(fetchFieldStats).toHaveBeenCalledTimes(2); expect(fetchFieldStats).toHaveBeenLastCalledWith({ + abortController: new AbortController(), data: mockedServices.data, dataView, dslQuery: { From 937e8f7ef2197d7154a028edee4895755397b59f Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 12 Aug 2022 11:08:35 +0200 Subject: [PATCH 30/92] [UnifiedFieldList] Rename to loadFieldStats --- .../components/sidebar/discover_field.tsx | 2 +- .../services/field_stats/field_stats.ts | 2 +- .../common/services/field_stats/index.ts | 2 +- .../components/field_stats/field_stats.tsx | 7 ++---- .../unified_field_list/public/index.ts | 2 +- .../field_item.test.tsx | 22 +++++++++---------- .../definitions/terms/helpers.test.ts | 2 +- .../operations/definitions/terms/helpers.ts | 4 ++-- .../definitions/terms/terms.test.tsx | 2 +- 9 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 2f14164ee95cc..1b60f6a412ea6 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -411,7 +411,7 @@ function DiscoverFieldComponent({ const details = getDetails(field); const dateRange = data?.query?.timefilter.timefilter.getTime(); const fieldForStats = multiFields ? multiFields[0].field : field; // TODO: how to handle multifields? - const showNewStatsPreviewInDiscover = false; // Toggle this variable to preview new stats locally + const showNewStatsPreviewInDiscover = true; // Toggle this variable to preview new stats locally return ( <> diff --git a/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts b/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts index 98f45d26dd338..27a0443865ccb 100644 --- a/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts +++ b/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts @@ -24,7 +24,7 @@ interface FetchFieldStatsParams { abortController?: AbortController; } -export const fetchFieldStats = async ({ +export const loadFieldStats = async ({ data, dataView, field, diff --git a/src/plugins/unified_field_list/common/services/field_stats/index.ts b/src/plugins/unified_field_list/common/services/field_stats/index.ts index 420f7a3312e8d..2af1f36f8f9ca 100755 --- a/src/plugins/unified_field_list/common/services/field_stats/index.ts +++ b/src/plugins/unified_field_list/common/services/field_stats/index.ts @@ -6,5 +6,5 @@ * Side Public License, v 1. */ -export { fetchFieldStats } from './field_stats'; +export { loadFieldStats } from './field_stats'; export { canProvideFieldStatsForField } from './field_stats_utils'; diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index fca599d9c1869..ead9129106180 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -39,10 +39,7 @@ import { import { i18n } from '@kbn/i18n'; import { buildEsQuery, Query, Filter, AggregateQuery } from '@kbn/es-query'; import type { BucketedAggregation } from '../../../common/types'; -import { - fetchFieldStats, - canProvideFieldStatsForField, -} from '../../../common/services/field_stats'; +import { loadFieldStats, canProvideFieldStatsForField } from '../../../common/services/field_stats'; import { useUnifiedFieldListServices } from '../../hooks/use_unified_field_list_services'; import './field_stats.scss'; @@ -131,7 +128,7 @@ const FieldStatsComponent: React.FC = ({ abortControllerRef.current?.abort(); abortControllerRef.current = new AbortController(); - const results = await fetchFieldStats({ + const results = await loadFieldStats({ data, dataView: loadedDataView, field, diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts index 5068b2f7ee2b2..107ba7e8fe6fc 100755 --- a/src/plugins/unified_field_list/public/index.ts +++ b/src/plugins/unified_field_list/public/index.ts @@ -24,4 +24,4 @@ export function plugin() { } export type { UnifiedFieldListPluginSetup, UnifiedFieldListPluginStart } from './types'; -export { fetchFieldStats } from '../common/services/field_stats'; +export { loadFieldStats } from '../common/services/field_stats'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 1b1ae26fd9a81..7b63be07ac98d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -22,11 +22,11 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { fetchFieldStats } from '@kbn/unified-field-list-plugin/common/services/field_stats'; +import { loadFieldStats } from '@kbn/unified-field-list-plugin/common/services/field_stats'; import { DOCUMENT_FIELD_NAME } from '../../common'; jest.mock('@kbn/unified-field-list-plugin/common/services/field_stats/field_stats', () => ({ - fetchFieldStats: jest.fn().mockResolvedValue({}), + loadFieldStats: jest.fn().mockResolvedValue({}), })); const chartsThemeService = chartPluginMock.createSetupContract().theme; @@ -202,7 +202,7 @@ describe('IndexPattern Field Item', () => { it('should request field stats every time the button is clicked', async () => { let resolveFunction: (arg: unknown) => void; - (fetchFieldStats as jest.Mock).mockImplementation(() => { + (loadFieldStats as jest.Mock).mockImplementation(() => { return new Promise((resolve) => { resolveFunction = resolve; }); @@ -214,7 +214,7 @@ describe('IndexPattern Field Item', () => { await wrapper.update(); - expect(fetchFieldStats).toHaveBeenCalledWith({ + expect(loadFieldStats).toHaveBeenCalledWith({ abortController: new AbortController(), data: mockedServices.data, dataView, @@ -257,7 +257,7 @@ describe('IndexPattern Field Item', () => { await wrapper.update(); - expect(fetchFieldStats).toHaveBeenCalledTimes(1); + expect(loadFieldStats).toHaveBeenCalledTimes(1); act(() => { const closePopover = wrapper.find(EuiPopover).prop('closePopover'); @@ -288,8 +288,8 @@ describe('IndexPattern Field Item', () => { await wrapper.update(); - expect(fetchFieldStats).toHaveBeenCalledTimes(2); - expect(fetchFieldStats).toHaveBeenLastCalledWith({ + expect(loadFieldStats).toHaveBeenCalledTimes(2); + expect(loadFieldStats).toHaveBeenLastCalledWith({ abortController: new AbortController(), data: mockedServices.data, dataView, @@ -316,8 +316,8 @@ describe('IndexPattern Field Item', () => { field: defaultProps.field, }); - (fetchFieldStats as jest.Mock).mockReset(); - (fetchFieldStats as jest.Mock).mockImplementation(() => Promise.resolve({})); + (loadFieldStats as jest.Mock).mockReset(); + (loadFieldStats as jest.Mock).mockImplementation(() => Promise.resolve({})); }); it('should not request field stats for document field', async () => { @@ -329,7 +329,7 @@ describe('IndexPattern Field Item', () => { await wrapper.update(); - expect(fetchFieldStats).not.toHaveBeenCalled(); + expect(loadFieldStats).not.toHaveBeenCalled(); expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); }); @@ -350,6 +350,6 @@ describe('IndexPattern Field Item', () => { await clickField(wrapper, 'ip_range'); - expect(fetchFieldStats).not.toHaveBeenCalled(); + expect(loadFieldStats).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.test.ts index 077e423eaea83..df62facf34379 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.test.ts @@ -24,7 +24,7 @@ import type { PercentileIndexPatternColumn } from '../percentile'; import { MULTI_KEY_VISUAL_SEPARATOR } from './constants'; jest.mock('@kbn/unified-field-list-plugin/common/services/field_stats', () => ({ - fetchFieldStats: jest.fn().mockResolvedValue({ + loadFieldStats: jest.fn().mockResolvedValue({ topValues: { buckets: [ { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts index 0e1ceb9c3e48d..b30edefb05b9b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts @@ -10,7 +10,7 @@ import { uniq } from 'lodash'; import type { CoreStart } from '@kbn/core/public'; import { buildEsQuery } from '@kbn/es-query'; import { getEsQueryConfig, DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { FieldStatsResponse, fetchFieldStats } from '@kbn/unified-field-list-plugin/public'; +import { FieldStatsResponse, loadFieldStats } from '@kbn/unified-field-list-plugin/public'; import { GenericIndexPatternColumn, operationDefinitionMap } from '..'; import { defaultLabel } from '../filters'; import { isReferenced } from '../../layer_helpers'; @@ -139,7 +139,7 @@ export function getDisallowedTermsMessage( if (!activeDataFieldNameMatch || currentTerms.length === 0) { if (fieldNames.length === 1) { const currentDataView = await data.dataViews.get(indexPattern.id); - const response: FieldStatsResponse = await fetchFieldStats({ + const response: FieldStatsResponse = await loadFieldStats({ data, dataView: currentDataView, field: indexPattern.getFieldByName(fieldNames[0])!, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 5f63c0e3a14e1..926b3d03319ba 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -35,7 +35,7 @@ import { cloneDeep } from 'lodash'; import { IncludeExcludeRow } from './include_exclude_options'; jest.mock('@kbn/unified-field-list-plugin/common/services/field_stats', () => ({ - fetchFieldStats: jest.fn().mockResolvedValue({ + loadFieldStats: jest.fn().mockResolvedValue({ topValues: { buckets: [ { From 693959c418a628217918aa4bac958a3b479a2c2d Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 12 Aug 2022 14:09:29 +0200 Subject: [PATCH 31/92] [UnifiedFieldList] Rearrange utils --- .github/CODEOWNERS | 3 +++ .../main/components/sidebar/discover_field.tsx | 2 +- .../{services/field_stats => utils}/field_stats_utils.ts | 6 +++--- .../public/components/field_stats/field_stats.tsx | 4 ++-- src/plugins/unified_field_list/public/index.ts | 2 +- .../field_stats => public/services}/field_stats.ts | 8 ++++++-- .../field_stats/index.ts => public/services/index.tsx} | 2 +- .../unified_field_list/server/routes/field_stats.ts | 2 +- .../apis/unified_field_list/field_stats.ts | 5 +++-- .../api_integration/apis/unified_field_list/index.ts | 5 +++-- .../public/indexpattern_datasource/field_item.test.tsx | 4 ++-- 11 files changed, 26 insertions(+), 17 deletions(-) rename src/plugins/unified_field_list/common/{services/field_stats => utils}/field_stats_utils.ts (97%) rename src/plugins/unified_field_list/{common/services/field_stats => public/services}/field_stats.ts (91%) rename src/plugins/unified_field_list/{common/services/field_stats/index.ts => public/services/index.tsx} (83%) rename {x-pack/test => test}/api_integration/apis/unified_field_list/field_stats.ts (98%) rename {x-pack/test => test}/api_integration/apis/unified_field_list/index.ts (68%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 44ff081afdeb6..d428ed7f73579 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -12,6 +12,7 @@ /src/plugins/discover/ @elastic/kibana-data-discovery /x-pack/plugins/discover_enhanced/ @elastic/kibana-data-discovery /test/functional/apps/discover/ @elastic/kibana-data-discovery +/test/api_integration/apis/unified_field_list/ @elastic/kibana-data-discovery /x-pack/plugins/graph/ @elastic/kibana-data-discovery /x-pack/test/functional/apps/graph @elastic/kibana-data-discovery /src/plugins/unified_field_list/ @elastic/kibana-data-discovery @@ -41,8 +42,10 @@ /src/plugins/url_forwarding/ @elastic/kibana-vis-editors /packages/kbn-tinymath/ @elastic/kibana-vis-editors /x-pack/test/functional/apps/lens @elastic/kibana-vis-editors +/x-pack/test/api_integration/apis/lens/ @elastic/kibana-vis-editors /test/functional/apps/visualize/ @elastic/kibana-vis-editors /src/plugins/unified_field_list/ @elastic/kibana-vis-editors +/test/api_integration/apis/unified_field_list/ @elastic/kibana-vis-editors # Application Services /examples/bfetch_explorer/ @elastic/kibana-app-services diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 1b60f6a412ea6..2f14164ee95cc 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -411,7 +411,7 @@ function DiscoverFieldComponent({ const details = getDetails(field); const dateRange = data?.query?.timefilter.timefilter.getTime(); const fieldForStats = multiFields ? multiFields[0].field : field; // TODO: how to handle multifields? - const showNewStatsPreviewInDiscover = true; // Toggle this variable to preview new stats locally + const showNewStatsPreviewInDiscover = false; // Toggle this variable to preview new stats locally return ( <> diff --git a/src/plugins/unified_field_list/common/services/field_stats/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts similarity index 97% rename from src/plugins/unified_field_list/common/services/field_stats/field_stats_utils.ts rename to src/plugins/unified_field_list/common/utils/field_stats_utils.ts index 6ef835af5e90c..33aa80323c22f 100644 --- a/src/plugins/unified_field_list/common/services/field_stats/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -10,7 +10,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import DateMath from '@kbn/datemath'; import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; import type { DataViewFieldBase } from '@kbn/es-query'; -import type { FieldStatsResponse } from '../../types'; +import type { FieldStatsResponse } from '../types'; export type SearchHandler = ( aggs: Record @@ -81,7 +81,7 @@ export async function fetchAndCalculateFieldStats({ toDate: string; size?: number; }) { - if (!canProvideFieldStatsForField(field)) { + if (!canProvideStatsForField(field)) { return {}; } @@ -100,7 +100,7 @@ export async function fetchAndCalculateFieldStats({ return await getStringSamples(searchHandler, field, size); } -export function canProvideFieldStatsForField(field: DataViewFieldBase): boolean { +export function canProvideStatsForField(field: DataViewFieldBase): boolean { return !( field.type === 'document' || field.type.includes('range') || diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index ead9129106180..7e637f5baa4a8 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -39,7 +39,7 @@ import { import { i18n } from '@kbn/i18n'; import { buildEsQuery, Query, Filter, AggregateQuery } from '@kbn/es-query'; import type { BucketedAggregation } from '../../../common/types'; -import { loadFieldStats, canProvideFieldStatsForField } from '../../../common/services/field_stats'; +import { loadFieldStats, canProvideStatsForField } from '../../services'; import { useUnifiedFieldListServices } from '../../hooks/use_unified_field_list_services'; import './field_stats.scss'; @@ -119,7 +119,7 @@ const FieldStatsComponent: React.FC = ({ setDataView(loadedDataView); - if (state.isLoading || !canProvideFieldStatsForField(field)) { + if (state.isLoading || !canProvideStatsForField(field)) { return; } diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts index 107ba7e8fe6fc..537f797e2c39c 100755 --- a/src/plugins/unified_field_list/public/index.ts +++ b/src/plugins/unified_field_list/public/index.ts @@ -24,4 +24,4 @@ export function plugin() { } export type { UnifiedFieldListPluginSetup, UnifiedFieldListPluginStart } from './types'; -export { loadFieldStats } from '../common/services/field_stats'; +export { loadFieldStats, canProvideStatsForField } from './services'; diff --git a/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts b/src/plugins/unified_field_list/public/services/field_stats.ts similarity index 91% rename from src/plugins/unified_field_list/common/services/field_stats/field_stats.ts rename to src/plugins/unified_field_list/public/services/field_stats.ts index 27a0443865ccb..cfd3798126594 100644 --- a/src/plugins/unified_field_list/common/services/field_stats/field_stats.ts +++ b/src/plugins/unified_field_list/public/services/field_stats.ts @@ -10,8 +10,12 @@ import { lastValueFrom } from 'rxjs'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewFieldBase } from '@kbn/es-query'; -import type { FieldStatsResponse } from '../../types'; -import { fetchAndCalculateFieldStats, SearchHandler, buildSearchParams } from './field_stats_utils'; +import type { FieldStatsResponse } from '../types'; +import { + fetchAndCalculateFieldStats, + SearchHandler, + buildSearchParams, +} from '../../common/utils/field_stats_utils'; interface FetchFieldStatsParams { data: DataPublicPluginStart; diff --git a/src/plugins/unified_field_list/common/services/field_stats/index.ts b/src/plugins/unified_field_list/public/services/index.tsx similarity index 83% rename from src/plugins/unified_field_list/common/services/field_stats/index.ts rename to src/plugins/unified_field_list/public/services/index.tsx index 2af1f36f8f9ca..c612fb2b1e917 100755 --- a/src/plugins/unified_field_list/common/services/field_stats/index.ts +++ b/src/plugins/unified_field_list/public/services/index.tsx @@ -7,4 +7,4 @@ */ export { loadFieldStats } from './field_stats'; -export { canProvideFieldStatsForField } from './field_stats_utils'; +export { canProvideStatsForField } from '../../common/utils/field_stats_utils'; diff --git a/src/plugins/unified_field_list/server/routes/field_stats.ts b/src/plugins/unified_field_list/server/routes/field_stats.ts index 1f348d1914cd4..4fc654f892210 100644 --- a/src/plugins/unified_field_list/server/routes/field_stats.ts +++ b/src/plugins/unified_field_list/server/routes/field_stats.ts @@ -16,7 +16,7 @@ import { fetchAndCalculateFieldStats, SearchHandler, buildSearchParams, -} from '../../common/services/field_stats/field_stats_utils'; +} from '../../common/utils/field_stats_utils'; export async function initFieldStatsRoute(setup: CoreSetup) { const router = setup.http.createRouter(); diff --git a/x-pack/test/api_integration/apis/unified_field_list/field_stats.ts b/test/api_integration/apis/unified_field_list/field_stats.ts similarity index 98% rename from x-pack/test/api_integration/apis/unified_field_list/field_stats.ts rename to test/api_integration/apis/unified_field_list/field_stats.ts index bf9954a3a23ae..b489dea50d740 100644 --- a/x-pack/test/api_integration/apis/unified_field_list/field_stats.ts +++ b/test/api_integration/apis/unified_field_list/field_stats.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import expect from '@kbn/expect'; diff --git a/x-pack/test/api_integration/apis/unified_field_list/index.ts b/test/api_integration/apis/unified_field_list/index.ts similarity index 68% rename from x-pack/test/api_integration/apis/unified_field_list/index.ts rename to test/api_integration/apis/unified_field_list/index.ts index 9b5771bffd3d2..da0a6098e0a42 100644 --- a/x-pack/test/api_integration/apis/unified_field_list/index.ts +++ b/test/api_integration/apis/unified_field_list/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 7b63be07ac98d..d2bb675d3f331 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -22,10 +22,10 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { loadFieldStats } from '@kbn/unified-field-list-plugin/common/services/field_stats'; +import { loadFieldStats } from '@kbn/unified-field-list-plugin/public'; import { DOCUMENT_FIELD_NAME } from '../../common'; -jest.mock('@kbn/unified-field-list-plugin/common/services/field_stats/field_stats', () => ({ +jest.mock('@kbn/unified-field-list-plugin/public/services/field_stats', () => ({ loadFieldStats: jest.fn().mockResolvedValue({}), })); From ce26c85887f97ede99e1ab7caa8e5af63d967037 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 12 Aug 2022 14:52:42 +0200 Subject: [PATCH 32/92] [UnifiedFieldList] Fix types --- src/plugins/unified_field_list/public/services/field_stats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/unified_field_list/public/services/field_stats.ts b/src/plugins/unified_field_list/public/services/field_stats.ts index cfd3798126594..d259c868a3857 100644 --- a/src/plugins/unified_field_list/public/services/field_stats.ts +++ b/src/plugins/unified_field_list/public/services/field_stats.ts @@ -10,7 +10,7 @@ import { lastValueFrom } from 'rxjs'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewFieldBase } from '@kbn/es-query'; -import type { FieldStatsResponse } from '../types'; +import type { FieldStatsResponse } from '../../common/types'; import { fetchAndCalculateFieldStats, SearchHandler, From e6b9a1c81307ef2d89a1609c59d9d7c928a0f45c Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 12 Aug 2022 16:03:47 +0200 Subject: [PATCH 33/92] [UnifiedFieldList] Fix references --- test/api_integration/apis/index.ts | 1 + .../plugins/lens/public/indexpattern_datasource/field_item.tsx | 3 ++- x-pack/test/api_integration/apis/index.ts | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/api_integration/apis/index.ts b/test/api_integration/apis/index.ts index f7801f4d42e71..3b8a182308e77 100644 --- a/test/api_integration/apis/index.ts +++ b/test/api_integration/apis/index.ts @@ -28,6 +28,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./stats')); loadTestFile(require.resolve('./ui_metric')); loadTestFile(require.resolve('./ui_counters')); + loadTestFile(require.resolve('./unified_field_list')); loadTestFile(require.resolve('./telemetry')); }); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 5b47b2ac0dd2f..85dba450cc5fe 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -33,7 +33,8 @@ import { FieldStats } from '@kbn/unified-field-list-plugin/public'; import { DragDrop, DragDropIdentifier } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; import { DOCUMENT_FIELD_NAME } from '../../common'; -import type { IndexPattern, IndexPatternField, DraggedField } from './types'; +import type { IndexPattern, IndexPatternField } from '../types'; +import type { DraggedField } from './types'; import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { VisualizeGeoFieldButton } from './visualize_geo_field_button'; import { getVisualizeGeoFieldMessage } from '../utils'; diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index e5ae694c1de31..5f99067833afd 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -25,7 +25,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./maps')); loadTestFile(require.resolve('./security_solution')); loadTestFile(require.resolve('./lens')); - loadTestFile(require.resolve('./unified_field_list')); loadTestFile(require.resolve('./transform')); loadTestFile(require.resolve('./lists')); loadTestFile(require.resolve('./upgrade_assistant')); From 51c852d8d5aa7a523bf9bcdd5bf338db491cdb59 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 12 Aug 2022 16:26:27 +0200 Subject: [PATCH 34/92] [UnifiedFieldList] Use emotion css --- .../components/field_stats/field_stats.scss | 16 --------- .../components/field_stats/field_stats.tsx | 35 +++++++++++++++---- 2 files changed, 29 insertions(+), 22 deletions(-) delete mode 100644 src/plugins/unified_field_list/public/components/field_stats/field_stats.scss diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.scss b/src/plugins/unified_field_list/public/components/field_stats/field_stats.scss deleted file mode 100644 index 3c96c78935d4e..0000000000000 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.scss +++ /dev/null @@ -1,16 +0,0 @@ -.unifiedFieldList__fieldStats__topValue { - margin-bottom: $euiSizeS; - - &:last-of-type { - margin-bottom: 0; - } -} - -.unifiedFieldList__fieldStats__topValueProgress { - background-color: $euiColorLightestShade; - - // sass-lint:disable-block no-vendor-prefixes - &::-webkit-progress-bar { - background-color: $euiColorLightestShade; - } -} diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 7e637f5baa4a8..05b5f99f1897a 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DataView, DataViewField, @@ -25,7 +25,9 @@ import { EuiText, EuiTitle, EuiToolTip, + useEuiTheme, } from '@elastic/eui'; +import { css } from '@emotion/react'; import { Axis, Chart, @@ -41,7 +43,6 @@ import { buildEsQuery, Query, Filter, AggregateQuery } from '@kbn/es-query'; import type { BucketedAggregation } from '../../../common/types'; import { loadFieldStats, canProvideStatsForField } from '../../services'; import { useUnifiedFieldListServices } from '../../hooks/use_unified_field_list_services'; -import './field_stats.scss'; interface State { isLoading: boolean; @@ -79,6 +80,7 @@ const FieldStatsComponent: React.FC = ({ overrideMissingContent, overrideFooter, }) => { + const { euiTheme } = useEuiTheme(); const services = useUnifiedFieldListServices(); const { fieldFormats, uiSettings, charts, dataViews, data } = services; const [state, changeState] = useState({ @@ -88,6 +90,28 @@ const FieldStatsComponent: React.FC = ({ const abortControllerRef = useRef(null); const isCanceledRef = useRef(false); + const topValueStyles = useMemo( + () => css` + margin-bottom: ${euiTheme.size.s}; + + &:last-of-type { + margin-bottom: 0; + } + `, + [euiTheme] + ); + + const topValueProgressStyles = useMemo( + () => css` + background-color: ${euiTheme.colors.lightestShade}; + + &::-webkit-progress-bar { + background-color: ${euiTheme.colors.lightestShade}; + } + `, + [euiTheme] + ); + const setState: typeof changeState = useCallback( (nextState) => { if (!isCanceledRef.current) { @@ -421,8 +445,7 @@ const FieldStatsComponent: React.FC = ({ {topValues.buckets.map((topValue) => { const formatted = formatter.convert(topValue.key); return ( - // TODO: convert styles to `css` prop -
+
= ({ = ({ Date: Fri, 12 Aug 2022 16:28:46 +0200 Subject: [PATCH 35/92] [UnifiedFieldList] Increase limits --- packages/kbn-optimizer/limits.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 5fd9e94573692..b0a7563308247 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -126,7 +126,7 @@ pageLoadAssetSize: cloudSecurityPosture: 19109 visTypeGauge: 24113 unifiedSearch: 71059 - unifiedFieldList: 36000 + unifiedFieldList: 65500 data: 454087 eventAnnotation: 19334 screenshotting: 22870 From 7a5f235074545eb490fc6b2603cb4780633045af Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 15 Aug 2022 10:18:23 +0200 Subject: [PATCH 36/92] [UnifiedFieldList] Add first tests --- .../field_stats/field_stats.test.tsx | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx new file mode 100644 index 0000000000000..49ea1f7873f3e --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -0,0 +1,215 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { coreMock } from '@kbn/core/public/mocks'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; +import { loadFieldStats } from '../../services'; +import { FieldStats, FieldStatsProps } from './field_stats'; + +jest.mock('../../services/field_stats', () => ({ + loadFieldStats: jest.fn().mockResolvedValue({}), +})); + +const mockedServices = { + data: dataPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), + charts: chartPluginMock.createSetupContract(), + uiSettings: coreMock.createStart().uiSettings, +}; + +const FieldStatsWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +describe('UnifiedFieldList ', () => { + let defaultProps: FieldStatsProps; + let dataView: DataView; + + beforeEach(() => { + dataView = { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + fields: [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytesLabel', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'unsupported', + displayName: 'unsupported', + type: 'geo', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'ip_range', + displayName: 'ip_range', + type: 'ip_range', + aggregatable: true, + searchable: true, + }, + ], + getFormatterForField: jest.fn(() => ({ + convert: jest.fn((s: unknown) => JSON.stringify(s)), + })), + } as unknown as DataView; + + defaultProps = { + dataViewOrDataViewId: dataView, + field: { + name: 'bytes', + type: 'number', + } as unknown as DataViewField, + fromDate: 'now-7d', + toDate: 'now', + query: { query: '', language: 'lucene' }, + filters: [], + testSubject: 'testing', + }; + + (mockedServices.dataViews.get as jest.Mock).mockImplementation(() => { + return Promise.resolve(dataView); + }); + }); + + it('should request field stats with correct params', async () => { + let resolveFunction: (arg: unknown) => void; + + (loadFieldStats as jest.Mock).mockImplementation(() => { + return new Promise((resolve) => { + resolveFunction = resolve; + }); + }); + + const wrapper = mountWithIntl( + + ); + + await wrapper.update(); + + expect(loadFieldStats).toHaveBeenCalledWith({ + abortController: new AbortController(), + data: mockedServices.data, + dataView, + dslQuery: { + bool: { + must: [], + filter: [ + { + bool: { + should: [{ match_phrase: { 'geo.src': 'US' } }], + minimum_should_match: 1, + }, + }, + { + match: { phrase: { 'geo.dest': 'US' } }, + }, + ], + should: [], + must_not: [], + }, + }, + fromDate: 'now-14d', + toDate: 'now-7d', + field: defaultProps.field, + }); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); + + await act(async () => { + resolveFunction!({ + totalDocuments: 4633, + sampledDocuments: 4633, + sampledValues: 4633, + histogram: { + buckets: [{ count: 705, key: 0 }], + }, + topValues: { + buckets: [{ count: 147, key: 0 }], + }, + }); + }); + + await wrapper.update(); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + + expect(loadFieldStats).toHaveBeenCalledTimes(1); + + (loadFieldStats as jest.Mock).mockReset(); + (loadFieldStats as jest.Mock).mockImplementation(() => Promise.resolve({})); + }); + + it('should not request field stats for range fields', async () => { + mountWithIntl( + + ); + + expect(loadFieldStats).not.toHaveBeenCalled(); + }); +}); From 9af3496c7fdce666ee20779c0f53794eabf6da61 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 15 Aug 2022 15:11:08 +0200 Subject: [PATCH 37/92] [UnifiedFieldList] Add more tests --- .../field_stats/field_stats.test.tsx | 339 +++++++++++++++++- .../components/field_stats/field_stats.tsx | 14 +- 2 files changed, 344 insertions(+), 9 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx index 49ea1f7873f3e..c6d1bcda65d5e 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiLoadingSpinner, EuiProgress } from '@elastic/eui'; import { coreMock } from '@kbn/core/public/mocks'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; @@ -93,6 +93,13 @@ describe('UnifiedFieldList ', () => { aggregatable: true, searchable: true, }, + { + name: 'machine.ram', + displayName: 'machine.ram', + type: 'number', + aggregatable: true, + searchable: true, + }, ], getFormatterForField: jest.fn(() => ({ convert: jest.fn((s: unknown) => JSON.stringify(s)), @@ -117,6 +124,11 @@ describe('UnifiedFieldList ', () => { }); }); + beforeEach(() => { + (loadFieldStats as jest.Mock).mockReset(); + (loadFieldStats as jest.Mock).mockImplementation(() => Promise.resolve({})); + }); + it('should request field stats with correct params', async () => { let resolveFunction: (arg: unknown) => void; @@ -191,9 +203,6 @@ describe('UnifiedFieldList ', () => { expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); expect(loadFieldStats).toHaveBeenCalledTimes(1); - - (loadFieldStats as jest.Mock).mockReset(); - (loadFieldStats as jest.Mock).mockImplementation(() => Promise.resolve({})); }); it('should not request field stats for range fields', async () => { @@ -212,4 +221,326 @@ describe('UnifiedFieldList ', () => { expect(loadFieldStats).not.toHaveBeenCalled(); }); + + it('should not request field stats for geo fields', async () => { + mountWithIntl( + + ); + + expect(loadFieldStats).not.toHaveBeenCalled(); + }); + + it('should render nothing if no data is found', async () => { + const wrapper = mountWithIntl(); + + await wrapper.update(); + + expect(loadFieldStats).toHaveBeenCalled(); + + expect(wrapper.text()).toBe(''); + }); + + it('should render Top Values field stats correctly for a keyword field', async () => { + let resolveFunction: (arg: unknown) => void; + + (loadFieldStats as jest.Mock).mockImplementation(() => { + return new Promise((resolve) => { + resolveFunction = resolve; + }); + }); + + const wrapper = mountWithIntl( + + ); + + await wrapper.update(); + + expect(loadFieldStats).toHaveBeenCalledWith({ + abortController: new AbortController(), + data: mockedServices.data, + dataView, + fromDate: 'now-7d', + toDate: 'now', + dslQuery: { + bool: { + must: [], + filter: [], + should: [], + must_not: [], + }, + }, + field: defaultProps.field, + }); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); + + await act(async () => { + resolveFunction!({ + totalDocuments: 1624, + sampledDocuments: 1624, + sampledValues: 3248, + topValues: { + buckets: [ + { + count: 1349, + key: 'success', + }, + { + count: 1206, + key: 'info', + }, + { + count: 329, + key: 'security', + }, + { + count: 164, + key: 'warning', + }, + { + count: 111, + key: 'error', + }, + { + count: 89, + key: 'login', + }, + ], + }, + }); + }); + + await wrapper.update(); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(EuiProgress)).toHaveLength(6); + + expect(loadFieldStats).toHaveBeenCalledTimes(1); + + const stats = wrapper.find('[data-test-subj="testing-topValues"]'); + const firstValue = stats.childAt(0); + + expect(stats).toHaveLength(1); + expect(firstValue.find('[data-test-subj="testing-topValues-value"]').first().text()).toBe( + '"success"' + ); + expect(firstValue.find('[data-test-subj="testing-topValues-valueCount"]').first().text()).toBe( + '41.5%' + ); + + expect(wrapper.find('[data-test-subj="testing-statsFooter"]').first().text()).toBe( + '100% of 1624 documents' + ); + + expect(wrapper.text()).toBe( + 'Top values"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%100% of 1624 documents' + ); + }); + + it('should render Histogram field stats correctly for a date field', async () => { + let resolveFunction: (arg: unknown) => void; + + (loadFieldStats as jest.Mock).mockImplementation(() => { + return new Promise((resolve) => { + resolveFunction = resolve; + }); + }); + + const wrapper = mountWithIntl( + + ); + + await wrapper.update(); + + expect(loadFieldStats).toHaveBeenCalledWith({ + abortController: new AbortController(), + data: mockedServices.data, + dataView, + fromDate: 'now-1h', + toDate: 'now', + dslQuery: { + bool: { + must: [], + filter: [], + should: [], + must_not: [], + }, + }, + field: dataView.fields[0], + }); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); + + await act(async () => { + resolveFunction!({ + totalDocuments: 13, + histogram: { + buckets: [ + { + count: 1, + key: 1660564080000, + }, + { + count: 2, + key: 1660564440000, + }, + { + count: 3, + key: 1660564800000, + }, + { + count: 1, + key: 1660565160000, + }, + { + count: 2, + key: 1660565520000, + }, + { + count: 0, + key: 1660565880000, + }, + { + count: 1, + key: 1660566240000, + }, + { + count: 1, + key: 1660566600000, + }, + { + count: 1, + key: 1660566960000, + }, + { + count: 1, + key: 1660567320000, + }, + ], + }, + }); + }); + + await wrapper.update(); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + + expect(loadFieldStats).toHaveBeenCalledTimes(1); + + expect(wrapper.find('[data-test-subj="testing-topValues"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="testing-histogram"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="testing-statsFooter"]').first().text()).toBe( + '13 documents' + ); + + expect(wrapper.text()).toBe('Time distribution13 documents'); + }); + + it('should render Top Values & Distribution field stats correctly for a number field', async () => { + let resolveFunction: (arg: unknown) => void; + + (loadFieldStats as jest.Mock).mockImplementation(() => { + return new Promise((resolve) => { + resolveFunction = resolve; + }); + }); + + const field = dataView.fields.find((f) => f.name === 'machine.ram')!; + + const wrapper = mountWithIntl( + + ); + + await wrapper.update(); + + expect(loadFieldStats).toHaveBeenCalledWith({ + abortController: new AbortController(), + data: mockedServices.data, + dataView, + fromDate: 'now-1h', + toDate: 'now', + dslQuery: { + bool: { + must: [], + filter: [], + should: [], + must_not: [], + }, + }, + field, + }); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); + + await act(async () => { + resolveFunction!({ + totalDocuments: 23, + sampledDocuments: 23, + sampledValues: 23, + histogram: { + buckets: [ + { + count: 17, + key: 12, + }, + { + count: 6, + key: 13, + }, + ], + }, + topValues: { + buckets: [ + { + count: 17, + key: 12, + }, + { + count: 6, + key: 13, + }, + ], + }, + }); + }); + + await wrapper.update(); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + + expect(loadFieldStats).toHaveBeenCalledTimes(1); + + expect(wrapper.text()).toBe( + 'Toggle either theTop valuesDistribution1273.9%1326.1%100% of 23 documents' + ); + }); }); diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 05b5f99f1897a..bd2ae4b39b8f5 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -313,7 +313,7 @@ const FieldStatsComponent: React.FC = ({ function combineWithTitleAndFooter(el: React.ReactElement) { const countsElement = totalDocuments ? ( - + {sampledDocuments && ( <> {i18n.translate('unifiedFieldList.fieldStats.percentageOfLabel', { @@ -321,9 +321,9 @@ const FieldStatsComponent: React.FC = ({ values: { percentage: Math.round((sampledDocuments / totalDocuments) * 100), }, - })} + })}{' '} - )}{' '} + )} {fieldFormats .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) @@ -452,7 +452,11 @@ const FieldStatsComponent: React.FC = ({ gutterSize="xs" responsive={false} > - + {formatted === '' ? ( @@ -469,7 +473,7 @@ const FieldStatsComponent: React.FC = ({ )} - + {(Math.round((topValue.count / sampledValues!) * 1000) / 10).toFixed( digitsRequired ? 1 : 0 From 6cebb323a5ac6cf4b82875832c0b2883ef85c695 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 16 Aug 2022 09:50:37 +0200 Subject: [PATCH 38/92] [UnifiedFieldList] Refactor interface to accept services object --- .../components/sidebar/discover_field.tsx | 1 + src/plugins/unified_field_list/kibana.json | 2 +- .../field_stats/field_stats.test.tsx | 32 +++++++------------ .../components/field_stats/field_stats.tsx | 24 ++++++++++++-- .../field_stats/field_stats_from_hits.tsx | 17 ---------- .../public/components/field_stats/index.tsx | 5 +-- .../hooks/use_unified_field_list_services.tsx | 14 -------- .../unified_field_list/public/index.ts | 4 +-- .../public/services/field_stats.ts | 19 +++++++++-- .../unified_field_list/public/types.ts | 15 --------- .../plugins/lens/public/app_plugin/types.ts | 2 ++ .../field_item.test.tsx | 4 +-- .../indexpattern_datasource/field_item.tsx | 7 ++-- .../operations/definitions/terms/helpers.ts | 2 +- 14 files changed, 65 insertions(+), 83 deletions(-) delete mode 100755 src/plugins/unified_field_list/public/components/field_stats/field_stats_from_hits.tsx delete mode 100755 src/plugins/unified_field_list/public/hooks/use_unified_field_list_services.tsx diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 2f14164ee95cc..5ff31e52bcab6 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -425,6 +425,7 @@ function DiscoverFieldComponent({ {Boolean(dateRange) && ( = (props) => { - return ( - - - - ); -}; - describe('UnifiedFieldList ', () => { let defaultProps: FieldStatsProps; let dataView: DataView; @@ -107,6 +98,7 @@ describe('UnifiedFieldList ', () => { } as unknown as DataView; defaultProps = { + services: mockedServices, dataViewOrDataViewId: dataView, field: { name: 'bytes', @@ -139,7 +131,7 @@ describe('UnifiedFieldList ', () => { }); const wrapper = mountWithIntl( - ', () => { expect(loadFieldStats).toHaveBeenCalledWith({ abortController: new AbortController(), - data: mockedServices.data, + services: { data: mockedServices.data }, dataView, dslQuery: { bool: { @@ -207,7 +199,7 @@ describe('UnifiedFieldList ', () => { it('should not request field stats for range fields', async () => { mountWithIntl( - ', () => { it('should not request field stats for geo fields', async () => { mountWithIntl( - ', () => { }); it('should render nothing if no data is found', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); await wrapper.update(); @@ -259,7 +251,7 @@ describe('UnifiedFieldList ', () => { }); const wrapper = mountWithIntl( - ', () => { expect(loadFieldStats).toHaveBeenCalledWith({ abortController: new AbortController(), - data: mockedServices.data, + services: { data: mockedServices.data }, dataView, fromDate: 'now-7d', toDate: 'now', @@ -362,7 +354,7 @@ describe('UnifiedFieldList ', () => { }); const wrapper = mountWithIntl( - ', () => { expect(loadFieldStats).toHaveBeenCalledWith({ abortController: new AbortController(), - data: mockedServices.data, + services: { data: mockedServices.data }, dataView, fromDate: 'now-1h', toDate: 'now', @@ -470,7 +462,7 @@ describe('UnifiedFieldList ', () => { const field = dataView.fields.find((f) => f.name === 'machine.ram')!; const wrapper = mountWithIntl( - ', () => { expect(loadFieldStats).toHaveBeenCalledWith({ abortController: new AbortController(), - data: mockedServices.data, + services: { data: mockedServices.data }, dataView, fromDate: 'now-1h', toDate: 'now', diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index bd2ae4b39b8f5..a84da946d96bf 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -14,6 +14,11 @@ import { getEsQueryConfig, KBN_FIELD_TYPES, } from '@kbn/data-plugin/common'; +import type { IUiSettingsClient } from '@kbn/core/public'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import DateMath from '@kbn/datemath'; import { EuiButtonGroup, @@ -42,7 +47,6 @@ import { i18n } from '@kbn/i18n'; import { buildEsQuery, Query, Filter, AggregateQuery } from '@kbn/es-query'; import type { BucketedAggregation } from '../../../common/types'; import { loadFieldStats, canProvideStatsForField } from '../../services'; -import { useUnifiedFieldListServices } from '../../hooks/use_unified_field_list_services'; interface State { isLoading: boolean; @@ -53,7 +57,16 @@ interface State { topValues?: BucketedAggregation; } +export interface FieldStatsServices { + uiSettings: IUiSettingsClient; + dataViews: DataViewsContract; + data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; + charts: ChartsPluginSetup; +} + export interface FieldStatsProps { + services: FieldStatsServices; query: Query | AggregateQuery; filters: Filter[]; fromDate: string; @@ -70,6 +83,7 @@ export interface FieldStatsProps { } const FieldStatsComponent: React.FC = ({ + services, query, filters, fromDate, @@ -81,7 +95,6 @@ const FieldStatsComponent: React.FC = ({ overrideFooter, }) => { const { euiTheme } = useEuiTheme(); - const services = useUnifiedFieldListServices(); const { fieldFormats, uiSettings, charts, dataViews, data } = services; const [state, changeState] = useState({ isLoading: false, @@ -153,7 +166,7 @@ const FieldStatsComponent: React.FC = ({ abortControllerRef.current = new AbortController(); const results = await loadFieldStats({ - data, + services: { data }, dataView: loadedDataView, field, fromDate, @@ -554,6 +567,11 @@ class ErrorBoundary extends React.Component<{}, { hasError: boolean }> { } } +/** + * Component which fetches and renders stats for a data view field + * @param props + * @constructor + */ export const FieldStats: React.FC = (props) => { return ( diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_hits.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_hits.tsx deleted file mode 100755 index 02622e751e127..0000000000000 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats_from_hits.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { EuiText } from '@elastic/eui'; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface FieldStatsFromHitsProps {} - -export const FieldStatsFromHits: React.FC = () => { - return {'TODO: move current field stats from Discover to this component'}; -}; diff --git a/src/plugins/unified_field_list/public/components/field_stats/index.tsx b/src/plugins/unified_field_list/public/components/field_stats/index.tsx index 90313e6860fea..0a4fd9e2d5542 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/index.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/index.tsx @@ -6,8 +6,5 @@ * Side Public License, v 1. */ -export type { FieldStatsProps } from './field_stats'; +export type { FieldStatsProps, FieldStatsServices } from './field_stats'; export { FieldStats } from './field_stats'; - -export type { FieldStatsFromHitsProps } from './field_stats_from_hits'; -export { FieldStatsFromHits } from './field_stats_from_hits'; diff --git a/src/plugins/unified_field_list/public/hooks/use_unified_field_list_services.tsx b/src/plugins/unified_field_list/public/hooks/use_unified_field_list_services.tsx deleted file mode 100755 index e372e13908c64..0000000000000 --- a/src/plugins/unified_field_list/public/hooks/use_unified_field_list_services.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { UnifiedFieldListServices } from '../types'; - -export const useUnifiedFieldListServices = (): UnifiedFieldListServices => { - return useKibana().services; -}; diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts index 537f797e2c39c..a2b6e9c874535 100755 --- a/src/plugins/unified_field_list/public/index.ts +++ b/src/plugins/unified_field_list/public/index.ts @@ -14,8 +14,8 @@ export type { NumberStatsResult, TopValuesResult, } from '../common/types'; -export type { FieldStatsProps, FieldStatsFromHitsProps } from './components/field_stats'; -export { FieldStats, FieldStatsFromHits } from './components/field_stats'; +export type { FieldStatsProps, FieldStatsServices } from './components/field_stats'; +export { FieldStats } from './components/field_stats'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. diff --git a/src/plugins/unified_field_list/public/services/field_stats.ts b/src/plugins/unified_field_list/public/services/field_stats.ts index d259c868a3857..91a28e4feda25 100644 --- a/src/plugins/unified_field_list/public/services/field_stats.ts +++ b/src/plugins/unified_field_list/public/services/field_stats.ts @@ -18,7 +18,9 @@ import { } from '../../common/utils/field_stats_utils'; interface FetchFieldStatsParams { - data: DataPublicPluginStart; + services: { + data: DataPublicPluginStart; + }; dataView: DataView; field: DataViewFieldBase; fromDate: string; @@ -28,8 +30,19 @@ interface FetchFieldStatsParams { abortController?: AbortController; } +/** + * Loads and aggregates stats data for a data view field + * @param services + * @param dataView + * @param field + * @param fromDate + * @param toDate + * @param dslQuery + * @param size + * @param abortController + */ export const loadFieldStats = async ({ - data, + services, dataView, field, fromDate, @@ -38,6 +51,8 @@ export const loadFieldStats = async ({ size, abortController, }: FetchFieldStatsParams): Promise> => { + const { data } = services; + try { if (!dataView?.id || !field?.type) { return {}; diff --git a/src/plugins/unified_field_list/public/types.ts b/src/plugins/unified_field_list/public/types.ts index b7ea064952c75..feb24509cdee5 100755 --- a/src/plugins/unified_field_list/public/types.ts +++ b/src/plugins/unified_field_list/public/types.ts @@ -6,23 +6,8 @@ * Side Public License, v 1. */ -import { HttpStart, IUiSettingsClient } from '@kbn/core/public'; -import { DataViewsContract } from '@kbn/data-views-plugin/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; - // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface UnifiedFieldListPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface UnifiedFieldListPluginStart {} - -export interface UnifiedFieldListServices { - http: HttpStart; - uiSettings: IUiSettingsClient; - dataViews: DataViewsContract; - data: DataPublicPluginStart; - fieldFormats: FieldFormatsStart; - charts: ChartsPluginSetup; -} diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 6a2718fc49820..be49ca768acdc 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -37,6 +37,7 @@ import { ACTION_CONVERT_TO_LENS } from '@kbn/visualizations-plugin/public'; import type { EmbeddableEditorState, EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { DatasourceMap, EditorFrameInstance, @@ -140,6 +141,7 @@ export interface LensAppServices { getOriginatingAppName: () => string | undefined; presentationUtil: PresentationUtilPluginStart; spaces: SpacesApi; + charts: ChartsPluginSetup; discover?: DiscoverStart; // Temporarily required until the 'by value' paradigm is default. diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 0e1dfd13c6bd8..615279b71b82c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -216,7 +216,7 @@ describe('IndexPattern Field Item', () => { expect(loadFieldStats).toHaveBeenCalledWith({ abortController: new AbortController(), - data: mockedServices.data, + services: { data: mockedServices.data }, dataView, dslQuery: { bool: { @@ -291,7 +291,7 @@ describe('IndexPattern Field Item', () => { expect(loadFieldStats).toHaveBeenCalledTimes(2); expect(loadFieldStats).toHaveBeenLastCalledWith({ abortController: new AbortController(), - data: mockedServices.data, + services: { data: mockedServices.data }, dataView, dslQuery: { bool: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 85dba450cc5fe..d18a0ed7bbc42 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -22,6 +22,7 @@ import { EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { FieldButton } from '@kbn/react-field'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { EuiHighlight } from '@elastic/eui'; @@ -38,7 +39,7 @@ import type { DraggedField } from './types'; import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { VisualizeGeoFieldButton } from './visualize_geo_field_button'; import { getVisualizeGeoFieldMessage } from '../utils'; - +import type { LensAppServices } from '../app_plugin/types'; import { debouncedComponent } from '../debounced_component'; export interface FieldItemProps { @@ -318,6 +319,7 @@ function FieldItemPopoverContents(props: FieldItemProps) { uiActions, core, } = props; + const services = useKibana().services; const panelHeader = ( {panelHeader} {element}} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts index d4059ae00e0d0..6dec5e3aef6cf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts @@ -140,7 +140,7 @@ export function getDisallowedTermsMessage( if (fieldNames.length === 1) { const currentDataView = await data.dataViews.get(indexPattern.id); const response: FieldStatsResponse = await loadFieldStats({ - data, + services: { data }, dataView: currentDataView, field: indexPattern.getFieldByName(fieldNames[0])!, dslQuery: buildEsQuery( From c5df448a1abe185eb44e67e6c3bc6569ce7d50b2 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 16 Aug 2022 09:52:04 +0200 Subject: [PATCH 39/92] [UnifiedFieldList] Update types --- .../unified_field_list/common/utils/field_stats_utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index 33aa80323c22f..9cf33961d76b6 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -14,7 +14,7 @@ import type { FieldStatsResponse } from '../types'; export type SearchHandler = ( aggs: Record -) => Promise>; +) => Promise>; const SHARD_SIZE = 5000; const DEFAULT_TOP_VALUES_SIZE = 10; @@ -146,8 +146,8 @@ export async function getNumberHistogram( const minMaxResult = (await aggSearchWithBody( useTopHits ? searchWithHits : searchWithoutHits )) as - | ESSearchResponse - | ESSearchResponse; + | ESSearchResponse + | ESSearchResponse; const minValue = minMaxResult.aggregations!.sample.min_value.value; const maxValue = minMaxResult.aggregations!.sample.max_value.value; From 3f90cadbd776b0d7803fb5685e49a56ec44c97b2 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 16 Aug 2022 10:05:47 +0200 Subject: [PATCH 40/92] [UnifiedFieldList] Add docs --- docs/developer/plugin-list.asciidoc | 2 +- src/plugins/unified_field_list/README.md | 14 +++++++++++++- src/plugins/unified_field_list/kibana.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 2be1efbea2c41..3461f25d6871a 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -304,7 +304,7 @@ In general this plugin provides: |{kib-repo}blob/{branch}/src/plugins/unified_field_list/README.md[unifiedFieldList] -|A Kibana plugin +|Contains components and services for field list UI (as in fields sidebar on Discover and Lens pages). |{kib-repo}blob/{branch}/src/plugins/unified_search/README.md[unifiedSearch] diff --git a/src/plugins/unified_field_list/README.md b/src/plugins/unified_field_list/README.md index 86e3ec9700b9b..657523aad9c1b 100755 --- a/src/plugins/unified_field_list/README.md +++ b/src/plugins/unified_field_list/README.md @@ -1,9 +1,21 @@ # unifiedFieldList -A Kibana plugin +This Kibana plugin contains components and services for field list UI (as in fields sidebar on Discover and Lens pages). --- +## Components + +* `` - loads and renders stats (Top values, Histogram) for a data view field. + +## Public Services + +* `loadStats(...)` - returns the loaded field stats (can also work with Ad-hoc data views) + +## Server APIs + +* `/api/unified_field_list/field_stats` - returns the loaded field stats (except for Ad-hoc data views) + ## Development See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/src/plugins/unified_field_list/kibana.json b/src/plugins/unified_field_list/kibana.json index 08297f8ae011a..6785d55989cc5 100755 --- a/src/plugins/unified_field_list/kibana.json +++ b/src/plugins/unified_field_list/kibana.json @@ -6,7 +6,7 @@ "name": "Data Discovery", "githubTeam": "kibana-data-discovery" }, - "description": "Contains functionality for the field list which can be integrated into apps sidebar", + "description": "Contains functionality for the field list which can be integrated into apps", "server": true, "ui": true, "requiredPlugins": ["dataViews", "data", "fieldFormats", "charts"], From 3a318aa7c1e9fcaa980ff2d8347e6b8f11a01ba1 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 16 Aug 2022 08:15:04 +0000 Subject: [PATCH 41/92] [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs' --- docs/developer/plugin-list.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 3461f25d6871a..eedb5a4db3e40 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -304,7 +304,7 @@ In general this plugin provides: |{kib-repo}blob/{branch}/src/plugins/unified_field_list/README.md[unifiedFieldList] -|Contains components and services for field list UI (as in fields sidebar on Discover and Lens pages). +|This Kibana plugin contains components and services for field list UI (as in fields sidebar on Discover and Lens pages). |{kib-repo}blob/{branch}/src/plugins/unified_search/README.md[unifiedSearch] From 1d5c2f966c0ca4ee759cbae5273dc1549fcee233 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 16 Aug 2022 10:15:09 +0200 Subject: [PATCH 42/92] [UnifiedFieldList] Add missing references --- x-pack/plugins/lens/public/app_plugin/mounter.tsx | 1 + x-pack/plugins/lens/public/mocks/services_mock.tsx | 2 ++ 2 files changed, 3 insertions(+) diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 34a06439473a3..8c73d826724ea 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -96,6 +96,7 @@ export async function getLensServices( dataViewEditor: startDependencies.dataViewEditor, dataViewFieldEditor: startDependencies.dataViewFieldEditor, dashboard: startDependencies.dashboard, + charts: startDependencies.charts, getOriginatingAppName: () => { return embeddableEditorIncomingState?.originatingApp ? stateTransfer?.getAppNameFromId(embeddableEditorIncomingState.originatingApp) diff --git a/x-pack/plugins/lens/public/mocks/services_mock.tsx b/x-pack/plugins/lens/public/mocks/services_mock.tsx index e9d0c7cd14f28..8c323e7e071fa 100644 --- a/x-pack/plugins/lens/public/mocks/services_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/services_mock.tsx @@ -16,6 +16,7 @@ import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks'; import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks'; import { dashboardPluginMock } from '@kbn/dashboard-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { @@ -158,6 +159,7 @@ export function makeDefaultServices( clear: jest.fn(), }, spaces: spacesPluginMock.createStartContract(), + charts: chartPluginMock.createSetupContract(), dataViewFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), dataViewEditor: indexPatternEditorPluginMock.createStartContract(), }; From 871016ae6df003b8fe107a425bb5428bfd6d8599 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 16 Aug 2022 11:24:26 +0200 Subject: [PATCH 43/92] [UnifiedFieldList] Tmp --- .../application/main/components/sidebar/discover_field.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 5ff31e52bcab6..e5a854d39e99c 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -411,7 +411,7 @@ function DiscoverFieldComponent({ const details = getDetails(field); const dateRange = data?.query?.timefilter.timefilter.getTime(); const fieldForStats = multiFields ? multiFields[0].field : field; // TODO: how to handle multifields? - const showNewStatsPreviewInDiscover = false; // Toggle this variable to preview new stats locally + const showNewStatsPreviewInDiscover = true; // Toggle this variable to preview new stats locally return ( <> From 3f4ae6e3959e90528ac2be28d00d71904cf9eb25 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 16 Aug 2022 11:41:19 +0200 Subject: [PATCH 44/92] [UnifiedFieldList] Revert changes from Discover for now --- src/plugins/discover/kibana.json | 3 +- .../sidebar/discover_field.test.tsx | 1 - .../components/sidebar/discover_field.tsx | 56 +------------------ .../components/sidebar/discover_sidebar.tsx | 4 -- src/plugins/discover/public/build_services.ts | 2 - src/plugins/discover/tsconfig.json | 1 - 6 files changed, 4 insertions(+), 63 deletions(-) diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 23bf678392ac3..78e5265bcb3ac 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -15,8 +15,7 @@ "savedObjects", "dataViewFieldEditor", "dataViewEditor", - "expressions", - "unifiedFieldList" + "expressions" ], "optionalPlugins": ["home", "share", "usageCollection", "spaces", "triggersActionsUi"], "requiredBundles": ["kibanaUtils", "kibanaReact", "dataViews", "unifiedSearch", "savedSearch"], diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx index 87f301c662df4..45129d171cb33 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx @@ -56,7 +56,6 @@ function getComponent({ onRemoveField: jest.fn(), showDetails, selected, - state: {}, }; const services = { history: () => ({ diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index e5a854d39e99c..743e0005ec756 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -20,22 +20,18 @@ import { EuiFlexItem, EuiSpacer, EuiHorizontalRule, - EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UiCounterMetricType } from '@kbn/analytics'; import classNames from 'classnames'; import { FieldButton, FieldIcon } from '@kbn/react-field'; import type { DataViewField, DataView } from '@kbn/data-views-plugin/public'; -import { FieldStats } from '@kbn/unified-field-list-plugin/public'; import { getFieldCapabilities } from '../../../../utils/get_field_capabilities'; import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon'; import { DiscoverFieldDetails } from './discover_field_details'; import { FieldDetails } from './types'; import { getFieldTypeName } from '../../../../utils/get_field_type_name'; import { DiscoverFieldVisualize } from './discover_field_visualize'; -import type { AppState } from '../../services/discover_state'; -import { useDiscoverServices } from '../../../../hooks/use_discover_services'; function wrapOnDot(str?: string) { // u200B is a non-width white-space character, which allows @@ -267,11 +263,6 @@ export interface DiscoverFieldProps { * Optionally show or hide field stats in the popover */ showFieldStats?: boolean; - - /** - * Discover App State - */ - state: AppState; } function DiscoverFieldComponent({ @@ -288,10 +279,7 @@ function DiscoverFieldComponent({ onEditField, onDeleteField, showFieldStats, - state, }: DiscoverFieldProps) { - const services = useDiscoverServices(); - const { data } = services; const [infoIsOpen, setOpen] = useState(false); const isDocumentRecord = !!onAddFilter; @@ -409,51 +397,13 @@ function DiscoverFieldComponent({ const renderPopover = () => { const details = getDetails(field); - const dateRange = data?.query?.timefilter.timefilter.getTime(); - const fieldForStats = multiFields ? multiFields[0].field : field; // TODO: how to handle multifields? - const showNewStatsPreviewInDiscover = true; // Toggle this variable to preview new stats locally + + // TODO: integrate return ( <> {showFieldStats && ( <> - {showNewStatsPreviewInDiscover && ( - <> - - {'Stats as in Lens:'} - - - {Boolean(dateRange) && ( - { - if (params?.noDataFound) { - return ( - {`TODO: add a custom "no data available" message for ${fieldForStats.type} field`} - ); - } - - return ( - {`TODO: add a custom "stats are not available" message for ${fieldForStats.type} field`} - ); - }} - /> - )} - {/* TODO: remove previous field stats view when we finish FieldStats component and add addFilter buttons to it */} - - - {'Current Discover stats:'} - - - - )}
{i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { @@ -482,7 +432,7 @@ function DiscoverFieldComponent({ )} {(showFieldStats || multiFields) && } - (null); @@ -413,7 +412,6 @@ export function DiscoverSidebarComponent({ onEditField={editField} onDeleteField={deleteField} showFieldStats={showFieldStats} - state={state} /> ); @@ -474,7 +472,6 @@ export function DiscoverSidebarComponent({ onEditField={editField} onDeleteField={deleteField} showFieldStats={showFieldStats} - state={state} /> ); @@ -504,7 +501,6 @@ export function DiscoverSidebarComponent({ onEditField={editField} onDeleteField={deleteField} showFieldStats={showFieldStats} - state={state} /> ); diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 58baacf3fa7f1..3e83b149d351e 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -83,7 +83,6 @@ export interface DiscoverServices { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; locator: DiscoverAppLocator; expressions: ExpressionsStart; - charts: ChartsPluginStart; } export const buildServices = memoize(function ( @@ -129,6 +128,5 @@ export const buildServices = memoize(function ( triggersActionsUi: plugins.triggersActionsUi, locator, expressions: plugins.expressions, - charts: plugins.charts, }; }); diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 2b96fbc59eca5..efadcc88443a1 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -28,7 +28,6 @@ { "path": "../field_formats/tsconfig.json" }, { "path": "../data_views/tsconfig.json" }, { "path": "../unified_search/tsconfig.json" }, - { "path": "../unified_field_list/tsconfig.json" }, { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, { "path": "../data_view_editor/tsconfig.json" }, { "path": "../../../x-pack/plugins/triggers_actions_ui/tsconfig.json" } From 97797bd3a1da4624db10bcf2cd703401a93f7df2 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 16 Aug 2022 15:32:57 +0200 Subject: [PATCH 45/92] Revert "[UnifiedFieldList] Revert changes from Discover for now" This reverts commit 3f4ae6e3959e90528ac2be28d00d71904cf9eb25. --- src/plugins/discover/kibana.json | 3 +- .../sidebar/discover_field.test.tsx | 1 + .../components/sidebar/discover_field.tsx | 56 ++++++++++++++++++- .../components/sidebar/discover_sidebar.tsx | 4 ++ src/plugins/discover/public/build_services.ts | 2 + src/plugins/discover/tsconfig.json | 1 + 6 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 78e5265bcb3ac..23bf678392ac3 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -15,7 +15,8 @@ "savedObjects", "dataViewFieldEditor", "dataViewEditor", - "expressions" + "expressions", + "unifiedFieldList" ], "optionalPlugins": ["home", "share", "usageCollection", "spaces", "triggersActionsUi"], "requiredBundles": ["kibanaUtils", "kibanaReact", "dataViews", "unifiedSearch", "savedSearch"], diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx index 45129d171cb33..87f301c662df4 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx @@ -56,6 +56,7 @@ function getComponent({ onRemoveField: jest.fn(), showDetails, selected, + state: {}, }; const services = { history: () => ({ diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 743e0005ec756..e5a854d39e99c 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -20,18 +20,22 @@ import { EuiFlexItem, EuiSpacer, EuiHorizontalRule, + EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UiCounterMetricType } from '@kbn/analytics'; import classNames from 'classnames'; import { FieldButton, FieldIcon } from '@kbn/react-field'; import type { DataViewField, DataView } from '@kbn/data-views-plugin/public'; +import { FieldStats } from '@kbn/unified-field-list-plugin/public'; import { getFieldCapabilities } from '../../../../utils/get_field_capabilities'; import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon'; import { DiscoverFieldDetails } from './discover_field_details'; import { FieldDetails } from './types'; import { getFieldTypeName } from '../../../../utils/get_field_type_name'; import { DiscoverFieldVisualize } from './discover_field_visualize'; +import type { AppState } from '../../services/discover_state'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; function wrapOnDot(str?: string) { // u200B is a non-width white-space character, which allows @@ -263,6 +267,11 @@ export interface DiscoverFieldProps { * Optionally show or hide field stats in the popover */ showFieldStats?: boolean; + + /** + * Discover App State + */ + state: AppState; } function DiscoverFieldComponent({ @@ -279,7 +288,10 @@ function DiscoverFieldComponent({ onEditField, onDeleteField, showFieldStats, + state, }: DiscoverFieldProps) { + const services = useDiscoverServices(); + const { data } = services; const [infoIsOpen, setOpen] = useState(false); const isDocumentRecord = !!onAddFilter; @@ -397,13 +409,51 @@ function DiscoverFieldComponent({ const renderPopover = () => { const details = getDetails(field); - - // TODO: integrate + const dateRange = data?.query?.timefilter.timefilter.getTime(); + const fieldForStats = multiFields ? multiFields[0].field : field; // TODO: how to handle multifields? + const showNewStatsPreviewInDiscover = true; // Toggle this variable to preview new stats locally return ( <> {showFieldStats && ( <> + {showNewStatsPreviewInDiscover && ( + <> + + {'Stats as in Lens:'} + + + {Boolean(dateRange) && ( + { + if (params?.noDataFound) { + return ( + {`TODO: add a custom "no data available" message for ${fieldForStats.type} field`} + ); + } + + return ( + {`TODO: add a custom "stats are not available" message for ${fieldForStats.type} field`} + ); + }} + /> + )} + {/* TODO: remove previous field stats view when we finish FieldStats component and add addFilter buttons to it */} + + + {'Current Discover stats:'} + + + + )}
{i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { @@ -432,7 +482,7 @@ function DiscoverFieldComponent({ )} {(showFieldStats || multiFields) && } - (null); @@ -412,6 +413,7 @@ export function DiscoverSidebarComponent({ onEditField={editField} onDeleteField={deleteField} showFieldStats={showFieldStats} + state={state} /> ); @@ -472,6 +474,7 @@ export function DiscoverSidebarComponent({ onEditField={editField} onDeleteField={deleteField} showFieldStats={showFieldStats} + state={state} /> ); @@ -501,6 +504,7 @@ export function DiscoverSidebarComponent({ onEditField={editField} onDeleteField={deleteField} showFieldStats={showFieldStats} + state={state} /> ); diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 3e83b149d351e..58baacf3fa7f1 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -83,6 +83,7 @@ export interface DiscoverServices { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; locator: DiscoverAppLocator; expressions: ExpressionsStart; + charts: ChartsPluginStart; } export const buildServices = memoize(function ( @@ -128,5 +129,6 @@ export const buildServices = memoize(function ( triggersActionsUi: plugins.triggersActionsUi, locator, expressions: plugins.expressions, + charts: plugins.charts, }; }); diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index efadcc88443a1..2b96fbc59eca5 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -28,6 +28,7 @@ { "path": "../field_formats/tsconfig.json" }, { "path": "../data_views/tsconfig.json" }, { "path": "../unified_search/tsconfig.json" }, + { "path": "../unified_field_list/tsconfig.json" }, { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, { "path": "../data_view_editor/tsconfig.json" }, { "path": "../../../x-pack/plugins/triggers_actions_ui/tsconfig.json" } From 8306e29d7eb07c3aceffceaba80be56a3639469c Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 17 Aug 2022 10:29:10 +0200 Subject: [PATCH 46/92] [Discover] Extract top values UI into a separate component. Update colors. --- .../components/field_stats/field_stats.tsx | 135 ++--------------- .../field_stats/field_top_values.tsx | 143 ++++++++++++++++++ 2 files changed, 154 insertions(+), 124 deletions(-) create mode 100755 src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index a84da946d96bf..6da5e37b7815f 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { DataView, DataViewField, @@ -20,19 +20,7 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import DateMath from '@kbn/datemath'; -import { - EuiButtonGroup, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiProgress, - EuiSpacer, - EuiText, - EuiTitle, - EuiToolTip, - useEuiTheme, -} from '@elastic/eui'; -import { css } from '@emotion/react'; +import { EuiButtonGroup, EuiLoadingSpinner, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { Axis, Chart, @@ -47,6 +35,7 @@ import { i18n } from '@kbn/i18n'; import { buildEsQuery, Query, Filter, AggregateQuery } from '@kbn/es-query'; import type { BucketedAggregation } from '../../../common/types'; import { loadFieldStats, canProvideStatsForField } from '../../services'; +import { FieldTopValues } from './field_top_values'; interface State { isLoading: boolean; @@ -94,7 +83,6 @@ const FieldStatsComponent: React.FC = ({ overrideMissingContent, overrideFooter, }) => { - const { euiTheme } = useEuiTheme(); const { fieldFormats, uiSettings, charts, dataViews, data } = services; const [state, changeState] = useState({ isLoading: false, @@ -103,28 +91,6 @@ const FieldStatsComponent: React.FC = ({ const abortControllerRef = useRef(null); const isCanceledRef = useRef(false); - const topValueStyles = useMemo( - () => css` - margin-bottom: ${euiTheme.size.s}; - - &:last-of-type { - margin-bottom: 0; - } - `, - [euiTheme] - ); - - const topValueProgressStyles = useMemo( - () => css` - background-color: ${euiTheme.colors.lightestShade}; - - &::-webkit-progress-bar { - background-color: ${euiTheme.colors.lightestShade}; - } - `, - [euiTheme] - ); - const setState: typeof changeState = useCallback( (nextState) => { if (!isCanceledRef.current) { @@ -211,6 +177,7 @@ const FieldStatsComponent: React.FC = ({ const fromDateParsed = DateMath.parse(fromDate); const toDateParsed = DateMath.parse(toDate); + // TODO: extract into an util function const totalValuesCount = topValues && topValues.buckets.reduce((prev, bucket) => bucket.count + prev, 0); const otherCount = sampledValues && totalValuesCount ? sampledValues - totalValuesCount : 0; @@ -450,94 +417,14 @@ const FieldStatsComponent: React.FC = ({ } if (topValues && topValues.buckets.length) { - const digitsRequired = topValues.buckets.some( - (topValue) => !Number.isInteger(topValue.count / sampledValues!) - ); return combineWithTitleAndFooter( -
- {topValues.buckets.map((topValue) => { - const formatted = formatter.convert(topValue.key); - return ( -
- - - {formatted === '' ? ( - - - {i18n.translate('unifiedFieldList.fieldStats.emptyStringValueLabel', { - defaultMessage: 'Empty string', - })} - - - ) : ( - - - {formatted} - - - )} - - - - {(Math.round((topValue.count / sampledValues!) * 1000) / 10).toFixed( - digitsRequired ? 1 : 0 - )} - % - - - - -
- ); - })} - {otherCount ? ( - <> - - - - {i18n.translate('unifiedFieldList.fieldStats.otherDocsLabel', { - defaultMessage: 'Other', - })} - - - - - - {(Math.round((otherCount / sampledValues!) * 1000) / 10).toFixed( - digitsRequired ? 1 : 0 - )} - % - - - - - - - ) : ( - <> - )} -
+ ); } diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx new file mode 100755 index 0000000000000..0f5ea544e4eff --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo } from 'react'; +import { DataView, DataViewField } from '@kbn/data-plugin/common'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiText, + EuiToolTip, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import type { BucketedAggregation } from '../../../common/types'; + +export interface FieldTopValuesProps { + buckets: BucketedAggregation['buckets']; + dataView: DataView; + field: DataViewField; + sampledValuesCount: number; + testSubject: string; +} + +export const FieldTopValues: React.FC = ({ + buckets, + dataView, + field, + testSubject, + sampledValuesCount, +}) => { + const { euiTheme } = useEuiTheme(); + + const topValueStyles = useMemo( + () => css` + margin-bottom: ${euiTheme.size.s}; + + &:last-of-type { + margin-bottom: 0; + } + `, + [euiTheme] + ); + + if (!buckets?.length) { + return null; + } + + const formatter = dataView.getFormatterForField(field); + const totalValuesCount = buckets.reduce((prev, bucket) => bucket.count + prev, 0); + const otherCount = + sampledValuesCount && totalValuesCount ? sampledValuesCount - totalValuesCount : 0; + const digitsRequired = buckets.some( + (topValue) => !Number.isInteger(topValue.count / sampledValuesCount!) + ); + + return ( +
+ {buckets.map((topValue) => { + const formatted = formatter.convert(topValue.key); + + return ( +
+ + + {formatted === '' ? ( + + + {i18n.translate('unifiedFieldList.fieldStats.emptyStringValueLabel', { + defaultMessage: 'Empty string', + })} + + + ) : ( + + + {formatted} + + + )} + + + + {(Math.round((topValue.count / sampledValuesCount!) * 1000) / 10).toFixed( + digitsRequired ? 1 : 0 + )} + % + + + + +
+ ); + })} + {otherCount ? ( + <> + + + + {i18n.translate('unifiedFieldList.fieldStats.otherDocsLabel', { + defaultMessage: 'Other', + })} + + + + + + {(Math.round((otherCount / sampledValuesCount!) * 1000) / 10).toFixed( + digitsRequired ? 1 : 0 + )} + % + + + + + + + ) : ( + <> + )} +
+ ); +}; From f0266632d763197b3559277a3f244f94ae7f2440 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 17 Aug 2022 11:31:03 +0200 Subject: [PATCH 47/92] [Discover] Extract bucket UI into a separate component. Update colors. --- .../field_stats/field_top_values.tsx | 117 +++++------------- .../field_stats/field_top_values_bucket.tsx | 86 +++++++++++++ 2 files changed, 114 insertions(+), 89 deletions(-) create mode 100755 src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx index 0f5ea544e4eff..5031821abda52 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx @@ -6,19 +6,12 @@ * Side Public License, v 1. */ -import React, { useMemo } from 'react'; +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; import { DataView, DataViewField } from '@kbn/data-plugin/common'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiText, - EuiToolTip, - useEuiTheme, -} from '@elastic/eui'; -import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import type { BucketedAggregation } from '../../../common/types'; +import { FieldTopValuesBucket } from './field_top_values_bucket'; export interface FieldTopValuesProps { buckets: BucketedAggregation['buckets']; @@ -35,19 +28,6 @@ export const FieldTopValues: React.FC = ({ testSubject, sampledValuesCount, }) => { - const { euiTheme } = useEuiTheme(); - - const topValueStyles = useMemo( - () => css` - margin-bottom: ${euiTheme.size.s}; - - &:last-of-type { - margin-bottom: 0; - } - `, - [euiTheme] - ); - if (!buckets?.length) { return null; } @@ -62,81 +42,40 @@ export const FieldTopValues: React.FC = ({ return (
- {buckets.map((topValue) => { + {buckets.map((topValue, index) => { const formatted = formatter.convert(topValue.key); return ( -
- + {index > 0 && } + - - {formatted === '' ? ( - - - {i18n.translate('unifiedFieldList.fieldStats.emptyStringValueLabel', { - defaultMessage: 'Empty string', - })} - - - ) : ( - - - {formatted} - - - )} - - - - {(Math.round((topValue.count / sampledValuesCount!) * 1000) / 10).toFixed( - digitsRequired ? 1 : 0 - )} - % - - - - -
+ ); })} - {otherCount ? ( + {otherCount > 0 && ( <> - - - - {i18n.translate('unifiedFieldList.fieldStats.otherDocsLabel', { - defaultMessage: 'Other', - })} - - - - - - {(Math.round((otherCount / sampledValuesCount!) * 1000) / 10).toFixed( - digitsRequired ? 1 : 0 - )} - % - - - - - + + - ) : ( - <> )}
); diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx new file mode 100755 index 0000000000000..fb3c75def566c --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + euiPaletteColorBlind, + EuiProgress, + EuiProgressProps, + EuiText, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface FieldTopValuesBucketProps { + formattedLabel: string; + formattedValue: string; + progressValue: number; + progressColor?: EuiProgressProps['color']; + testSubject: string; +} + +export const FieldTopValuesBucket: React.FC = ({ + formattedLabel, + formattedValue, + progressValue, + progressColor, + testSubject, +}) => { + const euiVisColorPalette = euiPaletteColorBlind(); + const euiColorVis1 = euiVisColorPalette[1]; + + return ( + + + + + {formattedLabel === '' ? ( + + + {i18n.translate('unifiedFieldList.fieldStats.emptyStringValueLabel', { + defaultMessage: 'Empty string', + })} + + + ) : ( + + + {formattedLabel} + + + )} + + + + {formattedValue} + + + + + + {/* TODO: add filter button */} + + ); +}; From 25a122b8ba7cf50c206a5a8a2d38a82aec1b5687 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 17 Aug 2022 15:55:28 +0200 Subject: [PATCH 48/92] [Discover] Update styling --- .../field_stats/field_stats.test.tsx | 12 ++-- .../components/field_stats/field_stats.tsx | 12 ++-- .../field_stats/field_top_values.tsx | 64 ++++++++++++------- .../field_stats/field_top_values_bucket.tsx | 32 ++++++---- 4 files changed, 72 insertions(+), 48 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx index ff761988814ec..c6d88d91c1454 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -328,12 +328,12 @@ describe('UnifiedFieldList ', () => { const firstValue = stats.childAt(0); expect(stats).toHaveLength(1); - expect(firstValue.find('[data-test-subj="testing-topValues-value"]').first().text()).toBe( - '"success"' - ); - expect(firstValue.find('[data-test-subj="testing-topValues-valueCount"]').first().text()).toBe( - '41.5%' - ); + expect( + firstValue.find('[data-test-subj="testing-topValues-formattedLabel"]').first().text() + ).toBe('"success"'); + expect( + firstValue.find('[data-test-subj="testing-topValues-formattedValue"]').first().text() + ).toBe('41.5%'); expect(wrapper.find('[data-test-subj="testing-statsFooter"]').first().text()).toBe( '100% of 1624 documents' diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 6da5e37b7815f..9c0ebc877372e 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -35,7 +35,7 @@ import { i18n } from '@kbn/i18n'; import { buildEsQuery, Query, Filter, AggregateQuery } from '@kbn/es-query'; import type { BucketedAggregation } from '../../../common/types'; import { loadFieldStats, canProvideStatsForField } from '../../services'; -import { FieldTopValues } from './field_top_values'; +import { FieldTopValues, getOtherCount, getBucketsValuesCount } from './field_top_values'; interface State { isLoading: boolean; @@ -177,20 +177,18 @@ const FieldStatsComponent: React.FC = ({ const fromDateParsed = DateMath.parse(fromDate); const toDateParsed = DateMath.parse(toDate); - // TODO: extract into an util function - const totalValuesCount = - topValues && topValues.buckets.reduce((prev, bucket) => bucket.count + prev, 0); - const otherCount = sampledValues && totalValuesCount ? sampledValues - totalValuesCount : 0; + const bucketsValuesCount = getBucketsValuesCount(topValues?.buckets); + const otherCount = getOtherCount(bucketsValuesCount, sampledValues!); if ( - totalValuesCount && + bucketsValuesCount && histogram && histogram.buckets.length && topValues && topValues.buckets.length ) { // Default to histogram when top values are less than 10% of total - histogramDefault = otherCount / totalValuesCount > 0.9; + histogramDefault = otherCount / bucketsValuesCount > 0.9; } const [showingHistogram, setShowingHistogram] = useState(histogramDefault); diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx index 5031821abda52..bb5a5f500f7cd 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx @@ -6,10 +6,9 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { Fragment } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { DataView, DataViewField } from '@kbn/data-plugin/common'; -import { i18n } from '@kbn/i18n'; import type { BucketedAggregation } from '../../../common/types'; import { FieldTopValuesBucket } from './field_top_values_bucket'; @@ -33,11 +32,9 @@ export const FieldTopValues: React.FC = ({ } const formatter = dataView.getFormatterForField(field); - const totalValuesCount = buckets.reduce((prev, bucket) => bucket.count + prev, 0); - const otherCount = - sampledValuesCount && totalValuesCount ? sampledValuesCount - totalValuesCount : 0; + const otherCount = getOtherCount(getBucketsValuesCount(buckets), sampledValuesCount); const digitsRequired = buckets.some( - (topValue) => !Number.isInteger(topValue.count / sampledValuesCount!) + (topValue) => !Number.isInteger(topValue.count / sampledValuesCount) ); return ( @@ -46,33 +43,32 @@ export const FieldTopValues: React.FC = ({ const formatted = formatter.convert(topValue.key); return ( - <> + {index > 0 && } - + ); })} {otherCount > 0 && ( <> @@ -80,3 +76,27 @@ export const FieldTopValues: React.FC = ({
); }; + +export const getFormattedPercentageValue = ( + currentValue: number, + totalCount: number, + digitsRequired: boolean +): string => { + return totalCount > 0 + ? `${(Math.round((currentValue / totalCount) * 1000) / 10).toFixed(digitsRequired ? 1 : 0)}%` + : ''; +}; + +export const getProgressValue = (currentValue: number, totalCount: number): number => { + return totalCount > 0 ? currentValue / totalCount : 0; +}; + +export const getBucketsValuesCount = ( + buckets?: BucketedAggregation['buckets'] +): number => { + return buckets?.reduce((prev, bucket) => bucket.count + prev, 0) || 0; +}; + +export const getOtherCount = (bucketsValuesCount: number, sampledValuesCount: number): number => { + return sampledValuesCount && bucketsValuesCount ? sampledValuesCount - bucketsValuesCount : 0; +}; diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx index fb3c75def566c..55a1c3472b059 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -12,25 +12,25 @@ import { EuiFlexItem, euiPaletteColorBlind, EuiProgress, - EuiProgressProps, EuiText, EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; export interface FieldTopValuesBucketProps { - formattedLabel: string; + type?: 'normal' | 'other'; + formattedLabel?: string; formattedValue: string; progressValue: number; - progressColor?: EuiProgressProps['color']; testSubject: string; } export const FieldTopValuesBucket: React.FC = ({ + type = 'normal', formattedLabel, formattedValue, progressValue, - progressColor, testSubject, }) => { const euiVisColorPalette = euiPaletteColorBlind(); @@ -40,7 +40,7 @@ export const FieldTopValuesBucket: React.FC = ({ @@ -50,17 +50,23 @@ export const FieldTopValuesBucket: React.FC = ({ className="eui-textTruncate" data-test-subj={`${testSubject}-topValues-formattedLabel`} > - {formattedLabel === '' ? ( + {!formattedLabel ? ( - - {i18n.translate('unifiedFieldList.fieldStats.emptyStringValueLabel', { - defaultMessage: 'Empty string', - })} - + {type === 'other' + ? i18n.translate('unifiedFieldList.fieldStats.otherDocsLabel', { + defaultMessage: 'Other', + }) + : formattedValue === '' && ( + + {i18n.translate('unifiedFieldList.fieldStats.emptyStringValueLabel', { + defaultMessage: 'Empty string', + })} + + )} ) : ( - + {formattedLabel} @@ -76,7 +82,7 @@ export const FieldTopValuesBucket: React.FC = ({ value={progressValue} max={1} size="s" - color={progressColor || euiColorVis1} + color={type === 'other' ? 'subdued' : euiColorVis1} aria-label={`${formattedLabel} (${formattedValue})`} /> From e3b289eda086dfe8600cbb99d6bb856511930710 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 17 Aug 2022 16:00:26 +0200 Subject: [PATCH 49/92] [Discover] Fix empty values --- .../field_stats/field_top_values_bucket.tsx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx index 55a1c3472b059..576d2b33e3048 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -50,26 +50,26 @@ export const FieldTopValuesBucket: React.FC = ({ className="eui-textTruncate" data-test-subj={`${testSubject}-topValues-formattedLabel`} > - {!formattedLabel ? ( - - {type === 'other' - ? i18n.translate('unifiedFieldList.fieldStats.otherDocsLabel', { - defaultMessage: 'Other', - }) - : formattedValue === '' && ( - - {i18n.translate('unifiedFieldList.fieldStats.emptyStringValueLabel', { - defaultMessage: 'Empty string', - })} - - )} - - ) : ( + {(formattedLabel?.length ?? 0) > 0 ? ( {formattedLabel} + ) : ( + + {type === 'other' ? ( + i18n.translate('unifiedFieldList.fieldStats.otherDocsLabel', { + defaultMessage: 'Other', + }) + ) : ( + + {i18n.translate('unifiedFieldList.fieldStats.emptyStringValueLabel', { + defaultMessage: 'Empty string', + })} + + )} + )} From 486527cf53f4fe53aa49f95def735eb0530253f9 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 17 Aug 2022 16:19:04 +0200 Subject: [PATCH 50/92] [Discover] Allow to customize colors --- .../components/field_stats/field_stats.tsx | 30 ++++++++++++++++--- .../field_stats/field_top_values.tsx | 10 +++++-- .../field_stats/field_top_values_bucket.tsx | 18 ++++------- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 9c0ebc877372e..9d4795e7ae809 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DataView, DataViewField, @@ -35,7 +35,12 @@ import { i18n } from '@kbn/i18n'; import { buildEsQuery, Query, Filter, AggregateQuery } from '@kbn/es-query'; import type { BucketedAggregation } from '../../../common/types'; import { loadFieldStats, canProvideStatsForField } from '../../services'; -import { FieldTopValues, getOtherCount, getBucketsValuesCount } from './field_top_values'; +import { + FieldTopValues, + getOtherCount, + getBucketsValuesCount, + getDefaultColor, +} from './field_top_values'; interface State { isLoading: boolean; @@ -62,6 +67,7 @@ export interface FieldStatsProps { toDate: string; dataViewOrDataViewId: DataView | string; field: DataViewField; + color?: string; testSubject: string; overrideMissingContent?: (params?: { noDataFound?: boolean }) => JSX.Element | null; overrideFooter?: (params: { @@ -79,6 +85,7 @@ const FieldStatsComponent: React.FC = ({ toDate, dataViewOrDataViewId, field, + color = getDefaultColor(), testSubject, overrideMissingContent, overrideFooter, @@ -169,6 +176,20 @@ const FieldStatsComponent: React.FC = ({ const chartTheme = charts.theme.useChartsTheme(); const chartBaseTheme = charts.theme.useChartsBaseTheme(); + const customChartTheme: typeof chartTheme = useMemo(() => { + return color + ? { + ...chartTheme, + barSeriesStyle: { + ...chartTheme.barSeriesStyle, + rect: { + ...(chartTheme.barSeriesStyle?.rect || {}), + fill: color, + }, + }, + } + : chartTheme; + }, [chartTheme, color]); const { isLoading, histogram, topValues, sampledValues, sampledDocuments, totalDocuments } = state; @@ -345,7 +366,7 @@ const FieldStatsComponent: React.FC = ({ = ({ @@ -421,6 +442,7 @@ const FieldStatsComponent: React.FC = ({ dataView={dataView} field={field} sampledValuesCount={sampledValues!} + color={color} testSubject={testSubject} /> ); diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx index bb5a5f500f7cd..37616a1978912 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx @@ -7,7 +7,7 @@ */ import React, { Fragment } from 'react'; -import { EuiSpacer } from '@elastic/eui'; +import { euiPaletteColorBlind, EuiSpacer } from '@elastic/eui'; import { DataView, DataViewField } from '@kbn/data-plugin/common'; import type { BucketedAggregation } from '../../../common/types'; import { FieldTopValuesBucket } from './field_top_values_bucket'; @@ -17,6 +17,7 @@ export interface FieldTopValuesProps { dataView: DataView; field: DataViewField; sampledValuesCount: number; + color?: string; testSubject: string; } @@ -24,8 +25,9 @@ export const FieldTopValues: React.FC = ({ buckets, dataView, field, - testSubject, sampledValuesCount, + color = getDefaultColor(), + testSubject, }) => { if (!buckets?.length) { return null; @@ -53,6 +55,7 @@ export const FieldTopValues: React.FC = ({ digitsRequired )} progressValue={getProgressValue(topValue.count, sampledValuesCount)} + color={color} testSubject={testSubject} /> @@ -69,6 +72,7 @@ export const FieldTopValues: React.FC = ({ digitsRequired )} progressValue={getProgressValue(otherCount, sampledValuesCount)} + color={color} testSubject={testSubject} /> @@ -77,6 +81,8 @@ export const FieldTopValues: React.FC = ({ ); }; +export const getDefaultColor = () => euiPaletteColorBlind()[1]; + export const getFormattedPercentageValue = ( currentValue: number, totalCount: number, diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx index 576d2b33e3048..4870feeedd35e 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -7,14 +7,7 @@ */ import React from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - euiPaletteColorBlind, - EuiProgress, - EuiText, - EuiToolTip, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiText, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; @@ -23,6 +16,7 @@ export interface FieldTopValuesBucketProps { formattedLabel?: string; formattedValue: string; progressValue: number; + color: string; testSubject: string; } @@ -31,11 +25,9 @@ export const FieldTopValuesBucket: React.FC = ({ formattedLabel, formattedValue, progressValue, + color, testSubject, }) => { - const euiVisColorPalette = euiPaletteColorBlind(); - const euiColorVis1 = euiVisColorPalette[1]; - return ( = ({ )} - + {formattedValue} @@ -82,7 +74,7 @@ export const FieldTopValuesBucket: React.FC = ({ value={progressValue} max={1} size="s" - color={type === 'other' ? 'subdued' : euiColorVis1} + color={type === 'other' ? 'subdued' : color} aria-label={`${formattedLabel} (${formattedValue})`} /> From 835679ca6b88c1dfb903395179ebc0baaec98fe1 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 17 Aug 2022 16:54:24 +0200 Subject: [PATCH 51/92] [Discover] Add filter buttons --- .../components/sidebar/discover_field.tsx | 3 +- .../sidebar/discover_sidebar_responsive.tsx | 2 +- .../components/field_stats/field_stats.tsx | 4 ++ .../field_stats/field_top_values.tsx | 14 +++- .../field_stats/field_top_values_bucket.tsx | 67 ++++++++++++++++++- .../unified_field_list/public/index.ts | 6 +- .../unified_field_list/public/types.ts | 4 ++ 7 files changed, 93 insertions(+), 7 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index e5a854d39e99c..d6b2542acdc2c 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -228,7 +228,7 @@ export interface DiscoverFieldProps { /** * Callback to add a filter to filter bar */ - onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void; + onAddFilter?: (field: DataViewField | string, value: unknown, type: '+' | '-') => void; /** * Callback to remove/deselect a the field * @param fieldName @@ -433,6 +433,7 @@ function DiscoverFieldComponent({ dataViewOrDataViewId={dataView} field={fieldForStats} testSubject="dscFieldListPanel" + onAddFilter={onAddFilter} overrideMissingContent={(params) => { if (params?.noDataFound) { return ( diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index bc813119342ef..3ed29efea8b2c 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -63,7 +63,7 @@ export interface DiscoverSidebarResponsiveProps { /** * Callback function when adding a filter from sidebar */ - onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void; + onAddFilter?: (field: DataViewField | string, value: unknown, type: '+' | '-') => void; /** * Callback function when changing an data view */ diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 9d4795e7ae809..9be2c180cf545 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -34,6 +34,7 @@ import { import { i18n } from '@kbn/i18n'; import { buildEsQuery, Query, Filter, AggregateQuery } from '@kbn/es-query'; import type { BucketedAggregation } from '../../../common/types'; +import type { AddFieldFilterHandler } from '../../types'; import { loadFieldStats, canProvideStatsForField } from '../../services'; import { FieldTopValues, @@ -75,6 +76,7 @@ export interface FieldStatsProps { totalDocuments?: number; sampledDocuments?: number; }) => JSX.Element; + onAddFilter?: AddFieldFilterHandler; } const FieldStatsComponent: React.FC = ({ @@ -89,6 +91,7 @@ const FieldStatsComponent: React.FC = ({ testSubject, overrideMissingContent, overrideFooter, + onAddFilter, }) => { const { fieldFormats, uiSettings, charts, dataViews, data } = services; const [state, changeState] = useState({ @@ -444,6 +447,7 @@ const FieldStatsComponent: React.FC = ({ sampledValuesCount={sampledValues!} color={color} testSubject={testSubject} + onAddFilter={onAddFilter} /> ); } diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx index 37616a1978912..58f48b504bddb 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx @@ -10,6 +10,7 @@ import React, { Fragment } from 'react'; import { euiPaletteColorBlind, EuiSpacer } from '@elastic/eui'; import { DataView, DataViewField } from '@kbn/data-plugin/common'; import type { BucketedAggregation } from '../../../common/types'; +import type { AddFieldFilterHandler } from '../../types'; import { FieldTopValuesBucket } from './field_top_values_bucket'; export interface FieldTopValuesProps { @@ -19,6 +20,7 @@ export interface FieldTopValuesProps { sampledValuesCount: number; color?: string; testSubject: string; + onAddFilter?: AddFieldFilterHandler; } export const FieldTopValues: React.FC = ({ @@ -28,6 +30,7 @@ export const FieldTopValues: React.FC = ({ sampledValuesCount, color = getDefaultColor(), testSubject, + onAddFilter, }) => { if (!buckets?.length) { return null; @@ -42,12 +45,15 @@ export const FieldTopValues: React.FC = ({ return (
{buckets.map((topValue, index) => { - const formatted = formatter.convert(topValue.key); + const fieldValue = topValue.key; + const formatted = formatter.convert(fieldValue); return ( - + {index > 0 && } = ({ progressValue={getProgressValue(topValue.count, sampledValuesCount)} color={color} testSubject={testSubject} + onAddFilter={onAddFilter} /> ); @@ -66,6 +73,8 @@ export const FieldTopValues: React.FC = ({ = ({ progressValue={getProgressValue(otherCount, sampledValuesCount)} color={color} testSubject={testSubject} + onAddFilter={onAddFilter} /> )} diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx index 4870feeedd35e..a47806d52beb3 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -7,27 +7,44 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiText, EuiToolTip } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiText, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; +import type { AddFieldFilterHandler } from '../../types'; export interface FieldTopValuesBucketProps { type?: 'normal' | 'other'; + field: DataViewField; + fieldValue: unknown; formattedLabel?: string; formattedValue: string; progressValue: number; color: string; testSubject: string; + onAddFilter?: AddFieldFilterHandler; } export const FieldTopValuesBucket: React.FC = ({ type = 'normal', + field, + fieldValue, formattedLabel, formattedValue, progressValue, color, testSubject, + onAddFilter, }) => { + const isFilterButtonDisabled = !onAddFilter || type === 'other'; + return ( = ({ aria-label={`${formattedLabel} (${formattedValue})`} /> - {/* TODO: add filter button */} + {onAddFilter && field.filterable && ( + +
+ onAddFilter(field, fieldValue, '+')} + aria-label={i18n.translate('unifiedFieldList.fieldStats.filterValueButtonAriaLabel', { + defaultMessage: 'Filter for {field}: "{value}"', + values: { value: formattedLabel, field: field.name }, + })} + data-test-subj={`plus-${field.name}-${fieldValue}`} + style={{ + minHeight: 'auto', + minWidth: 'auto', + paddingRight: 2, + paddingLeft: 2, + paddingTop: 0, + paddingBottom: 0, + }} + /> + onAddFilter(field, fieldValue, '-')} + aria-label={i18n.translate( + 'unifiedFieldList.fieldStats.filterOutValueButtonAriaLabel', + { + defaultMessage: 'Filter out {field}: "{value}"', + values: { value: formattedLabel, field: field.name }, + } + )} + data-test-subj={`minus-${field.name}-${fieldValue}`} + style={{ + minHeight: 'auto', + minWidth: 'auto', + paddingTop: 0, + paddingBottom: 0, + paddingRight: 2, + paddingLeft: 2, + }} + /> +
+
+ )}
); }; diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts index a2b6e9c874535..c5d91808a4dbb 100755 --- a/src/plugins/unified_field_list/public/index.ts +++ b/src/plugins/unified_field_list/public/index.ts @@ -22,6 +22,10 @@ export { FieldStats } from './components/field_stats'; export function plugin() { return new UnifiedFieldListPlugin(); } -export type { UnifiedFieldListPluginSetup, UnifiedFieldListPluginStart } from './types'; +export type { + UnifiedFieldListPluginSetup, + UnifiedFieldListPluginStart, + AddFieldFilterHandler, +} from './types'; export { loadFieldStats, canProvideStatsForField } from './services'; diff --git a/src/plugins/unified_field_list/public/types.ts b/src/plugins/unified_field_list/public/types.ts index feb24509cdee5..dcd425b8a880b 100755 --- a/src/plugins/unified_field_list/public/types.ts +++ b/src/plugins/unified_field_list/public/types.ts @@ -6,8 +6,12 @@ * Side Public License, v 1. */ +import type { DataViewField } from '@kbn/data-views-plugin/common'; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface UnifiedFieldListPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface UnifiedFieldListPluginStart {} + +export type AddFieldFilterHandler = (field: DataViewField, value: unknown, type: '+' | '-') => void; From e844c664660ddca95f610fe026e423c09603190d Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 17 Aug 2022 17:06:38 +0200 Subject: [PATCH 52/92] [Discover] Rename props --- .../field_stats/field_stats.test.tsx | 4 +-- .../field_stats/field_top_values.tsx | 6 ++--- .../field_stats/field_top_values_bucket.tsx | 26 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx index c6d88d91c1454..06846e26bd3a3 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -329,10 +329,10 @@ describe('UnifiedFieldList ', () => { expect(stats).toHaveLength(1); expect( - firstValue.find('[data-test-subj="testing-topValues-formattedLabel"]').first().text() + firstValue.find('[data-test-subj="testing-topValues-formattedFieldValue"]').first().text() ).toBe('"success"'); expect( - firstValue.find('[data-test-subj="testing-topValues-formattedValue"]').first().text() + firstValue.find('[data-test-subj="testing-topValues-formattedPercentage"]').first().text() ).toBe('41.5%'); expect(wrapper.find('[data-test-subj="testing-statsFooter"]').first().text()).toBe( diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx index 58f48b504bddb..4c8075e799583 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx @@ -54,8 +54,8 @@ export const FieldTopValues: React.FC = ({ = ({ type="other" field={field} fieldValue={undefined} - formattedValue={getFormattedPercentageValue( + formattedPercentage={getFormattedPercentageValue( otherCount, sampledValuesCount, digitsRequired diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx index a47806d52beb3..c8f889c88aadb 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -24,8 +24,8 @@ export interface FieldTopValuesBucketProps { type?: 'normal' | 'other'; field: DataViewField; fieldValue: unknown; - formattedLabel?: string; - formattedValue: string; + formattedFieldValue?: string; + formattedPercentage: string; progressValue: number; color: string; testSubject: string; @@ -36,8 +36,8 @@ export const FieldTopValuesBucket: React.FC = ({ type = 'normal', field, fieldValue, - formattedLabel, - formattedValue, + formattedFieldValue, + formattedPercentage, progressValue, color, testSubject, @@ -57,12 +57,12 @@ export const FieldTopValuesBucket: React.FC = ({ - {(formattedLabel?.length ?? 0) > 0 ? ( - + {(formattedFieldValue?.length ?? 0) > 0 ? ( + - {formattedLabel} + {formattedFieldValue} ) : ( @@ -81,9 +81,9 @@ export const FieldTopValuesBucket: React.FC = ({ )} - + - {formattedValue} + {formattedPercentage} @@ -92,7 +92,7 @@ export const FieldTopValuesBucket: React.FC = ({ max={1} size="s" color={type === 'other' ? 'subdued' : color} - aria-label={`${formattedLabel} (${formattedValue})`} + aria-label={`${formattedFieldValue} (${formattedPercentage})`} /> {onAddFilter && field.filterable && ( @@ -105,7 +105,7 @@ export const FieldTopValuesBucket: React.FC = ({ onClick={() => onAddFilter(field, fieldValue, '+')} aria-label={i18n.translate('unifiedFieldList.fieldStats.filterValueButtonAriaLabel', { defaultMessage: 'Filter for {field}: "{value}"', - values: { value: formattedLabel, field: field.name }, + values: { value: formattedFieldValue, field: field.name }, })} data-test-subj={`plus-${field.name}-${fieldValue}`} style={{ @@ -126,7 +126,7 @@ export const FieldTopValuesBucket: React.FC = ({ 'unifiedFieldList.fieldStats.filterOutValueButtonAriaLabel', { defaultMessage: 'Filter out {field}: "{value}"', - values: { value: formattedLabel, field: field.name }, + values: { value: formattedFieldValue, field: field.name }, } )} data-test-subj={`minus-${field.name}-${fieldValue}`} From 94a1fcf43d4ffedf51c17b17729d62a0235c0ad4 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 17 Aug 2022 17:31:17 +0200 Subject: [PATCH 53/92] [Discover] Improve format --- .../field_stats/field_top_values_bucket.tsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx index c8f889c88aadb..2c871b0d517d3 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -67,17 +67,15 @@ export const FieldTopValuesBucket: React.FC = ({ ) : ( - {type === 'other' ? ( - i18n.translate('unifiedFieldList.fieldStats.otherDocsLabel', { - defaultMessage: 'Other', - }) - ) : ( - - {i18n.translate('unifiedFieldList.fieldStats.emptyStringValueLabel', { - defaultMessage: 'Empty string', - })} - - )} + {type === 'other' + ? i18n.translate('unifiedFieldList.fieldStats.otherDocsLabel', { + defaultMessage: 'Other', + }) + : formattedFieldValue === '' + ? i18n.translate('unifiedFieldList.fieldStats.emptyStringValueLabel', { + defaultMessage: '(empty)', + }) + : '-'} )} From f6b05d53a10904da8ed1743a72ef3efb3b23783c Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 23 Aug 2022 09:41:17 +0200 Subject: [PATCH 54/92] [Discover] Add a switch in Settings. Move Visualize button into PopoverFooter. --- docs/management/advanced-options.asciidoc | 4 + src/plugins/discover/common/index.ts | 1 + .../components/sidebar/discover_field.tsx | 73 +++++++++---------- .../discover_field_visualize_inner.tsx | 30 ++++---- src/plugins/discover/server/ui_settings.ts | 14 ++++ .../server/collectors/management/schema.ts | 4 + .../server/collectors/management/types.ts | 1 + src/plugins/telemetry/schema/oss_plugins.json | 6 ++ .../components/field_stats/field_stats.tsx | 2 +- 9 files changed, 82 insertions(+), 53 deletions(-) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index c0fdb537aed73..18675b11254c4 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -331,6 +331,10 @@ Controls the way the document table looks and works. To use the new *Document Explorer* instead of the classic view, turn off this option. The *Document Explorer* offers better data sorting, resizable columns, and a full screen view. +[[discover:showLegacyFieldTopValues]]`discover:showLegacyFieldTopValues`:: +This setting will calculate Top Values for a field based only on the loaded records on Discover page. +To use the new and more accurate view, turn off this option. + [float] [[kibana-ml-settings]] ==== Machine Learning diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index c9ce8777f386e..1e204683c0cfb 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -28,4 +28,5 @@ export const TRUNCATE_MAX_HEIGHT = 'truncate:maxHeight'; export const ROW_HEIGHT_OPTION = 'discover:rowHeightOption'; export const SEARCH_EMBEDDABLE_TYPE = 'search'; export const HIDE_ANNOUNCEMENTS = 'hideAnnouncements'; +export const SHOW_LEGACY_FIELD_TOP_VALUES = 'discover:showLegacyFieldTopValues'; export const ENABLE_SQL = 'discover:enableSql'; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index d6b2542acdc2c..a085c944804d6 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -19,7 +19,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiHorizontalRule, EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -36,6 +35,7 @@ import { getFieldTypeName } from '../../../../utils/get_field_type_name'; import { DiscoverFieldVisualize } from './discover_field_visualize'; import type { AppState } from '../../services/discover_state'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; +import { SHOW_LEGACY_FIELD_TOP_VALUES } from '../../../../../common'; function wrapOnDot(str?: string) { // u200B is a non-width white-space character, which allows @@ -411,19 +411,35 @@ function DiscoverFieldComponent({ const details = getDetails(field); const dateRange = data?.query?.timefilter.timefilter.getTime(); const fieldForStats = multiFields ? multiFields[0].field : field; // TODO: how to handle multifields? - const showNewStatsPreviewInDiscover = true; // Toggle this variable to preview new stats locally + const showLegacyFieldStats = services.uiSettings.get(SHOW_LEGACY_FIELD_TOP_VALUES); return ( <> - {showFieldStats && ( - <> - {showNewStatsPreviewInDiscover && ( - <> - - {'Stats as in Lens:'} - - - {Boolean(dateRange) && ( + <> + {showLegacyFieldStats ? ( + <> + {showFieldStats && ( + <> + +
+ {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { + defaultMessage: 'Top 5 values', + })} +
+
+ + + )} + + ) : ( + <> + {Boolean(dateRange) && ( + <> - )} - {/* TODO: remove previous field stats view when we finish FieldStats component and add addFilter buttons to it */} - - - {'Current Discover stats:'} - - - - )} - -
- {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { - defaultMessage: 'Top 5 values', - })} -
-
- - - )} + + )} + + )} + {multiFields && ( <> - {showFieldStats && } + {(showFieldStats || !showLegacyFieldStats) && } )} - {(showFieldStats || multiFields) && } - { const { field, visualizeInfo, handleVisualizeLinkClick } = props; return ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - - - + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + + + ); }; diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index 0419594d31d47..749a3b34cbff4 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -30,6 +30,7 @@ import { TRUNCATE_MAX_HEIGHT, SHOW_FIELD_STATISTICS, ROW_HEIGHT_OPTION, + SHOW_LEGACY_FIELD_TOP_VALUES, ENABLE_SQL, } from '../common'; import { DEFAULT_ROWS_PER_PAGE, ROWS_PER_PAGE_OPTIONS } from '../common/constants'; @@ -124,6 +125,19 @@ export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'discover:showLegacyFieldTopValues': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'discover:sampleSize': { type: 'long', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 1a946e99f29bd..67fcae75f474f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -76,6 +76,7 @@ export interface UsageStats { 'doc_table:hideTimeColumn': boolean; 'discover:sampleSize': number; 'discover:sampleRowsPerPage': number; + 'discover:showLegacyFieldTopValues': boolean; defaultColumns: string[]; 'context:defaultSize': number; 'context:tieBreakerFields': string[]; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index cacd8067e4e03..366c05d6a4afe 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7883,6 +7883,12 @@ "description": "Non-default value of setting." } }, + "discover:showLegacyFieldTopValues": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "discover:sampleSize": { "type": "long", "_meta": { diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 9be2c180cf545..06abd5b301391 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -351,7 +351,7 @@ const FieldStatsComponent: React.FC = ({ overrideFooter?.({ element: countsElement, totalDocuments, sampledDocuments }) ) : ( <> - + {countsElement} )} From 055bb1a1e158d8c41846a6831458aaa187170fa3 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 23 Aug 2022 08:57:41 +0000 Subject: [PATCH 55/92] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../components/field_stats/field_stats.test.tsx | 12 ++++++------ .../public/components/field_stats/field_stats.tsx | 10 ++++++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx index ed1aa20a2187b..7a45f4c98fdfa 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -333,12 +333,12 @@ describe('UnifiedFieldList ', () => { const firstValue = stats.childAt(0); expect(stats).toHaveLength(1); - expect(firstValue.find('[data-test-subj="testing-topValues-formattedFieldValue"]').first().text()).toBe( - '"success"' - ); - expect(firstValue.find('[data-test-subj="testing-topValues-formattedPercentage"]').first().text()).toBe( - '41.5%' - ); + expect( + firstValue.find('[data-test-subj="testing-topValues-formattedFieldValue"]').first().text() + ).toBe('"success"'); + expect( + firstValue.find('[data-test-subj="testing-topValues-formattedPercentage"]').first().text() + ).toBe('41.5%'); expect(wrapper.find('[data-test-subj="testing-statsFooter"]').first().text()).toBe( '100% of 1624 documents' diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 4abe8f24ca773..b669feda1c758 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -367,7 +367,10 @@ const FieldStatsComponent: React.FC = ({ if (field.type === 'date') { return combineWithTitleAndFooter( - + = ({ if (showingHistogram || !topValues || !topValues.buckets.length) { return combineWithTitleAndFooter( - + Date: Tue, 23 Aug 2022 13:04:24 +0200 Subject: [PATCH 56/92] [Discover] Fix props --- .../application/main/components/sidebar/discover_field.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index a085c944804d6..bfda6a0ca080b 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -448,7 +448,7 @@ function DiscoverFieldComponent({ toDate={dateRange.to} dataViewOrDataViewId={dataView} field={fieldForStats} - testSubject="dscFieldListPanel" + data-test-subj="dscFieldListPanel" onAddFilter={onAddFilter} overrideMissingContent={(params) => { if (params?.noDataFound) { From 7d25dc512d7c14411ae321099ac2756f88d95c10 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 23 Aug 2022 13:26:55 +0200 Subject: [PATCH 57/92] [Discover] Hide filter buttons for Other section --- .../field_stats/field_top_values_bucket.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx index 2c871b0d517d3..b50b912a89cad 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -43,7 +43,7 @@ export const FieldTopValuesBucket: React.FC = ({ testSubject, onAddFilter, }) => { - const isFilterButtonDisabled = !onAddFilter || type === 'other'; + const isFilterButtonHidden = type === 'other'; return ( @@ -95,9 +95,17 @@ export const FieldTopValuesBucket: React.FC = ({ {onAddFilter && field.filterable && ( -
+
onAddFilter(field, fieldValue, '+')} @@ -116,7 +124,7 @@ export const FieldTopValuesBucket: React.FC = ({ }} /> onAddFilter(field, fieldValue, '-')} From 1f70f57bcdfa5c0613da878b0994f5498def5f51 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 23 Aug 2022 15:29:12 +0200 Subject: [PATCH 58/92] [Discover] Simplify default messages when analysis is not available --- .../components/sidebar/discover_field.tsx | 90 +++++------- .../discover_field_visualize_inner.tsx | 1 + .../common/utils/field_stats_utils.ts | 3 +- .../components/field_stats/field_stats.tsx | 128 ++++++++---------- .../field_stats/field_summary_message.tsx | 18 +++ .../indexpattern_datasource/field_item.tsx | 21 ++- .../visualize_geo_field_button.tsx | 5 +- 7 files changed, 131 insertions(+), 135 deletions(-) create mode 100755 src/plugins/unified_field_list/public/components/field_stats/field_summary_message.tsx diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index bfda6a0ca080b..894124fdcefb4 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -19,7 +19,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UiCounterMetricType } from '@kbn/analytics'; @@ -415,58 +414,43 @@ function DiscoverFieldComponent({ return ( <> - <> - {showLegacyFieldStats ? ( - <> - {showFieldStats && ( - <> - -
- {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { - defaultMessage: 'Top 5 values', - })} -
-
- - - )} - - ) : ( - <> - {Boolean(dateRange) && ( - <> - { - if (params?.noDataFound) { - return ( - {`TODO: add a custom "no data available" message for ${fieldForStats.type} field`} - ); - } - - return ( - {`TODO: add a custom "stats are not available" message for ${fieldForStats.type} field`} - ); - }} - /> - - )} - - )} - + {showLegacyFieldStats ? ( + <> + {showFieldStats && ( + <> + +
+ {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { + defaultMessage: 'Top 5 values', + })} +
+
+ + + )} + + ) : ( + <> + {Boolean(dateRange) && ( + + )} + + )} {multiFields && ( <> diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_visualize_inner.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_visualize_inner.tsx index f2ffb89430017..2bbdeed13fde3 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_visualize_inner.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field_visualize_inner.tsx @@ -20,6 +20,7 @@ interface DiscoverFieldVisualizeInnerProps { export const DiscoverFieldVisualizeInner = (props: DiscoverFieldVisualizeInnerProps) => { const { field, visualizeInfo, handleVisualizeLinkClick } = props; + return ( {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index 9cf33961d76b6..685590bb453e2 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -105,7 +105,8 @@ export function canProvideStatsForField(field: DataViewFieldBase): boolean { field.type === 'document' || field.type.includes('range') || field.type === 'geo_point' || - field.type === 'geo_shape' + field.type === 'geo_shape' || + field.type === 'murmur3' ); } diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index b669feda1c758..57b9d55457d91 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -43,6 +43,7 @@ import { getBucketsValuesCount, getDefaultColor, } from './field_top_values'; +import { FieldSummaryMessage } from './field_summary_message'; interface State { isLoading: boolean; @@ -71,7 +72,10 @@ export interface FieldStatsProps { field: DataViewField; color?: string; 'data-test-subj'?: string; - overrideMissingContent?: (params?: { noDataFound?: boolean }) => JSX.Element | null; + overrideMissingContent?: (params: { + element: JSX.Element; + noDataFound?: boolean; + }) => JSX.Element | null; overrideFooter?: (params: { element: JSX.Element; totalDocuments?: number; @@ -229,39 +233,73 @@ const FieldStatsComponent: React.FC = ({ const formatter = dataView.getFormatterForField(field); let title = <>; - if (field.type.includes('range')) { - return ( - <> - - {i18n.translate('unifiedFieldList.fieldStats.notAvailableForRangeFieldDescription', { - defaultMessage: `Summary information is not available for range type fields.`, - })} - - + function combineWithTitleAndFooter(el: React.ReactElement) { + const countsElement = totalDocuments ? ( + + {sampledDocuments && ( + <> + {i18n.translate('unifiedFieldList.fieldStats.percentageOfLabel', { + defaultMessage: '{percentage}% of', + values: { + percentage: Math.round((sampledDocuments / totalDocuments) * 100), + }, + })}{' '} + + )} + + {fieldFormats + .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) + .convert(totalDocuments)} + {' '} + {i18n.translate('unifiedFieldList.fieldStats.ofDocumentsLabel', { + defaultMessage: 'documents', + })} + + ) : ( + <> ); - } - if (field.type === 'murmur3') { return ( <> - - {i18n.translate('unifiedFieldList.fieldStats.notAvailableForMurmur3FieldDescription', { - defaultMessage: `Summary information is not available for murmur3 fields.`, - })} - + {title ? title : <>} + + + + {el} + + {overrideFooter ? ( + overrideFooter?.({ element: countsElement, totalDocuments, sampledDocuments }) + ) : ( + <> + + {countsElement} + + )} ); } - if (field.type === 'geo_point' || field.type === 'geo_shape') { - return overrideMissingContent ? overrideMissingContent() : null; - } - if ( (!histogram || histogram.buckets.length === 0) && (!topValues || topValues.buckets.length === 0) ) { - return overrideMissingContent ? overrideMissingContent({ noDataFound: true }) : null; + const message = ( + + ); + + return overrideMissingContent + ? overrideMissingContent({ + noDataFound: canProvideStatsForField(field), + element: message, + }) + : message; } if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { @@ -314,52 +352,6 @@ const FieldStatsComponent: React.FC = ({ ); } - function combineWithTitleAndFooter(el: React.ReactElement) { - const countsElement = totalDocuments ? ( - - {sampledDocuments && ( - <> - {i18n.translate('unifiedFieldList.fieldStats.percentageOfLabel', { - defaultMessage: '{percentage}% of', - values: { - percentage: Math.round((sampledDocuments / totalDocuments) * 100), - }, - })}{' '} - - )} - - {fieldFormats - .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) - .convert(totalDocuments)} - {' '} - {i18n.translate('unifiedFieldList.fieldStats.ofDocumentsLabel', { - defaultMessage: 'documents', - })} - - ) : ( - <> - ); - - return ( - <> - {title ? title : <>} - - - - {el} - - {overrideFooter ? ( - overrideFooter?.({ element: countsElement, totalDocuments, sampledDocuments }) - ) : ( - <> - - {countsElement} - - )} - - ); - } - if (histogram && histogram.buckets.length) { const specId = i18n.translate('unifiedFieldList.fieldStats.countLabel', { defaultMessage: 'Count', diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_summary_message.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_summary_message.tsx new file mode 100755 index 0000000000000..b67f00ccd7292 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_stats/field_summary_message.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiText } from '@elastic/eui'; + +export interface FieldSummaryMessageProps { + message: string; +} + +export const FieldSummaryMessage: React.FC = ({ message }) => { + return {message}; +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 427b8e4623353..c90b311a7a215 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -16,7 +16,6 @@ import { EuiPopover, EuiPopoverTitle, EuiPopoverFooter, - EuiSpacer, EuiText, EuiTitle, EuiToolTip, @@ -38,7 +37,6 @@ import type { IndexPattern, IndexPatternField } from '../types'; import type { DraggedField } from './types'; import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { VisualizeGeoFieldButton } from './visualize_geo_field_button'; -import { getVisualizeGeoFieldMessage } from '../utils'; import type { LensAppServices } from '../app_plugin/types'; import { debouncedComponent } from '../debounced_component'; @@ -348,24 +346,25 @@ function FieldItemPopoverContents(props: FieldItemProps) { dataViewOrDataViewId={indexPattern.id} // TODO: Refactor to pass a variable with DataView type instead of IndexPattern field={field as DataViewField} data-test-subj="lnsFieldListPanel" - overrideFooter={({ element }) => {element}} overrideMissingContent={(params) => { if (field.type === 'geo_point' || field.type === 'geo_shape') { return ( <> - {getVisualizeGeoFieldMessage(field.type)} + {params.element} - - + + + ); } if (params?.noDataFound) { + // TODO: should we replace this with a default message "Summary is not available for this field?" const isUsingSampling = core.uiSettings.get('lens:useFieldExistenceSampling'); return ( <> @@ -384,7 +383,7 @@ function FieldItemPopoverContents(props: FieldItemProps) { ); } - return null; + return params.element; }} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/visualize_geo_field_button.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/visualize_geo_field_button.tsx index 81e31ab4c64a3..10483923f6f71 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/visualize_geo_field_button.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/visualize_geo_field_button.tsx @@ -59,14 +59,15 @@ export function VisualizeGeoFieldButton(props: Props) { <> {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} From f64d36f1217f7bd463c3428a63c22f8648e59238 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 24 Aug 2022 13:32:54 +0200 Subject: [PATCH 59/92] [Discover] Small update --- .../field_stats/field_top_values.tsx | 8 +- .../public/services/field_stats.ts | 94 ------------------- 2 files changed, 4 insertions(+), 98 deletions(-) delete mode 100644 src/plugins/unified_field_list/public/services/field_stats.ts diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx index 4c8075e799583..d994e86968e53 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx @@ -44,8 +44,8 @@ export const FieldTopValues: React.FC = ({ return (
- {buckets.map((topValue, index) => { - const fieldValue = topValue.key; + {buckets.map((bucket, index) => { + const fieldValue = bucket.key; const formatted = formatter.convert(fieldValue); return ( @@ -56,11 +56,11 @@ export const FieldTopValues: React.FC = ({ fieldValue={fieldValue} formattedFieldValue={formatted} formattedPercentage={getFormattedPercentageValue( - topValue.count, + bucket.count, sampledValuesCount, digitsRequired )} - progressValue={getProgressValue(topValue.count, sampledValuesCount)} + progressValue={getProgressValue(bucket.count, sampledValuesCount)} color={color} testSubject={testSubject} onAddFilter={onAddFilter} diff --git a/src/plugins/unified_field_list/public/services/field_stats.ts b/src/plugins/unified_field_list/public/services/field_stats.ts deleted file mode 100644 index 91a28e4feda25..0000000000000 --- a/src/plugins/unified_field_list/public/services/field_stats.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { lastValueFrom } from 'rxjs'; -import type { DataView } from '@kbn/data-views-plugin/common'; -import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { DataViewFieldBase } from '@kbn/es-query'; -import type { FieldStatsResponse } from '../../common/types'; -import { - fetchAndCalculateFieldStats, - SearchHandler, - buildSearchParams, -} from '../../common/utils/field_stats_utils'; - -interface FetchFieldStatsParams { - services: { - data: DataPublicPluginStart; - }; - dataView: DataView; - field: DataViewFieldBase; - fromDate: string; - toDate: string; - dslQuery: object; - size?: number; - abortController?: AbortController; -} - -/** - * Loads and aggregates stats data for a data view field - * @param services - * @param dataView - * @param field - * @param fromDate - * @param toDate - * @param dslQuery - * @param size - * @param abortController - */ -export const loadFieldStats = async ({ - services, - dataView, - field, - fromDate, - toDate, - dslQuery, - size, - abortController, -}: FetchFieldStatsParams): Promise> => { - const { data } = services; - - try { - if (!dataView?.id || !field?.type) { - return {}; - } - - const searchHandler: SearchHandler = async (aggs) => { - const result = await lastValueFrom( - data.search.search( - { - params: buildSearchParams({ - dataViewPattern: dataView.title, - timeFieldName: dataView.timeFieldName, - fromDate, - toDate, - dslQuery, - runtimeMappings: dataView.getRuntimeMappings(), - aggs, - }), - }, - { - abortSignal: abortController?.signal, - } - ) - ); - return result.rawResponse; - }; - - return await fetchAndCalculateFieldStats({ - searchHandler, - field, - fromDate, - toDate, - size, - }); - } catch (error) { - // console.error(error); - throw new Error('Could not provide field stats'); - } -}; From da4dc19dc02bcdfde48bfceb53fe10c67fc8d787 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 24 Aug 2022 13:54:06 +0200 Subject: [PATCH 60/92] [Discover] Remove translations --- x-pack/plugins/translations/translations/fr-FR.json | 1 - x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 3 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 0273a23d4179f..998d20ce4fe20 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -17468,7 +17468,6 @@ "xpack.lens.indexPattern.existenceErrorLabel": "Impossible de charger les informations de champ", "xpack.lens.indexPattern.existenceTimeoutAriaLabel": "La récupération de l'existence a expiré", "xpack.lens.indexPattern.existenceTimeoutLabel": "Les informations de champ ont pris trop de temps", - "xpack.lens.indexPattern.fieldItem.visualizeGeoFieldLinkText": "Visualiser dans Maps", "xpack.lens.indexPattern.fieldItemTooltip": "Effectuez un glisser-déposer pour visualiser.", "xpack.lens.indexPattern.fieldPlaceholder": "Champ", "xpack.lens.indexPattern.fieldStatsButtonEmptyLabel": "Ce champ ne comporte aucune donnée mais vous pouvez toujours effectuer un glisser-déposer pour visualiser.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 556e8facd1534..731bf07740696 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17453,7 +17453,6 @@ "xpack.lens.indexPattern.existenceErrorLabel": "フィールド情報を読み込めません", "xpack.lens.indexPattern.existenceTimeoutAriaLabel": "存在の取り込みがタイムアウトしました", "xpack.lens.indexPattern.existenceTimeoutLabel": "フィールド情報に時間がかかりすぎました", - "xpack.lens.indexPattern.fieldItem.visualizeGeoFieldLinkText": "Mapsで可視化", "xpack.lens.indexPattern.fieldItemTooltip": "可視化するには、ドラッグアンドドロップします。", "xpack.lens.indexPattern.fieldPlaceholder": "フィールド", "xpack.lens.indexPattern.fieldStatsButtonEmptyLabel": "このフィールドにはデータがありませんが、ドラッグアンドドロップで可視化できます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 258ac5023fa9b..28d27a3f06884 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17475,7 +17475,6 @@ "xpack.lens.indexPattern.existenceErrorLabel": "无法加载字段信息", "xpack.lens.indexPattern.existenceTimeoutAriaLabel": "现有内容提取超时", "xpack.lens.indexPattern.existenceTimeoutLabel": "字段信息花费时间过久", - "xpack.lens.indexPattern.fieldItem.visualizeGeoFieldLinkText": "在 Maps 中可视化", "xpack.lens.indexPattern.fieldItemTooltip": "拖放以可视化。", "xpack.lens.indexPattern.fieldPlaceholder": "字段", "xpack.lens.indexPattern.fieldStatsButtonEmptyLabel": "此字段不包含任何数据,但您仍然可以拖放以进行可视化。", From e3c272153d33e84f361206e3ed15c7eeea60d07b Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 24 Aug 2022 15:41:04 +0200 Subject: [PATCH 61/92] [Discover] Update some tests --- .../discover_sidebar_responsive.test.tsx | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index 05f1e6a960918..03a60d2c41cf2 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -25,6 +25,48 @@ import { AvailableFields$, DataDocuments$, RecordRawType } from '../../hooks/use import { stubLogstashDataView } from '@kbn/data-plugin/common/stubs'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; + +jest.mock('@kbn/unified-field-list-plugin/public/services/field_stats', () => ({ + loadFieldStats: jest.fn().mockResolvedValue({ + totalDocuments: 1624, + sampledDocuments: 1624, + sampledValues: 3248, + topValues: { + buckets: [ + { + count: 1349, + key: 'gif', + }, + { + count: 1206, + key: 'zip', + }, + { + count: 329, + key: 'css', + }, + { + count: 164, + key: 'js', + }, + { + count: 111, + key: 'png', + }, + { + count: 89, + key: 'jpg', + }, + ], + }, + }), +})); + +const dataServiceMock = dataPluginMock.createStartContract(); const mockServices = { history: () => ({ @@ -45,6 +87,9 @@ const mockServices = { if (key === 'fields:popularLimit') { return 5; } + if (key === 'discover:showLegacyFieldTopValues') { + return true; + } }, }, docLinks: { links: { discover: { fieldTypeHelp: '' } } }, @@ -53,6 +98,25 @@ const mockServices = { editDataView: jest.fn(() => true), }, }, + data: { + ...dataServiceMock, + query: { + ...dataServiceMock.query, + timefilter: { + ...dataServiceMock.query.timefilter, + timefilter: { + ...dataServiceMock.query.timefilter.timefilter, + getTime: () => ({ + from: 'now-7d', + to: 'now', + }), + }, + }, + }, + }, + dataViews: dataViewPluginMocks.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), + charts: chartPluginMock.createSetupContract(), } as unknown as DiscoverServices; const mockfieldCounts: Record = {}; @@ -105,7 +169,10 @@ function getCompProps(): DiscoverSidebarResponsiveProps { onAddField: jest.fn(), onRemoveField: jest.fn(), selectedDataView: dataView, - state: {}, + state: { + query: { query: '', language: 'lucene' }, + filters: [], + }, trackUiMetric: jest.fn(), onFieldEdited: jest.fn(), viewMode: VIEW_MODE.DOCUMENT_LEVEL, @@ -118,9 +185,9 @@ describe('discover responsive sidebar', function () { let props: DiscoverSidebarResponsiveProps; let comp: ReactWrapper; - beforeAll(() => { + beforeAll(async () => { props = getCompProps(); - comp = mountWithIntl( + comp = await mountWithIntl( From 49d4cb3fdc96541b404693cc85b324b6dbe2b116 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 25 Aug 2022 10:59:48 +0200 Subject: [PATCH 62/92] [Discover] Fallback to old Discover logic and show examples for non-aggregatable fields --- .../common/utils/field_examples_calculator.ts | 109 ++++++++++++++++++ .../common/utils/field_stats_utils.ts | 102 ++++++++++++---- .../components/field_stats/field_stats.tsx | 14 ++- .../services/field_stats/load_field_stats.ts | 10 +- .../server/routes/field_stats.ts | 5 +- 5 files changed, 208 insertions(+), 32 deletions(-) create mode 100644 src/plugins/unified_field_list/common/utils/field_examples_calculator.ts diff --git a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts new file mode 100644 index 0000000000000..a9e3c853604bb --- /dev/null +++ b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Adapted from src/plugins/discover/public/application/main/components/sidebar/lib/field_calculator.js + +import { map, sortBy, without, each, defaults, isObject } from 'lodash'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { DataView, DataViewField } from '@kbn/data-plugin/common'; +import { flattenHit } from '@kbn/data-plugin/common'; + +type FieldHitValue = any; + +function getFieldValues( + hits: estypes.SearchHit[], + field: DataViewField, + dataView: DataView +): FieldHitValue[] { + return map(hits, function (hit) { + return flattenHit(hit, dataView, { includeIgnoredValues: true })[field.name]; + }); +} + +interface FieldValueCountsParams { + hits: estypes.SearchHit[]; + dataView: DataView; + field: DataViewField; + grouped?: boolean; + count?: number; +} + +export function getFieldValueCounts(params: FieldValueCountsParams) { + params = defaults(params, { + count: 5, + grouped: false, + }); + + if ( + params.field.type === 'geo_point' || + params.field.type === 'geo_shape' || + params.field.type === 'attachment' + ) { + throw new Error('Analysis is not available this field type'); + } + + const allValues = getFieldValues(params.hits, params.field, params.dataView); + const missing = countMissing(allValues); + + const groups = groupValues(allValues, params); + const exampleBuckets = map( + sortBy(groups, 'count').reverse().slice(0, params.count), + function (bucket) { + return { + key: bucket.value, + count: bucket.count, + }; + } + ); + + if (params.hits.length - missing === 0) { + throw new Error('No data for this field in the first found records'); + } + + return { + total: params.hits.length, + exists: params.hits.length - missing, + missing, + buckets: exampleBuckets, + }; +} + +// returns a count of fields in the array that are undefined or null +function countMissing(array: FieldHitValue[]): number { + return array.length - without(array, undefined, null).length; +} + +function groupValues(allValues: FieldHitValue[], params: FieldValueCountsParams) { + const groups: Record = {}; + let k; + + allValues.forEach(function (value) { + if (isObject(value) && !Array.isArray(value)) { + throw new Error('Analysis is not available for object fields.'); + } + + if (Array.isArray(value) && !params.grouped) { + k = value; + } else { + k = value == null ? undefined : [value]; + } + + each(k, function (key) { + if (groups.hasOwnProperty(key)) { + groups[key].count++; + } else { + groups[key] = { + value: params.grouped ? value : key, + count: 1, + }; + } + }); + }); + + return groups; +} diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index 685590bb453e2..62f00ac3fcbf4 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -8,16 +8,24 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import DateMath from '@kbn/datemath'; -import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; -import type { DataViewFieldBase } from '@kbn/es-query'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import type { ESSearchResponse } from '@kbn/core/types/elasticsearch'; import type { FieldStatsResponse } from '../types'; +import { getFieldValueCounts } from './field_examples_calculator'; -export type SearchHandler = ( - aggs: Record -) => Promise>; +export type SearchHandler = ({ + aggs, + fields, + size, +}: { + aggs?: Record; + fields?: object[]; + size?: number; +}) => Promise>; const SHARD_SIZE = 5000; const DEFAULT_TOP_VALUES_SIZE = 10; +const SIMPLE_EXAMPLES_SIZE = 100; export function buildSearchParams({ dataViewPattern, @@ -27,6 +35,8 @@ export function buildSearchParams({ dslQuery, runtimeMappings, aggs, + fields, + size, }: { dataViewPattern: string; timeFieldName?: string; @@ -34,7 +44,9 @@ export function buildSearchParams({ toDate: string; dslQuery: object; runtimeMappings: estypes.MappingRuntimeFields; - aggs: Record; + aggs?: Record; + fields?: object[]; + size?: number; }) { const filter = timeFieldName ? [ @@ -50,6 +62,12 @@ export function buildSearchParams({ ] : [dslQuery]; + if (fields?.length === 1) { + filter.push({ + exists: fields[0], + }); + } + const query = { bool: { filter, @@ -61,26 +79,34 @@ export function buildSearchParams({ body: { query, aggs, + fields, runtime_mappings: runtimeMappings, + _source: fields?.length ? false : undefined, }, track_total_hits: true, - size: 0, + size: size ?? 0, }; } export async function fetchAndCalculateFieldStats({ searchHandler, + dataView, field, fromDate, toDate, size, }: { searchHandler: SearchHandler; - field: DataViewFieldBase; + dataView: DataView; + field: DataViewField; fromDate: string; toDate: string; size?: number; }) { + if (!field.aggregatable) { + return await getSimpleExamples(searchHandler, field, dataView); + } + if (!canProvideStatsForField(field)) { return {}; } @@ -100,19 +126,20 @@ export async function fetchAndCalculateFieldStats({ return await getStringSamples(searchHandler, field, size); } -export function canProvideStatsForField(field: DataViewFieldBase): boolean { +export function canProvideStatsForField(field: DataViewField): boolean { return !( field.type === 'document' || field.type.includes('range') || field.type === 'geo_point' || field.type === 'geo_shape' || - field.type === 'murmur3' + field.type === 'murmur3' || + field.type === 'attachment' ); } export async function getNumberHistogram( aggSearchWithBody: SearchHandler, - field: DataViewFieldBase, + field: DataViewField, useTopHits = true ): Promise> { const fieldRef = getFieldRef(field); @@ -144,9 +171,9 @@ export async function getNumberHistogram( }, }; - const minMaxResult = (await aggSearchWithBody( - useTopHits ? searchWithHits : searchWithoutHits - )) as + const minMaxResult = (await aggSearchWithBody({ + aggs: useTopHits ? searchWithHits : searchWithoutHits, + })) as | ESSearchResponse | ESSearchResponse; @@ -200,7 +227,7 @@ export async function getNumberHistogram( }, }, }; - const histogramResult = (await aggSearchWithBody(histogramBody)) as ESSearchResponse< + const histogramResult = (await aggSearchWithBody({ aggs: histogramBody })) as ESSearchResponse< unknown, { body: { aggs: typeof histogramBody } } >; @@ -221,7 +248,7 @@ export async function getNumberHistogram( export async function getStringSamples( aggSearchWithBody: SearchHandler, - field: DataViewFieldBase, + field: DataViewField, size = DEFAULT_TOP_VALUES_SIZE ): Promise> { const fieldRef = getFieldRef(field); @@ -240,7 +267,7 @@ export async function getStringSamples( }, }, }; - const topValuesResult = (await aggSearchWithBody(topValuesBody)) as ESSearchResponse< + const topValuesResult = (await aggSearchWithBody({ aggs: topValuesBody })) as ESSearchResponse< unknown, { body: { aggs: typeof topValuesBody } } >; @@ -261,7 +288,7 @@ export async function getStringSamples( // This one is not sampled so that it returns the full date range export async function getDateHistogram( aggSearchWithBody: SearchHandler, - field: DataViewFieldBase, + field: DataViewField, range: { fromDate: string; toDate: string } ): Promise> { const fromDate = DateMath.parse(range.fromDate); @@ -287,7 +314,7 @@ export async function getDateHistogram( const histogramBody = { histo: { date_histogram: { ...getFieldRef(field), fixed_interval: fixedInterval } }, }; - const results = (await aggSearchWithBody(histogramBody)) as ESSearchResponse< + const results = (await aggSearchWithBody({ aggs: histogramBody })) as ESSearchResponse< unknown, { body: { aggs: typeof histogramBody } } >; @@ -303,7 +330,42 @@ export async function getDateHistogram( }; } -function getFieldRef(field: DataViewFieldBase) { +export async function getSimpleExamples( + search: SearchHandler, + field: DataViewField, + dataView: DataView +): Promise> { + try { + const fieldRef = getFieldRef(field); + + const simpleExamplesBody = { + size: SIMPLE_EXAMPLES_SIZE, + fields: [fieldRef], + }; + + const simpleExamplesResult = await search(simpleExamplesBody); + + const groupedSimpleExamples = getFieldValueCounts({ + hits: simpleExamplesResult.hits.hits, + field, + dataView, + }); + + return { + totalDocuments: getHitsTotal(simpleExamplesResult), + sampledDocuments: groupedSimpleExamples.total, // TODO: check if that's correct mapping + sampledValues: groupedSimpleExamples.exists, // TODO: check if that's correct mapping + topValues: { + buckets: groupedSimpleExamples.buckets, + }, + }; + } catch (error) { + // console.error(error) + return {}; + } +} + +function getFieldRef(field: DataViewField) { return field.scripted ? { script: { diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 57b9d55457d91..1fa4a707dd23d 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -137,7 +137,7 @@ const FieldStatsComponent: React.FC = ({ setDataView(loadedDataView); - if (state.isLoading || !canProvideStatsForField(field)) { + if (state.isLoading) { return; } @@ -296,7 +296,7 @@ const FieldStatsComponent: React.FC = ({ return overrideMissingContent ? overrideMissingContent({ - noDataFound: canProvideStatsForField(field), + noDataFound: canProvideStatsForField(field), // TODO: should we have different messaging? element: message, }) : message; @@ -344,9 +344,13 @@ const FieldStatsComponent: React.FC = ({ title = (
- {i18n.translate('unifiedFieldList.fieldStats.topValuesLabel', { - defaultMessage: 'Top values', - })} + {field.aggregatable + ? i18n.translate('unifiedFieldList.fieldStats.topValuesLabel', { + defaultMessage: 'Top values', + }) + : i18n.translate('unifiedFieldList.fieldStats.examplesLabel', { + defaultMessage: 'Examples', + })}
); diff --git a/src/plugins/unified_field_list/public/services/field_stats/load_field_stats.ts b/src/plugins/unified_field_list/public/services/field_stats/load_field_stats.ts index fd38cd0bfca64..9cf468d29fe91 100644 --- a/src/plugins/unified_field_list/public/services/field_stats/load_field_stats.ts +++ b/src/plugins/unified_field_list/public/services/field_stats/load_field_stats.ts @@ -7,9 +7,8 @@ */ import { lastValueFrom } from 'rxjs'; -import type { DataView } from '@kbn/data-views-plugin/common'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { DataViewFieldBase } from '@kbn/es-query'; import type { FieldStatsResponse } from '../../../common/types'; import { fetchAndCalculateFieldStats, @@ -22,7 +21,7 @@ interface FetchFieldStatsParams { data: DataPublicPluginStart; }; dataView: DataView; - field: DataViewFieldBase; + field: DataViewField; fromDate: string; toDate: string; dslQuery: object; @@ -62,7 +61,7 @@ export const loadFieldStats: LoadFieldStatsHandler = async ({ return {}; } - const searchHandler: SearchHandler = async (aggs) => { + const searchHandler: SearchHandler = async (body) => { const result = await lastValueFrom( data.search.search( { @@ -73,7 +72,7 @@ export const loadFieldStats: LoadFieldStatsHandler = async ({ toDate, dslQuery, runtimeMappings: dataView.getRuntimeMappings(), - aggs, + ...body, }), }, { @@ -86,6 +85,7 @@ export const loadFieldStats: LoadFieldStatsHandler = async ({ return await fetchAndCalculateFieldStats({ searchHandler, + dataView, field, fromDate, toDate, diff --git a/src/plugins/unified_field_list/server/routes/field_stats.ts b/src/plugins/unified_field_list/server/routes/field_stats.ts index 4fc654f892210..143389f524815 100644 --- a/src/plugins/unified_field_list/server/routes/field_stats.ts +++ b/src/plugins/unified_field_list/server/routes/field_stats.ts @@ -57,7 +57,7 @@ export async function initFieldStatsRoute(setup: CoreSetup) { throw new Error(`Field {fieldName} not found in data view ${dataView.title}`); } - const searchHandler: SearchHandler = async (aggs) => { + const searchHandler: SearchHandler = async (body) => { const result = await requestClient.search( buildSearchParams({ dataViewPattern: dataView.title, @@ -66,7 +66,7 @@ export async function initFieldStatsRoute(setup: CoreSetup) { toDate, dslQuery, runtimeMappings: dataView.getRuntimeMappings(), - aggs, + ...body, }) ); return result; @@ -74,6 +74,7 @@ export async function initFieldStatsRoute(setup: CoreSetup) { const stats = await fetchAndCalculateFieldStats({ searchHandler, + dataView, field, fromDate, toDate, From 210d5be60c31f311b87617cc8ecafa507099d475 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 25 Aug 2022 11:10:55 +0200 Subject: [PATCH 63/92] [Discover] Exclude vector fields --- .../common/utils/field_examples_calculator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts index a9e3c853604bb..41ae6834a254b 100644 --- a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts +++ b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts @@ -42,7 +42,8 @@ export function getFieldValueCounts(params: FieldValueCountsParams) { if ( params.field.type === 'geo_point' || params.field.type === 'geo_shape' || - params.field.type === 'attachment' + params.field.type === 'attachment' || + params.field.type === 'unknown' ) { throw new Error('Analysis is not available this field type'); } From 15a715122a30c642f3a46f7f4f0ac86157b205eb Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 25 Aug 2022 14:29:15 +0200 Subject: [PATCH 64/92] [Discover] Don't call details unless for legacy code --- .../sidebar/discover_field.test.tsx | 51 +++++++++++++++---- .../components/sidebar/discover_field.tsx | 11 ++-- .../sidebar/discover_field_visualize.tsx | 11 ++-- .../components/sidebar/discover_sidebar.tsx | 7 ++- .../components/sidebar/lib/get_details.ts | 2 - .../main/components/sidebar/types.ts | 1 - .../components/field_stats/field_stats.tsx | 2 +- 7 files changed, 60 insertions(+), 25 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx index 87f301c662df4..12154dfc21e46 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx @@ -9,12 +9,17 @@ import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { mountWithIntl } from '@kbn/test-jest-helpers'; - -import { DiscoverField } from './discover_field'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { DiscoverField, DiscoverFieldProps } from './discover_field'; import { DataViewField } from '@kbn/data-views-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { stubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +const dataServiceMock = dataPluginMock.createStartContract(); + jest.mock('../../../../kibana_services', () => ({ getUiActions: jest.fn(() => { return { @@ -25,12 +30,12 @@ jest.mock('../../../../kibana_services', () => ({ function getComponent({ selected = false, - showDetails = false, + showFieldStats = false, field, onAddFilterExists = true, }: { selected?: boolean; - showDetails?: boolean; + showFieldStats?: boolean; field?: DataViewField; onAddFilterExists?: boolean; }) { @@ -47,16 +52,20 @@ function getComponent({ readFromDocValues: true, }); - const props = { + const props: DiscoverFieldProps = { dataView: stubDataView, field: finalField, - getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: 2, columns: [] })), + getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: 2 })), ...(onAddFilterExists && { onAddFilter: jest.fn() }), onAddField: jest.fn(), onRemoveField: jest.fn(), - showDetails, + showFieldStats, selected, - state: {}, + state: { + query: { query: '', language: 'lucene' }, + filters: [], + }, + contextualFields: [], }; const services = { history: () => ({ @@ -74,8 +83,30 @@ function getComponent({ if (key === 'fields:popularLimit') { return 5; } + if (key === 'discover:showLegacyFieldTopValues') { + return true; + } }, }, + data: { + ...dataServiceMock, + query: { + ...dataServiceMock.query, + timefilter: { + ...dataServiceMock.query.timefilter, + timefilter: { + ...dataServiceMock.query.timefilter.timefilter, + getTime: () => ({ + from: 'now-7d', + to: 'now', + }), + }, + }, + }, + }, + dataViews: dataViewPluginMocks.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), + charts: chartPluginMock.createSetupContract(), }; const comp = mountWithIntl( @@ -97,7 +128,7 @@ describe('discover sidebar field', function () { expect(props.onRemoveField).toHaveBeenCalledWith('bytes'); }); it('should trigger getDetails', function () { - const { comp, props } = getComponent({ selected: true }); + const { comp, props } = getComponent({ selected: true, showFieldStats: true }); findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); expect(props.getDetails).toHaveBeenCalledWith(props.field); }); @@ -138,7 +169,7 @@ describe('discover sidebar field', function () { expect(props.getDetails.mock.calls.length).toEqual(0); }); it('should execute getDetails when show details is requested', function () { - const { props, comp } = getComponent({}); + const { props, comp } = getComponent({ showFieldStats: true }); findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); expect(props.getDetails.mock.calls.length).toEqual(1); }); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 894124fdcefb4..775e78e2a4bbb 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -271,6 +271,11 @@ export interface DiscoverFieldProps { * Discover App State */ state: AppState; + + /** + * Columns + */ + contextualFields: string[]; } function DiscoverFieldComponent({ @@ -288,6 +293,7 @@ function DiscoverFieldComponent({ onDeleteField, showFieldStats, state, + contextualFields, }: DiscoverFieldProps) { const services = useDiscoverServices(); const { data } = services; @@ -407,7 +413,6 @@ function DiscoverFieldComponent({ } const renderPopover = () => { - const details = getDetails(field); const dateRange = data?.query?.timefilter.timefilter.getTime(); const fieldForStats = multiFields ? multiFields[0].field : field; // TODO: how to handle multifields? const showLegacyFieldStats = services.uiSettings.get(SHOW_LEGACY_FIELD_TOP_VALUES); @@ -428,7 +433,7 @@ function DiscoverFieldComponent({ @@ -469,7 +474,7 @@ function DiscoverFieldComponent({ dataView={dataView} multiFields={rawMultiFields} trackUiMetric={trackUiMetric} - details={details} + contextualFields={contextualFields} /> ); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_visualize.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_visualize.tsx index 3f89b5b47a0cf..04d53a0165074 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_visualize.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field_visualize.tsx @@ -11,27 +11,26 @@ import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { triggerVisualizeActions, VisualizeInformation } from './lib/visualize_trigger_utils'; -import type { FieldDetails } from './types'; import { getVisualizeInformation } from './lib/visualize_trigger_utils'; import { DiscoverFieldVisualizeInner } from './discover_field_visualize_inner'; interface Props { field: DataViewField; dataView: DataView; - details: FieldDetails; multiFields?: DataViewField[]; + contextualFields: string[]; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; } export const DiscoverFieldVisualize: React.FC = React.memo( - ({ field, dataView, details, trackUiMetric, multiFields }) => { + ({ field, dataView, contextualFields, trackUiMetric, multiFields }) => { const [visualizeInfo, setVisualizeInfo] = useState(); useEffect(() => { - getVisualizeInformation(field, dataView.id, details.columns, multiFields).then( + getVisualizeInformation(field, dataView.id, contextualFields, multiFields).then( setVisualizeInfo ); - }, [details.columns, field, dataView, multiFields]); + }, [contextualFields, field, dataView, multiFields]); if (!visualizeInfo) { return null; @@ -41,7 +40,7 @@ export const DiscoverFieldVisualize: React.FC = React.memo( // regular link click. let the uiActions code handle the navigation and show popup if needed event.preventDefault(); trackUiMetric?.(METRIC_TYPE.CLICK, 'visualize_link_click'); - triggerVisualizeActions(visualizeInfo.field, dataView.id, details.columns); + triggerVisualizeActions(visualizeInfo.field, dataView.id, contextualFields); }; return ( diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index 8b59f35cf6d51..a4a351eebb337 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx @@ -145,8 +145,8 @@ export function DiscoverSidebarComponent({ ); const getDetailsByField = useCallback( - (ipField: DataViewField) => getDetails(ipField, documents, columns, selectedDataView), - [documents, columns, selectedDataView] + (ipField: DataViewField) => getDetails(ipField, documents, selectedDataView), + [documents, selectedDataView] ); const popularLimit = useMemo(() => uiSettings.get(FIELDS_LIMIT_SETTING), [uiSettings]); @@ -414,6 +414,7 @@ export function DiscoverSidebarComponent({ onDeleteField={deleteField} showFieldStats={showFieldStats} state={state} + contextualFields={columns} /> ); @@ -475,6 +476,7 @@ export function DiscoverSidebarComponent({ onDeleteField={deleteField} showFieldStats={showFieldStats} state={state} + contextualFields={columns} /> ); @@ -505,6 +507,7 @@ export function DiscoverSidebarComponent({ onDeleteField={deleteField} showFieldStats={showFieldStats} state={state} + contextualFields={columns} /> ); diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/get_details.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/get_details.ts index d022116745f7c..cc9f56b73feed 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/get_details.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/get_details.ts @@ -14,7 +14,6 @@ import { DataTableRecord } from '../../../../../types'; export function getDetails( field: DataViewField, hits: DataTableRecord[] | undefined, - columns: string[], dataView?: DataView ) { if (!dataView || !hits) { @@ -27,7 +26,6 @@ export function getDetails( count: 5, grouped: false, }), - columns, }; if (details.buckets) { for (const bucket of details.buckets) { diff --git a/src/plugins/discover/public/application/main/components/sidebar/types.ts b/src/plugins/discover/public/application/main/components/sidebar/types.ts index e434d55f729c2..45921f676f144 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/types.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/types.ts @@ -17,7 +17,6 @@ export interface FieldDetails { exists: number; total: number; buckets: Bucket[]; - columns: string[]; } export interface Bucket { diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 1fa4a707dd23d..c844eae7b12b7 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -288,7 +288,7 @@ const FieldStatsComponent: React.FC = ({ message={i18n.translate( 'unifiedFieldList.fieldStats.notAvailableForThisFieldWithoutDataDescription', { - defaultMessage: `Summary information is not available for this field.`, + defaultMessage: `Analysis is not available for this field.`, } )} /> From f41f12ad99ed916d8adf0a8419eab146c27333b1 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 25 Aug 2022 16:28:58 +0200 Subject: [PATCH 65/92] [Discover] Fix types --- .../sidebar/__stories__/discover_field_details.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/__stories__/discover_field_details.stories.tsx b/src/plugins/discover/public/application/main/components/sidebar/__stories__/discover_field_details.stories.tsx index fa39f179d33ec..9b02ffd15a282 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/__stories__/discover_field_details.stories.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/__stories__/discover_field_details.stories.tsx @@ -88,7 +88,7 @@ storiesOf('components/sidebar/DiscoverFieldDetails', module) {}} /> )); From 194e1f984b8acffad79a94eb45771ad877917e91 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 25 Aug 2022 16:37:22 +0200 Subject: [PATCH 66/92] [Discover] Small update for stories --- .../__mocks__/__storybook_mocks__/with_discover_services.tsx | 4 ++++ .../main/components/layout/__stories__/get_layout_props.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/plugins/discover/public/__mocks__/__storybook_mocks__/with_discover_services.tsx b/src/plugins/discover/public/__mocks__/__storybook_mocks__/with_discover_services.tsx index faf1f063e9e8d..d8309e452f7a1 100644 --- a/src/plugins/discover/public/__mocks__/__storybook_mocks__/with_discover_services.tsx +++ b/src/plugins/discover/public/__mocks__/__storybook_mocks__/with_discover_services.tsx @@ -70,6 +70,10 @@ const services = { getAbsoluteTime: () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }, + getTime: () => ({ + from: 'now-7d', + to: 'now', + }), }, }, savedQueries: { findSavedQueries: () => Promise.resolve({ queries: [] as SavedQuery[] }) }, diff --git a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts index 7c24b4641485d..a4c9227a2c072 100644 --- a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts +++ b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts @@ -172,6 +172,7 @@ export function getDocumentsLayoutProps(dataView: DataView) { language: 'kuery', query: '', }, + filters: [], }, } as unknown as DiscoverLayoutProps; } @@ -186,6 +187,7 @@ export const getPlainRecordLayoutProps = (dataView: DataView) => { query: { sql: 'SELECT * FROM "kibana_sample_data_ecommerce"', }, + filters: [], }, } as unknown as DiscoverLayoutProps; }; From 7baeb563de581d9be8312f841bfac7506c2c91e8 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 25 Aug 2022 17:22:50 +0200 Subject: [PATCH 67/92] [Discover] Adapt tests --- .../sidebar/discover_field.test.tsx | 4 +- .../utils/field_examples_calculator.test.ts | 289 ++++++++++++++++++ .../common/utils/field_examples_calculator.ts | 26 +- .../common/utils/field_stats_utils.ts | 10 +- 4 files changed, 309 insertions(+), 20 deletions(-) create mode 100644 src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx index 12154dfc21e46..dde38091c82ff 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx @@ -166,12 +166,12 @@ describe('discover sidebar field', function () { }); it('should not execute getDetails when rendered, since it can be expensive', function () { const { props } = getComponent({}); - expect(props.getDetails.mock.calls.length).toEqual(0); + expect(props.getDetails).toHaveBeenCalledTimes(0); }); it('should execute getDetails when show details is requested', function () { const { props, comp } = getComponent({ showFieldStats: true }); findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); - expect(props.getDetails.mock.calls.length).toEqual(1); + expect(props.getDetails).toHaveBeenCalledTimes(1); }); it('should not return the popover if onAddFilter is not provided', function () { const field = new DataViewField({ diff --git a/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts b/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts new file mode 100644 index 0000000000000..1f7b6b6bafa50 --- /dev/null +++ b/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts @@ -0,0 +1,289 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { keys, clone, uniq, filter, map, flatten } from 'lodash'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { + getFieldExampleBuckets, + countMissing, + groupValues, + getFieldValues, +} from './field_examples_calculator'; + +const hitsAsValues: Array> = [ + { + extension: 'html', + bytes: 360.20000000000005, + }, + { + extension: 'gif', + bytes: 5848.700000000001, + }, + { + extension: 'png', + bytes: 841.6, + }, + { + extension: 'html', + bytes: 1626.4, + }, + { + extension: 'php', + bytes: 2070.6, + phpmemory: 276080, + }, + { + extension: 'gif', + bytes: 8421.6, + }, + { + extension: 'html', + bytes: 994.8000000000001, + }, + { + extension: 'html', + bytes: 374, + }, + { + extension: 'php', + bytes: 506.09999999999997, + phpmemory: 67480, + }, + { + extension: 'php', + bytes: 506.09999999999997, + phpmemory: 67480, + }, + { + extension: 'php', + bytes: 2591.1, + phpmemory: 345480, + }, + { + extension: 'html', + bytes: 1450, + }, + { + extension: 'php', + bytes: 1803.8999999999999, + phpmemory: 240520, + }, + { + extension: 'html', + bytes: 1626.4, + }, + { + extension: 'gif', + bytes: 10617.2, + }, + { + extension: 'gif', + bytes: 10961.5, + }, + { + extension: 'html', + bytes: 382.8, + }, + { + extension: 'html', + bytes: 374, + }, + { + extension: 'png', + bytes: 3059.2000000000003, + }, + { + extension: 'gif', + bytes: 10617.2, + }, +]; + +const hits = hitsAsValues.map((value) => ({ + _index: 'logstash-2014.09.09', + _id: '1945', + _score: 1, + fields: Object.keys(value).reduce( + (result: Record>, fieldName: string) => { + result[fieldName] = [value[fieldName]]; + return result; + }, + {} + ), +})); + +describe('fieldExamplesCalculator', function () { + it('should have a _countMissing that counts nulls & undefineds in an array', function () { + const values = [ + ['foo', 'bar'], + 'foo', + 'foo', + undefined, + ['foo', 'bar'], + 'bar', + 'baz', + null, + null, + null, + 'foo', + undefined, + ]; + expect(countMissing(values)).toBe(5); + }); + + describe('groupValues', function () { + let groups: Record; + let params: any; + let values: any; + beforeEach(function () { + values = [ + ['foo', 'bar'], + 'foo', + 'foo', + undefined, + ['foo', 'bar'], + 'bar', + 'baz', + null, + null, + null, + 'foo', + undefined, + ]; + params = {}; + groups = groupValues(values, params); + }); + + it('should have a groupValues that counts values', function () { + expect(groups).toBeInstanceOf(Object); + }); + + it('should throw an error if any value is a plain object', function () { + expect(function () { + groupValues([{}, true, false], params); + }).toThrowError(); + }); + + it('should handle values with dots in them', function () { + values = ['0', '0.........', '0.......,.....']; + params = {}; + groups = groupValues(values, params); + expect(groups[values[0]].count).toBe(1); + expect(groups[values[1]].count).toBe(1); + expect(groups[values[2]].count).toBe(1); + }); + + it('should have a a key for value in the array when not grouping array terms', function () { + expect(keys(groups).length).toBe(3); + expect(groups.foo).toBeInstanceOf(Object); + expect(groups.bar).toBeInstanceOf(Object); + expect(groups.baz).toBeInstanceOf(Object); + }); + + it('should count array terms independently', function () { + expect(groups['foo,bar']).toBe(undefined); + expect(groups.foo.count).toBe(5); + expect(groups.bar.count).toBe(3); + expect(groups.baz.count).toBe(1); + }); + + describe('grouped array terms', function () { + beforeEach(function () { + params.grouped = true; + groups = groupValues(values, params); + }); + + it('should group array terms when passed params.grouped', function () { + expect(keys(groups).length).toBe(4); + expect(groups['foo,bar']).toBeInstanceOf(Object); + }); + + it('should contain the original array as the value', function () { + expect(groups['foo,bar'].value).toEqual(['foo', 'bar']); + }); + + it('should count the pairs separately from the values they contain', function () { + expect(groups['foo,bar'].count).toBe(2); + expect(groups.foo.count).toBe(3); + expect(groups.bar.count).toBe(1); + }); + }); + }); + + describe('getFieldValues', function () { + it('Should return an array of values for _source fields', function () { + const extensions = getFieldValues(hits, dataView.fields.getByName('extension')!, dataView); + expect(extensions).toBeInstanceOf(Array); + expect( + filter(extensions, function (v) { + return v.includes('html'); + }).length + ).toBe(8); + expect(uniq(flatten(clone(extensions))).sort()).toEqual(['gif', 'html', 'php', 'png']); + }); + + it('Should return an array of values for core meta fields', function () { + const types = getFieldValues(hits, dataView.fields.getByName('_id')!, dataView); + expect(types).toBeInstanceOf(Array); + expect(types.length).toBe(20); + }); + }); + + describe('getFieldExampleBuckets', function () { + let params: { hits: any; field: any; count: number; dataView: DataView }; + beforeEach(function () { + params = { + hits, + field: dataView.fields.getByName('extension'), + count: 3, + dataView, + }; + }); + + it('counts the top 3 values', function () { + const extensions = getFieldExampleBuckets(params); + expect(extensions).toBeInstanceOf(Object); + expect(extensions.buckets).toBeInstanceOf(Array); + expect(extensions.buckets.length).toBe(3); + expect(map(extensions.buckets, 'key')).toEqual(['html', 'php', 'gif']); + }); + + it('fails to analyze geo and attachment types', function () { + params.field = dataView.fields.getByName('point'); + expect(() => getFieldExampleBuckets(params)).toThrowError( + 'Analysis is not available this field type' + ); + + params.field = dataView.fields.getByName('area'); + expect(() => getFieldExampleBuckets(params)).toThrowError( + 'Analysis is not available this field type' + ); + + params.field = dataView.fields.getByName('request_body'); + expect(() => getFieldExampleBuckets(params)).toThrowError( + 'Analysis is not available this field type' + ); + }); + + it('fails to analyze fields that are in the mapping, but not the hits', function () { + params.field = dataView.fields.getByName('ip'); + expect(() => getFieldExampleBuckets(params)).toThrowError( + 'No data for this field in the first found records' + ); + }); + + it('counts the total hits', function () { + expect(getFieldExampleBuckets(params).total).toBe(params.hits.length); + }); + + it('counts the hits the field exists in', function () { + params.field = dataView.fields.getByName('phpmemory'); + expect(getFieldExampleBuckets(params).exists).toBe(5); + }); + }); +}); diff --git a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts index 41ae6834a254b..7ff39b077dd40 100644 --- a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts +++ b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts @@ -15,16 +15,6 @@ import { flattenHit } from '@kbn/data-plugin/common'; type FieldHitValue = any; -function getFieldValues( - hits: estypes.SearchHit[], - field: DataViewField, - dataView: DataView -): FieldHitValue[] { - return map(hits, function (hit) { - return flattenHit(hit, dataView, { includeIgnoredValues: true })[field.name]; - }); -} - interface FieldValueCountsParams { hits: estypes.SearchHit[]; dataView: DataView; @@ -33,7 +23,7 @@ interface FieldValueCountsParams { count?: number; } -export function getFieldValueCounts(params: FieldValueCountsParams) { +export function getFieldExampleBuckets(params: FieldValueCountsParams) { params = defaults(params, { count: 5, grouped: false, @@ -74,12 +64,22 @@ export function getFieldValueCounts(params: FieldValueCountsParams) { }; } +export function getFieldValues( + hits: estypes.SearchHit[], + field: DataViewField, + dataView: DataView +): FieldHitValue[] { + return map(hits, function (hit) { + return flattenHit(hit, dataView, { includeIgnoredValues: true })[field.name]; + }); +} + // returns a count of fields in the array that are undefined or null -function countMissing(array: FieldHitValue[]): number { +export function countMissing(array: FieldHitValue[]): number { return array.length - without(array, undefined, null).length; } -function groupValues(allValues: FieldHitValue[], params: FieldValueCountsParams) { +export function groupValues(allValues: FieldHitValue[], params: FieldValueCountsParams) { const groups: Record = {}; let k; diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index 62f00ac3fcbf4..a31e1db89868c 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -11,7 +11,7 @@ import DateMath from '@kbn/datemath'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import type { ESSearchResponse } from '@kbn/core/types/elasticsearch'; import type { FieldStatsResponse } from '../types'; -import { getFieldValueCounts } from './field_examples_calculator'; +import { getFieldExampleBuckets } from './field_examples_calculator'; export type SearchHandler = ({ aggs, @@ -345,7 +345,7 @@ export async function getSimpleExamples( const simpleExamplesResult = await search(simpleExamplesBody); - const groupedSimpleExamples = getFieldValueCounts({ + const fieldExampleBuckets = getFieldExampleBuckets({ hits: simpleExamplesResult.hits.hits, field, dataView, @@ -353,10 +353,10 @@ export async function getSimpleExamples( return { totalDocuments: getHitsTotal(simpleExamplesResult), - sampledDocuments: groupedSimpleExamples.total, // TODO: check if that's correct mapping - sampledValues: groupedSimpleExamples.exists, // TODO: check if that's correct mapping + sampledDocuments: fieldExampleBuckets.total, // TODO: check if that's correct mapping + sampledValues: fieldExampleBuckets.exists, // TODO: check if that's correct mapping topValues: { - buckets: groupedSimpleExamples.buckets, + buckets: fieldExampleBuckets.buckets, }, }; } catch (error) { From 953409cc923252a6b4744b5f26876f001c3bc04f Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 25 Aug 2022 18:02:26 +0200 Subject: [PATCH 68/92] [Discover] Update tests --- .../field_stats/field_stats.test.tsx | 113 +++++++++++++----- 1 file changed, 85 insertions(+), 28 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx index 7a45f4c98fdfa..12982583ee62a 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -101,10 +101,7 @@ describe('UnifiedFieldList ', () => { defaultProps = { services: mockedServices, dataViewOrDataViewId: dataView, - field: { - name: 'bytes', - type: 'number', - } as unknown as DataViewField, + field: dataView.fields.find((f) => f.name === 'bytes')!, fromDate: 'now-7d', toDate: 'now', query: { query: '', language: 'lucene' }, @@ -200,50 +197,36 @@ describe('UnifiedFieldList ', () => { it('should not request field stats for range fields', async () => { const wrapper = await mountWithIntl( - + f.name === 'ip_range')!} /> ); await wrapper.update(); - expect(loadFieldStats).not.toHaveBeenCalled(); + expect(loadFieldStats).toHaveBeenCalled(); + + expect(wrapper.text()).toBe('Analysis is not available for this field.'); }); it('should not request field stats for geo fields', async () => { const wrapper = await mountWithIntl( - + f.name === 'geo_shape')!} /> ); await wrapper.update(); - expect(loadFieldStats).not.toHaveBeenCalled(); + expect(loadFieldStats).toHaveBeenCalled(); + + expect(wrapper.text()).toBe('Analysis is not available for this field.'); }); it('should render nothing if no data is found', async () => { - const wrapper = mountWithIntl(); + const wrapper = await mountWithIntl(); await wrapper.update(); expect(loadFieldStats).toHaveBeenCalled(); - expect(wrapper.text()).toBe(''); + expect(wrapper.text()).toBe('Analysis is not available for this field.'); }); it('should render Top Values field stats correctly for a keyword field', async () => { @@ -349,6 +332,80 @@ describe('UnifiedFieldList ', () => { ); }); + it('should render Examples correctly for a non-aggregatable field', async () => { + let resolveFunction: (arg: unknown) => void; + + (loadFieldStats as jest.Mock).mockImplementation(() => { + return new Promise((resolve) => { + resolveFunction = resolve; + }); + }); + + const wrapper = mountWithIntl( + + ); + + await wrapper.update(); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); + + await act(async () => { + resolveFunction!({ + totalDocuments: 1624, + sampledDocuments: 1624, + sampledValues: 3248, + topValues: { + buckets: [ + { + count: 1349, + key: 'success', + }, + { + count: 1206, + key: 'info', + }, + { + count: 329, + key: 'security', + }, + { + count: 164, + key: 'warning', + }, + { + count: 111, + key: 'error', + }, + { + count: 89, + key: 'login', + }, + ], + }, + }); + }); + + await wrapper.update(); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(EuiProgress)).toHaveLength(6); + + expect(loadFieldStats).toHaveBeenCalledTimes(1); + + expect(wrapper.text()).toBe( + 'Examples"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%100% of 1624 documents' + ); + }); + it('should render Histogram field stats correctly for a date field', async () => { let resolveFunction: (arg: unknown) => void; From 42ea0edaf3bbade620541058fcebd274224abdef Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 26 Aug 2022 13:40:56 +0200 Subject: [PATCH 69/92] [Discover] Update tests --- .../public/services/index.tsx | 1 - .../field_item.test.tsx | 33 ++++++++++++------- .../indexpattern_datasource/field_item.tsx | 2 +- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/plugins/unified_field_list/public/services/index.tsx b/src/plugins/unified_field_list/public/services/index.tsx index c612fb2b1e917..84f75d1bb50e6 100755 --- a/src/plugins/unified_field_list/public/services/index.tsx +++ b/src/plugins/unified_field_list/public/services/index.tsx @@ -7,4 +7,3 @@ */ export { loadFieldStats } from './field_stats'; -export { canProvideStatsForField } from '../../common/utils/field_stats_utils'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 615279b71b82c..1a5de5e77bba9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -22,7 +22,8 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { loadFieldStats } from '@kbn/unified-field-list-plugin/public'; +import { loadFieldStats } from '@kbn/unified-field-list-plugin/public/services/field_stats'; +import { FieldStats } from '@kbn/unified-field-list-plugin/public'; import { DOCUMENT_FIELD_NAME } from '../../common'; jest.mock('@kbn/unified-field-list-plugin/public/services/field_stats', () => ({ @@ -33,7 +34,9 @@ const chartsThemeService = chartPluginMock.createSetupContract().theme; const clickField = async (wrapper: ReactWrapper, field: string) => { await act(async () => { - wrapper.find(`[data-test-subj="lnsFieldListPanelField-${field}"] button`).simulate('click'); + await wrapper + .find(`[data-test-subj="lnsFieldListPanelField-${field}"] button`) + .simulate('click'); }); }; @@ -152,6 +155,11 @@ describe('IndexPattern Field Item', () => { }); }); + beforeEach(() => { + (loadFieldStats as jest.Mock).mockReset(); + (loadFieldStats as jest.Mock).mockImplementation(() => Promise.resolve({})); + }); + it('should display displayName of a field', () => { const wrapper = mountWithIntl(); @@ -168,7 +176,7 @@ describe('IndexPattern Field Item', () => { ); await clickField(wrapper, 'bytes'); - wrapper.update(); + await wrapper.update(); const popoverContent = wrapper.find(EuiPopover).prop('children'); act(() => { mountWithIntl(popoverContent as ReactElement) @@ -190,7 +198,7 @@ describe('IndexPattern Field Item', () => { /> ); await clickField(wrapper, documentField.name); - wrapper.update(); + await wrapper.update(); const popoverContent = wrapper.find(EuiPopover).prop('children'); expect( mountWithIntl(popoverContent as ReactElement) @@ -315,13 +323,10 @@ describe('IndexPattern Field Item', () => { toDate: 'now-7d', field: defaultProps.field, }); - - (loadFieldStats as jest.Mock).mockReset(); - (loadFieldStats as jest.Mock).mockImplementation(() => Promise.resolve({})); }); it('should not request field stats for document field', async () => { - const wrapper = mountWithIntl( + const wrapper = await mountWithIntl( ); @@ -329,13 +334,14 @@ describe('IndexPattern Field Item', () => { await wrapper.update(); - expect(loadFieldStats).not.toHaveBeenCalled(); + expect(loadFieldStats).toHaveBeenCalled(); expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(FieldStats).text()).toBe('Analysis is not available for this field.'); }); it('should not request field stats for range fields', async () => { - const wrapper = mountWithIntl( + const wrapper = await mountWithIntl( { await clickField(wrapper, 'ip_range'); - expect(loadFieldStats).not.toHaveBeenCalled(); + await wrapper.update(); + + expect(loadFieldStats).toHaveBeenCalled(); + expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(FieldStats).text()).toBe('Analysis is not available for this field.'); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index c90b311a7a215..a94f12aeeda8e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -364,7 +364,7 @@ function FieldItemPopoverContents(props: FieldItemProps) { } if (params?.noDataFound) { - // TODO: should we replace this with a default message "Summary is not available for this field?" + // TODO: should we replace this with a default message "Analysis is not available for this field?" const isUsingSampling = core.uiSettings.get('lens:useFieldExistenceSampling'); return ( <> From 86e6f86e289c3ec03519c6614393d2117c4d1f41 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 26 Aug 2022 14:15:08 +0200 Subject: [PATCH 70/92] [Discover] Update tests --- .../components/field_stats/field_top_values_bucket.tsx | 10 ++++++---- .../operations/definitions/terms/helpers.ts | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx index b50b912a89cad..74a890b7e3265 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; +import type { IFieldSubTypeMulti } from '@kbn/es-query'; import type { DataViewField } from '@kbn/data-views-plugin/common'; import type { AddFieldFilterHandler } from '../../types'; @@ -44,6 +45,7 @@ export const FieldTopValuesBucket: React.FC = ({ onAddFilter, }) => { const isFilterButtonHidden = type === 'other'; + const fieldName = (field?.subType as IFieldSubTypeMulti)?.multi?.parent ?? field.name; // TODO: check if this is a right approach for handling multi fields return ( @@ -111,9 +113,9 @@ export const FieldTopValuesBucket: React.FC = ({ onClick={() => onAddFilter(field, fieldValue, '+')} aria-label={i18n.translate('unifiedFieldList.fieldStats.filterValueButtonAriaLabel', { defaultMessage: 'Filter for {field}: "{value}"', - values: { value: formattedFieldValue, field: field.name }, + values: { value: formattedFieldValue, field: fieldName }, })} - data-test-subj={`plus-${field.name}-${fieldValue}`} + data-test-subj={`plus-${fieldName}-${fieldValue}`} style={{ minHeight: 'auto', minWidth: 'auto', @@ -132,10 +134,10 @@ export const FieldTopValuesBucket: React.FC = ({ 'unifiedFieldList.fieldStats.filterOutValueButtonAriaLabel', { defaultMessage: 'Filter out {field}: "{value}"', - values: { value: formattedFieldValue, field: field.name }, + values: { value: formattedFieldValue, field: fieldName }, } )} - data-test-subj={`minus-${field.name}-${fieldValue}`} + data-test-subj={`minus-${fieldName}-${fieldValue}`} style={{ minHeight: 'auto', minWidth: 'auto', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts index 6dec5e3aef6cf..b3378558cfd18 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.ts @@ -10,6 +10,7 @@ import { uniq } from 'lodash'; import type { CoreStart } from '@kbn/core/public'; import { buildEsQuery } from '@kbn/es-query'; import { getEsQueryConfig, DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { FieldStatsResponse, loadFieldStats } from '@kbn/unified-field-list-plugin/public'; import { GenericIndexPatternColumn, operationDefinitionMap } from '..'; import { defaultLabel } from '../filters'; @@ -142,7 +143,7 @@ export function getDisallowedTermsMessage( const response: FieldStatsResponse = await loadFieldStats({ services: { data }, dataView: currentDataView, - field: indexPattern.getFieldByName(fieldNames[0])!, + field: indexPattern.getFieldByName(fieldNames[0])! as DataViewField, dslQuery: buildEsQuery( indexPattern, frame.query, From cc0e700d6d0356fff5744ab8d365bee97d634cce Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 29 Aug 2022 17:47:20 +0200 Subject: [PATCH 71/92] [Discover] Update tests --- .../field_stats/field_stats.test.tsx | 12 +-- .../components/field_stats/field_stats.tsx | 87 +++++++++---------- .../apps/context/_discover_navigation.ts | 2 +- .../discover/group2/_data_grid_context.ts | 2 +- .../apps/management/_scripted_fields.ts | 10 +-- .../_scripted_fields_classic_table.ts | 5 +- 6 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx index 12982583ee62a..22c9a1b9a9d3c 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -324,11 +324,11 @@ describe('UnifiedFieldList ', () => { ).toBe('41.5%'); expect(wrapper.find('[data-test-subj="testing-statsFooter"]').first().text()).toBe( - '100% of 1624 documents' + '100% of 1624 records' ); expect(wrapper.text()).toBe( - 'Top values"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%100% of 1624 documents' + 'Top values"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%100% of 1624 records' ); }); @@ -402,7 +402,7 @@ describe('UnifiedFieldList ', () => { expect(loadFieldStats).toHaveBeenCalledTimes(1); expect(wrapper.text()).toBe( - 'Examples"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%100% of 1624 documents' + 'Examples"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%100% of 1624 records' ); }); @@ -506,10 +506,10 @@ describe('UnifiedFieldList ', () => { expect(wrapper.find('[data-test-subj="testing-topValues"]')).toHaveLength(0); expect(wrapper.find('[data-test-subj="testing-histogram"]')).toHaveLength(1); expect(wrapper.find('[data-test-subj="testing-statsFooter"]').first().text()).toBe( - '13 documents' + '13 records' ); - expect(wrapper.text()).toBe('Time distribution13 documents'); + expect(wrapper.text()).toBe('Time distribution13 records'); }); it('should render Top Values & Distribution field stats correctly for a number field', async () => { @@ -594,7 +594,7 @@ describe('UnifiedFieldList ', () => { expect(loadFieldStats).toHaveBeenCalledTimes(1); expect(wrapper.text()).toBe( - 'Toggle either theTop valuesDistribution1273.9%1326.1%100% of 23 documents' + 'Toggle either theTop valuesDistribution1273.9%1326.1%100% of 23 records' ); }); }); diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index c844eae7b12b7..9ecd94a6a790e 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -251,8 +251,8 @@ const FieldStatsComponent: React.FC = ({ .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) .convert(totalDocuments)}
{' '} - {i18n.translate('unifiedFieldList.fieldStats.ofDocumentsLabel', { - defaultMessage: 'documents', + {i18n.translate('unifiedFieldList.fieldStats.ofRecordsLabel', { + defaultMessage: 'records', })} ) : ( @@ -363,48 +363,47 @@ const FieldStatsComponent: React.FC = ({ if (field.type === 'date') { return combineWithTitleAndFooter( - - - - - - - +
+ + + + + + + +
); } diff --git a/test/functional/apps/context/_discover_navigation.ts b/test/functional/apps/context/_discover_navigation.ts index 46f03b512bff3..52efeefeb6546 100644 --- a/test/functional/apps/context/_discover_navigation.ts +++ b/test/functional/apps/context/_discover_navigation.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; const TEST_COLUMN_NAMES = ['@message']; const TEST_FILTER_COLUMN_NAMES = [ - ['extension', 'jpg'], + ['extension.raw', 'jpg'], ['geo.src', 'IN'], ]; diff --git a/test/functional/apps/discover/group2/_data_grid_context.ts b/test/functional/apps/discover/group2/_data_grid_context.ts index 4903cd446dfc9..a85c97a27ffe9 100644 --- a/test/functional/apps/discover/group2/_data_grid_context.ts +++ b/test/functional/apps/discover/group2/_data_grid_context.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; const TEST_COLUMN_NAMES = ['@message']; const TEST_FILTER_COLUMN_NAMES = [ - ['extension', 'jpg'], + ['extension.raw', 'jpg'], ['geo.src', 'IN'], ]; diff --git a/test/functional/apps/management/_scripted_fields.ts b/test/functional/apps/management/_scripted_fields.ts index 04a3c104c1bc6..03a298182eec0 100644 --- a/test/functional/apps/management/_scripted_fields.ts +++ b/test/functional/apps/management/_scripted_fields.ts @@ -487,12 +487,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should filter by scripted field value in Discover', async function () { - await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); - await log.debug('filter by "Sep 17, 2015 @ 23:00" in the expanded scripted field list'); - await PageObjects.discover.clickFieldListPlusFilter( - scriptedPainlessFieldName2, - '1442531297065' - ); + await PageObjects.header.waitUntilLoadingHasFinished(); + const documentCell = await dataGrid.getCellElement(0, 3); + await documentCell.click(); + await testSubjects.click('filterForButton'); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async function () { diff --git a/test/functional/apps/management/_scripted_fields_classic_table.ts b/test/functional/apps/management/_scripted_fields_classic_table.ts index a6bbe798cf56b..bf39bae566816 100644 --- a/test/functional/apps/management/_scripted_fields_classic_table.ts +++ b/test/functional/apps/management/_scripted_fields_classic_table.ts @@ -48,7 +48,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.setWindowSize(1200, 800); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await kibanaServer.uiSettings.replace({}); - await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); + await kibanaServer.uiSettings.update({ + 'doc_table:legacy': true, + 'discover:showLegacyFieldTopValues': true, + }); }); after(async function afterAll() { From 499b9e30bff4a3d17bc81132849315e2aac38146 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 30 Aug 2022 09:54:43 +0200 Subject: [PATCH 72/92] [Discover] Update tests --- .../components/field_stats/field_stats.tsx | 2 +- .../field_stats/field_top_values.tsx | 10 ++++----- .../field_stats/field_top_values_bucket.tsx | 21 +++++++++++-------- .../discover/group2/_data_grid_context.ts | 8 +++---- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 9ecd94a6a790e..65ff4a1224b5a 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -448,7 +448,7 @@ const FieldStatsComponent: React.FC = ({ field={field} sampledValuesCount={sampledValues!} color={color} - testSubject={dataTestSubject} + data-test-subj={dataTestSubject} onAddFilter={onAddFilter} /> ); diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx index d994e86968e53..0bee95061ae3d 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx @@ -19,7 +19,7 @@ export interface FieldTopValuesProps { field: DataViewField; sampledValuesCount: number; color?: string; - testSubject: string; + 'data-test-subj': string; onAddFilter?: AddFieldFilterHandler; } @@ -29,7 +29,7 @@ export const FieldTopValues: React.FC = ({ field, sampledValuesCount, color = getDefaultColor(), - testSubject, + 'data-test-subj': dataTestSubject, onAddFilter, }) => { if (!buckets?.length) { @@ -43,7 +43,7 @@ export const FieldTopValues: React.FC = ({ ); return ( -
+
{buckets.map((bucket, index) => { const fieldValue = bucket.key; const formatted = formatter.convert(fieldValue); @@ -62,7 +62,7 @@ export const FieldTopValues: React.FC = ({ )} progressValue={getProgressValue(bucket.count, sampledValuesCount)} color={color} - testSubject={testSubject} + data-test-subj={dataTestSubject} onAddFilter={onAddFilter} /> @@ -82,7 +82,7 @@ export const FieldTopValues: React.FC = ({ )} progressValue={getProgressValue(otherCount, sampledValuesCount)} color={color} - testSubject={testSubject} + data-test-subj={dataTestSubject} onAddFilter={onAddFilter} /> diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx index 74a890b7e3265..8923d4a02fb0d 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -29,7 +29,7 @@ export interface FieldTopValuesBucketProps { formattedPercentage: string; progressValue: number; color: string; - testSubject: string; + 'data-test-subj': string; onAddFilter?: AddFieldFilterHandler; } @@ -41,11 +41,11 @@ export const FieldTopValuesBucket: React.FC = ({ formattedPercentage, progressValue, color, - testSubject, + 'data-test-subj': dataTestSubject, onAddFilter, }) => { const isFilterButtonHidden = type === 'other'; - const fieldName = (field?.subType as IFieldSubTypeMulti)?.multi?.parent ?? field.name; // TODO: check if this is a right approach for handling multi fields + const testSubjFieldName = (field?.subType as IFieldSubTypeMulti)?.multi?.parent ?? field.name; return ( @@ -59,7 +59,7 @@ export const FieldTopValuesBucket: React.FC = ({ {(formattedFieldValue?.length ?? 0) > 0 ? ( @@ -81,7 +81,10 @@ export const FieldTopValuesBucket: React.FC = ({ )} - + {formattedPercentage} @@ -113,9 +116,9 @@ export const FieldTopValuesBucket: React.FC = ({ onClick={() => onAddFilter(field, fieldValue, '+')} aria-label={i18n.translate('unifiedFieldList.fieldStats.filterValueButtonAriaLabel', { defaultMessage: 'Filter for {field}: "{value}"', - values: { value: formattedFieldValue, field: fieldName }, + values: { value: formattedFieldValue, field: testSubjFieldName }, })} - data-test-subj={`plus-${fieldName}-${fieldValue}`} + data-test-subj={`plus-${testSubjFieldName}-${fieldValue}`} style={{ minHeight: 'auto', minWidth: 'auto', @@ -134,10 +137,10 @@ export const FieldTopValuesBucket: React.FC = ({ 'unifiedFieldList.fieldStats.filterOutValueButtonAriaLabel', { defaultMessage: 'Filter out {field}: "{value}"', - values: { value: formattedFieldValue, field: fieldName }, + values: { value: formattedFieldValue, field: testSubjFieldName }, } )} - data-test-subj={`minus-${fieldName}-${fieldValue}`} + data-test-subj={`minus-${testSubjFieldName}-${fieldValue}`} style={{ minHeight: 'auto', minWidth: 'auto', diff --git a/test/functional/apps/discover/group2/_data_grid_context.ts b/test/functional/apps/discover/group2/_data_grid_context.ts index a85c97a27ffe9..407ec8dd542f9 100644 --- a/test/functional/apps/discover/group2/_data_grid_context.ts +++ b/test/functional/apps/discover/group2/_data_grid_context.ts @@ -11,8 +11,8 @@ import { FtrProviderContext } from '../ftr_provider_context'; const TEST_COLUMN_NAMES = ['@message']; const TEST_FILTER_COLUMN_NAMES = [ - ['extension.raw', 'jpg'], - ['geo.src', 'IN'], + ['extension', 'jpg', 'extension.raw'], + ['geo.src', 'IN', 'geo.src'], ]; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -84,8 +84,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should open the context view with the filters disabled', async () => { let disabledFilterCounter = 0; - for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { - if (await filterBar.hasFilter(columnName, value, false)) { + for (const [_, value, columnId] of TEST_FILTER_COLUMN_NAMES) { + if (await filterBar.hasFilter(columnId, value, false)) { disabledFilterCounter++; } } From dde211e326b21de9c79c2ce442b07db91b6acf48 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 30 Aug 2022 11:17:08 +0200 Subject: [PATCH 73/92] [Discover] Add tooltips. Update examples sample values. Update tests. --- .../utils/field_examples_calculator.test.ts | 63 ++++++++++++------- .../common/utils/field_examples_calculator.ts | 13 +++- .../common/utils/field_stats_utils.ts | 4 +- .../field_stats/field_stats.test.tsx | 10 +-- .../components/field_stats/field_stats.tsx | 46 +++++++++++--- .../field_stats/field_top_values.tsx | 2 + .../field_stats/field_top_values_bucket.tsx | 20 +++++- .../context/classic/_discover_navigation.ts | 8 +-- 8 files changed, 116 insertions(+), 50 deletions(-) diff --git a/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts b/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts index 1f7b6b6bafa50..24de6dc04ae54 100644 --- a/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts +++ b/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts @@ -16,14 +16,16 @@ import { getFieldValues, } from './field_examples_calculator'; -const hitsAsValues: Array> = [ +const hitsAsValues: Array> = [ { extension: 'html', bytes: 360.20000000000005, + '@tags': ['success', 'info'], }, { extension: 'gif', bytes: 5848.700000000001, + '@tags': ['error'], }, { extension: 'png', @@ -110,7 +112,8 @@ const hits = hitsAsValues.map((value) => ({ _score: 1, fields: Object.keys(value).reduce( (result: Record>, fieldName: string) => { - result[fieldName] = [value[fieldName]]; + const fieldValue = value[fieldName]; + result[fieldName] = Array.isArray(fieldValue) ? fieldValue : [fieldValue]; return result; }, {} @@ -137,7 +140,7 @@ describe('fieldExamplesCalculator', function () { }); describe('groupValues', function () { - let groups: Record; + let grouped: { groups: Record; valuesCount: number }; let params: any; let values: any; beforeEach(function () { @@ -156,11 +159,12 @@ describe('fieldExamplesCalculator', function () { undefined, ]; params = {}; - groups = groupValues(values, params); + grouped = groupValues(values, params); }); it('should have a groupValues that counts values', function () { - expect(groups).toBeInstanceOf(Object); + expect(grouped.groups).toBeInstanceOf(Object); + expect(grouped.valuesCount).toBe(9); }); it('should throw an error if any value is a plain object', function () { @@ -172,45 +176,47 @@ describe('fieldExamplesCalculator', function () { it('should handle values with dots in them', function () { values = ['0', '0.........', '0.......,.....']; params = {}; - groups = groupValues(values, params); - expect(groups[values[0]].count).toBe(1); - expect(groups[values[1]].count).toBe(1); - expect(groups[values[2]].count).toBe(1); + grouped = groupValues(values, params); + expect(grouped.groups[values[0]].count).toBe(1); + expect(grouped.groups[values[1]].count).toBe(1); + expect(grouped.groups[values[2]].count).toBe(1); + expect(grouped.valuesCount).toBe(3); }); it('should have a a key for value in the array when not grouping array terms', function () { - expect(keys(groups).length).toBe(3); - expect(groups.foo).toBeInstanceOf(Object); - expect(groups.bar).toBeInstanceOf(Object); - expect(groups.baz).toBeInstanceOf(Object); + expect(keys(grouped.groups).length).toBe(3); + expect(grouped.groups.foo).toBeInstanceOf(Object); + expect(grouped.groups.bar).toBeInstanceOf(Object); + expect(grouped.groups.baz).toBeInstanceOf(Object); }); it('should count array terms independently', function () { - expect(groups['foo,bar']).toBe(undefined); - expect(groups.foo.count).toBe(5); - expect(groups.bar.count).toBe(3); - expect(groups.baz.count).toBe(1); + expect(grouped.groups['foo,bar']).toBe(undefined); + expect(grouped.groups.foo.count).toBe(5); + expect(grouped.groups.bar.count).toBe(3); + expect(grouped.groups.baz.count).toBe(1); + expect(grouped.valuesCount).toBe(9); }); describe('grouped array terms', function () { beforeEach(function () { params.grouped = true; - groups = groupValues(values, params); + grouped = groupValues(values, params); }); it('should group array terms when passed params.grouped', function () { - expect(keys(groups).length).toBe(4); - expect(groups['foo,bar']).toBeInstanceOf(Object); + expect(keys(grouped.groups).length).toBe(4); + expect(grouped.groups['foo,bar']).toBeInstanceOf(Object); }); it('should contain the original array as the value', function () { - expect(groups['foo,bar'].value).toEqual(['foo', 'bar']); + expect(grouped.groups['foo,bar'].value).toEqual(['foo', 'bar']); }); it('should count the pairs separately from the values they contain', function () { - expect(groups['foo,bar'].count).toBe(2); - expect(groups.foo.count).toBe(3); - expect(groups.bar.count).toBe(1); + expect(grouped.groups['foo,bar'].count).toBe(2); + expect(grouped.groups.foo.count).toBe(3); + expect(grouped.groups.bar.count).toBe(1); }); }); }); @@ -285,5 +291,14 @@ describe('fieldExamplesCalculator', function () { params.field = dataView.fields.getByName('phpmemory'); expect(getFieldExampleBuckets(params).exists).toBe(5); }); + + it('counts total number of values', function () { + params.field = dataView.fields.getByName('@tags'); + expect(getFieldExampleBuckets(params).valuesCount).toBe(3); + params.field = dataView.fields.getByName('extension'); + expect(getFieldExampleBuckets(params).valuesCount).toBe(params.hits.length); + params.field = dataView.fields.getByName('phpmemory'); + expect(getFieldExampleBuckets(params).valuesCount).toBe(5); + }); }); }); diff --git a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts index 7ff39b077dd40..07f9e4efb0c2e 100644 --- a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts +++ b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts @@ -41,7 +41,7 @@ export function getFieldExampleBuckets(params: FieldValueCountsParams) { const allValues = getFieldValues(params.hits, params.field, params.dataView); const missing = countMissing(allValues); - const groups = groupValues(allValues, params); + const { groups, valuesCount } = groupValues(allValues, params); const exampleBuckets = map( sortBy(groups, 'count').reverse().slice(0, params.count), function (bucket) { @@ -60,6 +60,7 @@ export function getFieldExampleBuckets(params: FieldValueCountsParams) { total: params.hits.length, exists: params.hits.length - missing, missing, + valuesCount, buckets: exampleBuckets, }; } @@ -79,7 +80,10 @@ export function countMissing(array: FieldHitValue[]): number { return array.length - without(array, undefined, null).length; } -export function groupValues(allValues: FieldHitValue[], params: FieldValueCountsParams) { +export function groupValues( + allValues: FieldHitValue[], + params: FieldValueCountsParams +): { groups: Record; valuesCount: number } { const groups: Record = {}; let k; @@ -106,5 +110,8 @@ export function groupValues(allValues: FieldHitValue[], params: FieldValueCounts }); }); - return groups; + return { + groups, + valuesCount: Object.values(groups).reduce((sum, group) => sum + group.count, 0), + }; } diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index a31e1db89868c..2fa2d464c3d84 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -353,8 +353,8 @@ export async function getSimpleExamples( return { totalDocuments: getHitsTotal(simpleExamplesResult), - sampledDocuments: fieldExampleBuckets.total, // TODO: check if that's correct mapping - sampledValues: fieldExampleBuckets.exists, // TODO: check if that's correct mapping + sampledDocuments: fieldExampleBuckets.total, + sampledValues: fieldExampleBuckets.valuesCount, topValues: { buckets: fieldExampleBuckets.buckets, }, diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx index 22c9a1b9a9d3c..5dcb1ce6fa448 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -324,11 +324,11 @@ describe('UnifiedFieldList ', () => { ).toBe('41.5%'); expect(wrapper.find('[data-test-subj="testing-statsFooter"]').first().text()).toBe( - '100% of 1624 records' + '1624 records' ); expect(wrapper.text()).toBe( - 'Top values"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%100% of 1624 records' + 'Top values"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%1624 records' ); }); @@ -402,7 +402,7 @@ describe('UnifiedFieldList ', () => { expect(loadFieldStats).toHaveBeenCalledTimes(1); expect(wrapper.text()).toBe( - 'Examples"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%100% of 1624 records' + 'Examples"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%1624 records' ); }); @@ -557,7 +557,7 @@ describe('UnifiedFieldList ', () => { await act(async () => { resolveFunction!({ - totalDocuments: 23, + totalDocuments: 100, sampledDocuments: 23, sampledValues: 23, histogram: { @@ -594,7 +594,7 @@ describe('UnifiedFieldList ', () => { expect(loadFieldStats).toHaveBeenCalledTimes(1); expect(wrapper.text()).toBe( - 'Toggle either theTop valuesDistribution1273.9%1326.1%100% of 23 records' + 'Toggle either theTop valuesDistribution1273.9%1326.1%Based on 23% of 100 records' ); }); }); diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 65ff4a1224b5a..86e8b2985c461 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -20,7 +20,14 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import DateMath from '@kbn/datemath'; -import { EuiButtonGroup, EuiLoadingSpinner, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { + EuiButtonGroup, + EuiLoadingSpinner, + EuiSpacer, + EuiText, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; import { Axis, Chart, @@ -234,16 +241,34 @@ const FieldStatsComponent: React.FC = ({ let title = <>; function combineWithTitleAndFooter(el: React.ReactElement) { + const percentage = + sampledDocuments && totalDocuments + ? Math.round((sampledDocuments / totalDocuments) * 100) + : 0; const countsElement = totalDocuments ? ( - {sampledDocuments && ( + {sampledDocuments && percentage < 100 && ( <> - {i18n.translate('unifiedFieldList.fieldStats.percentageOfLabel', { - defaultMessage: '{percentage}% of', - values: { - percentage: Math.round((sampledDocuments / totalDocuments) * 100), - }, - })}{' '} + + + {i18n.translate('unifiedFieldList.fieldStats.basedOnPercentageOfLabel', { + defaultMessage: 'Based on {percentage}% of', + values: { + percentage, + }, + })} + + {' '} )} @@ -252,7 +277,10 @@ const FieldStatsComponent: React.FC = ({ .convert(totalDocuments)} {' '} {i18n.translate('unifiedFieldList.fieldStats.ofRecordsLabel', { - defaultMessage: 'records', + defaultMessage: '{totalDocuments, plural, one {record} other {records}}', + values: { + totalDocuments, + }, })} ) : ( diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx index 0bee95061ae3d..7d9d54609c5e7 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx @@ -61,6 +61,7 @@ export const FieldTopValues: React.FC = ({ digitsRequired )} progressValue={getProgressValue(bucket.count, sampledValuesCount)} + valuesCount={bucket.count} color={color} data-test-subj={dataTestSubject} onAddFilter={onAddFilter} @@ -81,6 +82,7 @@ export const FieldTopValues: React.FC = ({ digitsRequired )} progressValue={getProgressValue(otherCount, sampledValuesCount)} + valuesCount={otherCount} color={color} data-test-subj={dataTestSubject} onAddFilter={onAddFilter} diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx index 8923d4a02fb0d..f23551d6bf21a 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -28,6 +28,7 @@ export interface FieldTopValuesBucketProps { formattedFieldValue?: string; formattedPercentage: string; progressValue: number; + valuesCount: number; color: string; 'data-test-subj': string; onAddFilter?: AddFieldFilterHandler; @@ -40,6 +41,7 @@ export const FieldTopValuesBucket: React.FC = ({ formattedFieldValue, formattedPercentage, progressValue, + valuesCount, color, 'data-test-subj': dataTestSubject, onAddFilter, @@ -85,9 +87,21 @@ export const FieldTopValuesBucket: React.FC = ({ grow={false} data-test-subj={`${dataTestSubject}-topValues-formattedPercentage`} > - - {formattedPercentage} - + + + {formattedPercentage} + + { let disabledFilterCounter = 0; - for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { - if (await filterBar.hasFilter(columnName, value, false)) { + for (const [_, value, columnId] of TEST_FILTER_COLUMN_NAMES) { + if (await filterBar.hasFilter(columnId, value, false)) { disabledFilterCounter++; } } From 0cb63f71b16badb5ae895099a942ee77b9336e3e Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 30 Aug 2022 14:37:16 +0200 Subject: [PATCH 74/92] [Discover] Close the popover when filter is pressed --- .../main/components/sidebar/discover_field.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 775e78e2a4bbb..976d416aaf138 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -300,6 +300,17 @@ function DiscoverFieldComponent({ const [infoIsOpen, setOpen] = useState(false); const isDocumentRecord = !!onAddFilter; + const addFilterAndClosePopover: typeof onAddFilter | undefined = useMemo( + () => + onAddFilter + ? (...params) => { + setOpen(false); + onAddFilter?.(...params); + } + : undefined, + [setOpen, onAddFilter] + ); + const toggleDisplay = useCallback( (f: DataViewField) => { if (selected) { @@ -451,7 +462,7 @@ function DiscoverFieldComponent({ dataViewOrDataViewId={dataView} field={fieldForStats} data-test-subj="dscFieldListPanel" - onAddFilter={onAddFilter} + onAddFilter={addFilterAndClosePopover} /> )} From e8d0650c1c1265d20c34ea7dedbbfe9585e53931 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 31 Aug 2022 10:24:07 +0200 Subject: [PATCH 75/92] [Discover] Add functional tests for non-aggregatable fields --- .../common/utils/field_stats_utils.ts | 9 ++-- .../apis/unified_field_list/field_stats.ts | 44 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index 2fa2d464c3d84..a2a1c405518c9 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -44,9 +44,9 @@ export function buildSearchParams({ toDate: string; dslQuery: object; runtimeMappings: estypes.MappingRuntimeFields; - aggs?: Record; - fields?: object[]; - size?: number; + aggs?: Record; // is used for aggregatable fields + fields?: object[]; // is used for non-aggregatable fields + size?: number; // is used for non-aggregatable fields }) { const filter = timeFieldName ? [ @@ -85,6 +85,9 @@ export function buildSearchParams({ }, track_total_hits: true, size: size ?? 0, + ...(fields?.length && timeFieldName + ? { sort: [{ [timeFieldName]: { order: 'desc', unmapped_type: 'boolean' } }] } + : {}), }; } diff --git a/test/api_integration/apis/unified_field_list/field_stats.ts b/test/api_integration/apis/unified_field_list/field_stats.ts index b489dea50d740..39cc3709ab8aa 100644 --- a/test/api_integration/apis/unified_field_list/field_stats.ts +++ b/test/api_integration/apis/unified_field_list/field_stats.ts @@ -395,6 +395,50 @@ export default ({ getService }: FtrProviderContext) => { }); }); + it('should return examples for non-aggregatable fields', async () => { + const { body } = await supertest + .post(API_PATH) + .set(COMMON_HEADERS) + .send({ + dataViewId: 'logstash-2015.09.22', + dslQuery: { match_all: {} }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + fieldName: 'extension', // `extension.keyword` is an aggregatable field but `extension` is not + }) + .expect(200); + + expect(body).to.eql({ + totalDocuments: 4634, + sampledDocuments: 100, + sampledValues: 100, + topValues: { + buckets: [ + { + count: 64, + key: 'jpg', + }, + { + count: 17, + key: 'png', + }, + { + count: 13, + key: 'css', + }, + { + count: 4, + key: 'gif', + }, + { + count: 2, + key: 'php', + }, + ], + }, + }); + }); + it('should return top values for index pattern runtime string fields', async () => { const { body } = await supertest .post(API_PATH) From 4a43224de76a61d6b09f2f106122e5dde670ebf1 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 31 Aug 2022 10:56:58 +0200 Subject: [PATCH 76/92] [Discover] Fix query --- .../unified_field_list/common/utils/field_stats_utils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index a2a1c405518c9..dd4ba681b4596 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -82,12 +82,13 @@ export function buildSearchParams({ fields, runtime_mappings: runtimeMappings, _source: fields?.length ? false : undefined, + sort: + fields?.length && timeFieldName + ? [{ [timeFieldName]: { order: 'desc', unmapped_type: 'boolean' } }] + : undefined, }, track_total_hits: true, size: size ?? 0, - ...(fields?.length && timeFieldName - ? { sort: [{ [timeFieldName]: { order: 'desc', unmapped_type: 'boolean' } }] } - : {}), }; } From 42324d86ebbcd0b06c894dec18cb343066ea7b4c Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 31 Aug 2022 11:43:36 +0200 Subject: [PATCH 77/92] [Discover] Add more tests --- .../field_stats/field_top_values.test.tsx | 132 ++++++++++++++++++ .../field_stats/field_top_values_bucket.tsx | 104 +++++++------- 2 files changed, 184 insertions(+), 52 deletions(-) create mode 100644 src/plugins/unified_field_list/public/components/field_stats/field_top_values.test.tsx diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.test.tsx new file mode 100644 index 0000000000000..00225bb6d8708 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.test.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiProgress, EuiButtonIcon } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { FieldTopValues, FieldTopValuesProps } from './field_top_values'; +import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; +import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; + +describe('UnifiedFieldList ', () => { + let defaultProps: FieldTopValuesProps; + let dataView: DataView; + + beforeEach(() => { + dataView = { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + fields: [ + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + filterable: true, + }, + ], + getFormatterForField: jest.fn(() => ({ + convert: jest.fn((s: unknown) => + fieldFormatsServiceMock + .createStartContract() + .getDefaultInstance(KBN_FIELD_TYPES.STRING, [ES_FIELD_TYPES.STRING]) + .convert(s) + ), + })), + } as unknown as DataView; + + defaultProps = { + dataView, + field: dataView.fields.find((f) => f.name === 'source')!, + sampledValuesCount: 5000, + buckets: [ + { + count: 500, + key: 'sourceA', + }, + { + count: 1, + key: 'sourceB', + }, + ], + 'data-test-subj': 'testing', + }; + }); + + it('should render correctly without filter actions', async () => { + const wrapper = mountWithIntl(); + + expect(wrapper.text()).toBe('sourceA10.0%sourceB0.0%Other90.0%'); + expect(wrapper.find(EuiProgress)).toHaveLength(3); + expect(wrapper.find(EuiButtonIcon)).toHaveLength(0); + }); + + it('should render correctly with filter actions', async () => { + const mockAddFilter = jest.fn(); + const wrapper = mountWithIntl(); + + expect(wrapper.text()).toBe('sourceA10.0%sourceB0.0%Other90.0%'); + expect(wrapper.find(EuiProgress)).toHaveLength(3); + expect(wrapper.find(EuiButtonIcon)).toHaveLength(4); + + wrapper.find(EuiButtonIcon).first().simulate('click'); + + expect(mockAddFilter).toHaveBeenCalledWith(defaultProps.field, 'sourceA', '+'); + }); + + it('should render correctly without Other section', async () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.text()).toBe('sourceA60.0%sourceB30.0%sourceC10.0%'); + }); + + it('should render correctly with empty strings', async () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.text()).toBe('(empty)60.0%sourceA30.0%sourceB0.4%Other9.6%'); + }); +}); diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx index f23551d6bf21a..c93153cc0a4b6 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -46,8 +46,7 @@ export const FieldTopValuesBucket: React.FC = ({ 'data-test-subj': dataTestSubject, onAddFilter, }) => { - const isFilterButtonHidden = type === 'other'; - const testSubjFieldName = (field?.subType as IFieldSubTypeMulti)?.multi?.parent ?? field.name; + const fieldLabel = (field?.subType as IFieldSubTypeMulti)?.multi?.parent ?? field.name; return ( @@ -114,57 +113,58 @@ export const FieldTopValuesBucket: React.FC = ({ {onAddFilter && field.filterable && ( -
- onAddFilter(field, fieldValue, '+')} - aria-label={i18n.translate('unifiedFieldList.fieldStats.filterValueButtonAriaLabel', { - defaultMessage: 'Filter for {field}: "{value}"', - values: { value: formattedFieldValue, field: testSubjFieldName }, - })} - data-test-subj={`plus-${testSubjFieldName}-${fieldValue}`} - style={{ - minHeight: 'auto', - minWidth: 'auto', - paddingRight: 2, - paddingLeft: 2, - paddingTop: 0, - paddingBottom: 0, - }} - /> - onAddFilter(field, fieldValue, '-')} - aria-label={i18n.translate( - 'unifiedFieldList.fieldStats.filterOutValueButtonAriaLabel', - { - defaultMessage: 'Filter out {field}: "{value}"', - values: { value: formattedFieldValue, field: testSubjFieldName }, - } - )} - data-test-subj={`minus-${testSubjFieldName}-${fieldValue}`} - style={{ - minHeight: 'auto', - minWidth: 'auto', - paddingTop: 0, - paddingBottom: 0, - paddingRight: 2, - paddingLeft: 2, - }} + {type === 'other' ? ( +
-
+ ) : ( +
+ onAddFilter(field, fieldValue, '+')} + aria-label={i18n.translate( + 'unifiedFieldList.fieldStats.filterValueButtonAriaLabel', + { + defaultMessage: 'Filter for {field}: "{value}"', + values: { value: formattedFieldValue, field: fieldLabel }, + } + )} + data-test-subj={`plus-${fieldLabel}-${fieldValue}`} + style={{ + minHeight: 'auto', + minWidth: 'auto', + paddingRight: 2, + paddingLeft: 2, + paddingTop: 0, + paddingBottom: 0, + }} + /> + onAddFilter(field, fieldValue, '-')} + aria-label={i18n.translate( + 'unifiedFieldList.fieldStats.filterOutValueButtonAriaLabel', + { + defaultMessage: 'Filter out {field}: "{value}"', + values: { value: formattedFieldValue, field: fieldLabel }, + } + )} + data-test-subj={`minus-${fieldLabel}-${fieldValue}`} + style={{ + minHeight: 'auto', + minWidth: 'auto', + paddingTop: 0, + paddingBottom: 0, + paddingRight: 2, + paddingLeft: 2, + }} + /> +
+ )} )} From 70088b2d123ab3d8ef72a9448960653e200d871a Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 31 Aug 2022 11:56:13 +0200 Subject: [PATCH 78/92] [Discover] Add more tests --- .../field_stats/field_top_values.test.tsx | 16 ++++++++++++++++ .../components/field_stats/field_top_values.tsx | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.test.tsx index 00225bb6d8708..a481633846b2b 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.test.tsx @@ -129,4 +129,20 @@ describe('UnifiedFieldList ', () => { expect(wrapper.text()).toBe('(empty)60.0%sourceA30.0%sourceB0.4%Other9.6%'); }); + + it('should render correctly without floating point', async () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.text()).toBe('sourceA100%'); + }); }); diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx index 7d9d54609c5e7..9c50efef1b28a 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx @@ -39,7 +39,7 @@ export const FieldTopValues: React.FC = ({ const formatter = dataView.getFormatterForField(field); const otherCount = getOtherCount(getBucketsValuesCount(buckets), sampledValuesCount); const digitsRequired = buckets.some( - (topValue) => !Number.isInteger(topValue.count / sampledValuesCount) + (bucket) => !Number.isInteger(bucket.count / sampledValuesCount) ); return ( From 66a985da2e00340433901d87e7922d5220c3c870 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 31 Aug 2022 15:23:03 +0200 Subject: [PATCH 79/92] [Discover] Add more tests --- .../sidebar/discover_field.test.tsx | 104 ++++++++++++++---- .../components/sidebar/discover_field.tsx | 2 +- .../components/field_stats/field_stats.tsx | 2 +- .../field_stats/field_top_values.tsx | 4 +- .../field_stats/field_top_values_bucket.tsx | 15 ++- 5 files changed, 99 insertions(+), 28 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx index dde38091c82ff..a9a7ba4b45b13 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx @@ -6,6 +6,9 @@ * Side Public License, v 1. */ +import { act } from 'react-dom/test-utils'; +import { EuiPopover, EuiProgress, EuiButtonIcon } from '@elastic/eui'; +import { ReactWrapper } from 'enzyme'; import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { mountWithIntl } from '@kbn/test-jest-helpers'; @@ -18,6 +21,26 @@ import { DataViewField } from '@kbn/data-views-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { stubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +jest.mock('@kbn/unified-field-list-plugin/public/services/field_stats', () => ({ + loadFieldStats: jest.fn().mockResolvedValue({ + totalDocuments: 1624, + sampledDocuments: 1624, + sampledValues: 3248, + topValues: { + buckets: [ + { + count: 2042, + key: 'osx', + }, + { + count: 1206, + key: 'winx', + }, + ], + }, + }), +})); + const dataServiceMock = dataPluginMock.createStartContract(); jest.mock('../../../../kibana_services', () => ({ @@ -28,16 +51,18 @@ jest.mock('../../../../kibana_services', () => ({ }), })); -function getComponent({ +async function getComponent({ selected = false, showFieldStats = false, field, onAddFilterExists = true, + showLegacyFieldTopValues = false, }: { selected?: boolean; showFieldStats?: boolean; field?: DataViewField; onAddFilterExists?: boolean; + showLegacyFieldTopValues?: boolean; }) { const finalField = field ?? @@ -84,7 +109,7 @@ function getComponent({ return 5; } if (key === 'discover:showLegacyFieldTopValues') { - return true; + return showLegacyFieldTopValues; } }, }, @@ -108,7 +133,7 @@ function getComponent({ fieldFormats: fieldFormatsServiceMock.createStartContract(), charts: chartPluginMock.createSetupContract(), }; - const comp = mountWithIntl( + const comp = await mountWithIntl( @@ -117,22 +142,26 @@ function getComponent({ } describe('discover sidebar field', function () { - it('should allow selecting fields', function () { - const { comp, props } = getComponent({}); + it('should allow selecting fields', async function () { + const { comp, props } = await getComponent({}); findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); expect(props.onAddField).toHaveBeenCalledWith('bytes'); }); - it('should allow deselecting fields', function () { - const { comp, props } = getComponent({ selected: true }); + it('should allow deselecting fields', async function () { + const { comp, props } = await getComponent({ selected: true }); findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); expect(props.onRemoveField).toHaveBeenCalledWith('bytes'); }); - it('should trigger getDetails', function () { - const { comp, props } = getComponent({ selected: true, showFieldStats: true }); + it('should trigger getDetails', async function () { + const { comp, props } = await getComponent({ + selected: true, + showFieldStats: true, + showLegacyFieldTopValues: true, + }); findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); expect(props.getDetails).toHaveBeenCalledWith(props.field); }); - it('should not allow clicking on _source', function () { + it('should not allow clicking on _source', async function () { const field = new DataViewField({ name: '_source', type: '_source', @@ -141,14 +170,15 @@ describe('discover sidebar field', function () { aggregatable: true, readFromDocValues: true, }); - const { comp, props } = getComponent({ + const { comp, props } = await getComponent({ selected: true, field, + showLegacyFieldTopValues: true, }); findTestSubject(comp, 'field-_source-showDetails').simulate('click'); expect(props.getDetails).not.toHaveBeenCalled(); }); - it('displays warning for conflicting fields', function () { + it('displays warning for conflicting fields', async function () { const field = new DataViewField({ name: 'troubled_field', type: 'conflict', @@ -157,23 +187,26 @@ describe('discover sidebar field', function () { aggregatable: true, readFromDocValues: false, }); - const { comp } = getComponent({ + const { comp } = await getComponent({ selected: true, field, }); const dscField = findTestSubject(comp, 'field-troubled_field-showDetails'); expect(dscField.find('.kbnFieldButton__infoIcon').length).toEqual(1); }); - it('should not execute getDetails when rendered, since it can be expensive', function () { - const { props } = getComponent({}); + it('should not execute getDetails when rendered, since it can be expensive', async function () { + const { props } = await getComponent({}); expect(props.getDetails).toHaveBeenCalledTimes(0); }); - it('should execute getDetails when show details is requested', function () { - const { props, comp } = getComponent({ showFieldStats: true }); + it('should execute getDetails when show details is requested', async function () { + const { props, comp } = await getComponent({ + showFieldStats: true, + showLegacyFieldTopValues: true, + }); findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); expect(props.getDetails).toHaveBeenCalledTimes(1); }); - it('should not return the popover if onAddFilter is not provided', function () { + it('should not return the popover if onAddFilter is not provided', async function () { const field = new DataViewField({ name: '_source', type: '_source', @@ -182,7 +215,7 @@ describe('discover sidebar field', function () { aggregatable: true, readFromDocValues: true, }); - const { comp } = getComponent({ + const { comp } = await getComponent({ selected: true, field, onAddFilterExists: false, @@ -190,4 +223,37 @@ describe('discover sidebar field', function () { const popover = findTestSubject(comp, 'discoverFieldListPanelPopover'); expect(popover.length).toBe(0); }); + it('should request field stats', async function () { + const field = new DataViewField({ + name: 'machine.os.raw', + type: 'string', + esTypes: ['keyword'], + aggregatable: true, + searchable: true, + }); + let comp: ReactWrapper; + + await act(async () => { + const result = await getComponent({ showFieldStats: true, field, onAddFilterExists: true }); + comp = result.comp; + await comp.update(); + }); + + await act(async () => { + const fieldItem = findTestSubject(comp, 'field-machine.os.raw-showDetails'); + await fieldItem.simulate('click'); + await comp.update(); + }); + + await comp!.update(); + + expect(comp!.find(EuiPopover).prop('isOpen')).toBe(true); + expect(findTestSubject(comp!, 'dscFieldStats-title').text()).toBe('Top values'); + expect(findTestSubject(comp!, 'dscFieldStats-topValues-bucket')).toHaveLength(2); + expect( + findTestSubject(comp!, 'dscFieldStats-topValues-formattedFieldValue').first().text() + ).toBe('osx'); + expect(comp!.find(EuiProgress)).toHaveLength(2); + expect(findTestSubject(comp!, 'dscFieldStats-topValues').find(EuiButtonIcon)).toHaveLength(4); + }); }); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 976d416aaf138..32748917db06a 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -461,7 +461,7 @@ function DiscoverFieldComponent({ toDate={dateRange.to} dataViewOrDataViewId={dataView} field={fieldForStats} - data-test-subj="dscFieldListPanel" + data-test-subj="dscFieldStats" onAddFilter={addFilterAndClosePopover} /> )} diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 86e8b2985c461..3781bfa31aa8b 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -289,7 +289,7 @@ const FieldStatsComponent: React.FC = ({ return ( <> - {title ? title : <>} + {title ?
{title}
: <>} diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx index 9c50efef1b28a..adec20e38b727 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values.tsx @@ -61,7 +61,7 @@ export const FieldTopValues: React.FC = ({ digitsRequired )} progressValue={getProgressValue(bucket.count, sampledValuesCount)} - valuesCount={bucket.count} + count={bucket.count} color={color} data-test-subj={dataTestSubject} onAddFilter={onAddFilter} @@ -82,7 +82,7 @@ export const FieldTopValues: React.FC = ({ digitsRequired )} progressValue={getProgressValue(otherCount, sampledValuesCount)} - valuesCount={otherCount} + count={otherCount} color={color} data-test-subj={dataTestSubject} onAddFilter={onAddFilter} diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx index c93153cc0a4b6..e45a55d7b350e 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_top_values_bucket.tsx @@ -28,7 +28,7 @@ export interface FieldTopValuesBucketProps { formattedFieldValue?: string; formattedPercentage: string; progressValue: number; - valuesCount: number; + count: number; color: string; 'data-test-subj': string; onAddFilter?: AddFieldFilterHandler; @@ -41,7 +41,7 @@ export const FieldTopValuesBucket: React.FC = ({ formattedFieldValue, formattedPercentage, progressValue, - valuesCount, + count, color, 'data-test-subj': dataTestSubject, onAddFilter, @@ -49,7 +49,12 @@ export const FieldTopValuesBucket: React.FC = ({ const fieldLabel = (field?.subType as IFieldSubTypeMulti)?.multi?.parent ?? field.name; return ( - + = ({ Date: Wed, 31 Aug 2022 15:50:52 +0200 Subject: [PATCH 80/92] [Discover] Add more tests --- .../discover_sidebar_responsive.test.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index 03a60d2c41cf2..7137f4a1af2b3 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -87,9 +87,6 @@ const mockServices = { if (key === 'fields:popularLimit') { return 5; } - if (key === 'discover:showLegacyFieldTopValues') { - return true; - } }, }, docLinks: { links: { discover: { fieldTypeHelp: '' } } }, @@ -187,11 +184,14 @@ describe('discover responsive sidebar', function () { beforeAll(async () => { props = getCompProps(); - comp = await mountWithIntl( - - - - ); + await act(async () => { + comp = await mountWithIntl( + + + + ); + comp.update(); + }); }); it('should have Selected Fields and Available Fields with Popular Fields sections', function () { @@ -211,8 +211,14 @@ describe('discover responsive sidebar', function () { findTestSubject(comp, 'fieldToggle-extension').simulate('click'); expect(props.onRemoveField).toHaveBeenCalledWith('extension'); }); - it('should allow adding filters', function () { - findTestSubject(comp, 'field-extension-showDetails').simulate('click'); + it('should allow adding filters', async function () { + await act(async () => { + const button = findTestSubject(comp, 'field-extension-showDetails'); + await button.simulate('click'); + await comp.update(); + }); + + await comp.update(); findTestSubject(comp, 'plus-extension-gif').simulate('click'); expect(props.onAddFilter).toHaveBeenCalled(); }); From 65c393b29c37efa4e328f6b7caa6e71a97fd1665 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 1 Sep 2022 11:22:32 +0200 Subject: [PATCH 81/92] [Discover] Fix time range for field stats --- .../main/components/sidebar/discover_field.test.tsx | 6 +++--- .../application/main/components/sidebar/discover_field.tsx | 2 +- .../components/sidebar/discover_sidebar_responsive.test.tsx | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx index a9a7ba4b45b13..0507fb0475b52 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx @@ -121,9 +121,9 @@ async function getComponent({ ...dataServiceMock.query.timefilter, timefilter: { ...dataServiceMock.query.timefilter.timefilter, - getTime: () => ({ - from: 'now-7d', - to: 'now', + getAbsoluteTime: () => ({ + from: '2021-08-31T22:00:00.000Z', + to: '2022-09-01T09:16:29.553Z', }), }, }, diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 32748917db06a..c475fde62b29c 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -424,7 +424,7 @@ function DiscoverFieldComponent({ } const renderPopover = () => { - const dateRange = data?.query?.timefilter.timefilter.getTime(); + const dateRange = data?.query?.timefilter.timefilter.getAbsoluteTime(); const fieldForStats = multiFields ? multiFields[0].field : field; // TODO: how to handle multifields? const showLegacyFieldStats = services.uiSettings.get(SHOW_LEGACY_FIELD_TOP_VALUES); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index 7137f4a1af2b3..c8f8495dcc887 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -103,9 +103,9 @@ const mockServices = { ...dataServiceMock.query.timefilter, timefilter: { ...dataServiceMock.query.timefilter.timefilter, - getTime: () => ({ - from: 'now-7d', - to: 'now', + getAbsoluteTime: () => ({ + from: '2021-08-31T22:00:00.000Z', + to: '2022-09-01T09:16:29.553Z', }), }, }, From 60c9da0daf6968f71d6a51b746690320db6646d0 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 1 Sep 2022 11:41:51 +0200 Subject: [PATCH 82/92] [Discover] Remove sort param from examples query --- .../common/utils/field_stats_utils.ts | 4 --- .../apis/unified_field_list/field_stats.ts | 33 +++---------------- 2 files changed, 4 insertions(+), 33 deletions(-) diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index dd4ba681b4596..9f2ab8fbf26b4 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -82,10 +82,6 @@ export function buildSearchParams({ fields, runtime_mappings: runtimeMappings, _source: fields?.length ? false : undefined, - sort: - fields?.length && timeFieldName - ? [{ [timeFieldName]: { order: 'desc', unmapped_type: 'boolean' } }] - : undefined, }, track_total_hits: true, size: size ?? 0, diff --git a/test/api_integration/apis/unified_field_list/field_stats.ts b/test/api_integration/apis/unified_field_list/field_stats.ts index 39cc3709ab8aa..1a359c09f146b 100644 --- a/test/api_integration/apis/unified_field_list/field_stats.ts +++ b/test/api_integration/apis/unified_field_list/field_stats.ts @@ -408,35 +408,10 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - expect(body).to.eql({ - totalDocuments: 4634, - sampledDocuments: 100, - sampledValues: 100, - topValues: { - buckets: [ - { - count: 64, - key: 'jpg', - }, - { - count: 17, - key: 'png', - }, - { - count: 13, - key: 'css', - }, - { - count: 4, - key: 'gif', - }, - { - count: 2, - key: 'php', - }, - ], - }, - }); + expect(body.totalDocuments).to.eql(4634); + expect(body.sampledDocuments).to.eql(100); + expect(body.sampledValues).to.eql(100); + expect(body.topValues.buckets.length).to.eql(5); }); it('should return top values for index pattern runtime string fields', async () => { From 2524ffc4a28d67a958d4ad0dfd777844c39cb6bc Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 1 Sep 2022 13:43:49 +0200 Subject: [PATCH 83/92] [Discover] Prevent reduntant requests --- .../common/utils/field_examples_calculator.ts | 16 ++++++++++------ .../common/utils/field_stats_utils.ts | 6 +++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts index 07f9e4efb0c2e..de466611041b9 100644 --- a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts +++ b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts @@ -23,18 +23,22 @@ interface FieldValueCountsParams { count?: number; } +export const canFetchFieldExamples = (field: DataViewField): boolean => { + return !( + field.type === 'geo_point' || + field.type === 'geo_shape' || + field.type === 'attachment' || + field.type === 'unknown' + ); +}; + export function getFieldExampleBuckets(params: FieldValueCountsParams) { params = defaults(params, { count: 5, grouped: false, }); - if ( - params.field.type === 'geo_point' || - params.field.type === 'geo_shape' || - params.field.type === 'attachment' || - params.field.type === 'unknown' - ) { + if (!canFetchFieldExamples(params.field)) { throw new Error('Analysis is not available this field type'); } diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index 9f2ab8fbf26b4..2aebf8a535613 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -11,7 +11,7 @@ import DateMath from '@kbn/datemath'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import type { ESSearchResponse } from '@kbn/core/types/elasticsearch'; import type { FieldStatsResponse } from '../types'; -import { getFieldExampleBuckets } from './field_examples_calculator'; +import { getFieldExampleBuckets, canFetchFieldExamples } from './field_examples_calculator'; export type SearchHandler = ({ aggs, @@ -336,6 +336,10 @@ export async function getSimpleExamples( dataView: DataView ): Promise> { try { + if (!canFetchFieldExamples(field)) { + return {}; + } + const fieldRef = getFieldRef(field); const simpleExamplesBody = { From 1a2c4f571dc9d0cbe5b1474870c3f040ef5a4287 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 1 Sep 2022 13:53:51 +0200 Subject: [PATCH 84/92] [Discover] Increase examples size --- src/plugins/unified_field_list/common/utils/field_stats_utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index 2aebf8a535613..3aa9878d3b87a 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -353,6 +353,7 @@ export async function getSimpleExamples( hits: simpleExamplesResult.hits.hits, field, dataView, + count: DEFAULT_TOP_VALUES_SIZE, }); return { From a69517b0a8a6422a71dd86e029318d5d28c59550 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 1 Sep 2022 14:58:09 +0200 Subject: [PATCH 85/92] [Discover] Add exist filter to Discover popover --- .../components/sidebar/discover_field.tsx | 70 +++++++++++++------ .../discover_sidebar_responsive.test.tsx | 11 +++ 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index c475fde62b29c..dd45bc8b79e00 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -351,36 +351,66 @@ function DiscoverFieldComponent({ const { canEdit, canDelete } = getFieldCapabilities(dataView, field); const canEditField = onEditField && canEdit; const canDeleteField = onDeleteField && canDelete; + + const addExistFilterTooltip = i18n.translate( + 'discover.fieldChooser.discoverField.addExistFieldLabel', + { + defaultMessage: 'Filter for field present', + } + ); + + const editFieldTooltip = i18n.translate('discover.fieldChooser.discoverField.editFieldLabel', { + defaultMessage: 'Edit data view field', + }); + + const deleteFieldTooltip = i18n.translate( + 'discover.fieldChooser.discoverField.deleteFieldLabel', + { + defaultMessage: 'Delete data view field', + } + ); + const popoverTitle = (
{field.displayName}
+ {onAddFilter && !dataView.metaFields.includes(field.name) && !field.scripted && ( + + + { + setOpen(false); + onAddFilter('_exists_', field.name, '+'); + }} + iconType="filter" + data-test-subj={`discoverFieldListPanelAddExistFilter-${field.name}`} + aria-label={addExistFilterTooltip} + /> + + + )} {canEditField && ( - { - if (onEditField) { - togglePopover(); - onEditField(field.name); - } - }} - iconType="pencil" - data-test-subj={`discoverFieldListPanelEdit-${field.name}`} - aria-label={i18n.translate('discover.fieldChooser.discoverField.editFieldLabel', { - defaultMessage: 'Edit data view field', - })} - /> + + { + if (onEditField) { + togglePopover(); + onEditField(field.name); + } + }} + iconType="pencil" + data-test-subj={`discoverFieldListPanelEdit-${field.name}`} + aria-label={editFieldTooltip} + /> + )} {canDeleteField && ( - + { onDeleteField?.(field.name); @@ -388,9 +418,7 @@ function DiscoverFieldComponent({ iconType="trash" data-test-subj={`discoverFieldListPanelDelete-${field.name}`} color="danger" - aria-label={i18n.translate('discover.fieldChooser.discoverField.deleteFieldLabel', { - defaultMessage: 'Delete data view field', - })} + aria-label={deleteFieldTooltip} /> diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index c8f8495dcc887..cff0f5436047b 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -222,6 +222,17 @@ describe('discover responsive sidebar', function () { findTestSubject(comp, 'plus-extension-gif').simulate('click'); expect(props.onAddFilter).toHaveBeenCalled(); }); + it('should allow adding "exist" filter', async function () { + await act(async () => { + const button = findTestSubject(comp, 'field-extension-showDetails'); + await button.simulate('click'); + await comp.update(); + }); + + await comp.update(); + findTestSubject(comp, 'discoverFieldListPanelAddExistFilter-extension').simulate('click'); + expect(props.onAddFilter).toHaveBeenCalledWith('_exists_', 'extension', '+'); + }); it('should allow filtering by string, and calcFieldCount should just be executed once', function () { expect(findTestSubject(comp, 'fieldList-unpopular').children().length).toBe(6); act(() => { From 2db92bf237d8e0a314b91145907309f50153eae1 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 5 Sep 2022 14:15:53 +0200 Subject: [PATCH 86/92] [Discover] Update label --- .../field_stats/field_stats.test.tsx | 12 +-- .../components/field_stats/field_stats.tsx | 78 ++++++++----------- 2 files changed, 38 insertions(+), 52 deletions(-) diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx index 5dcb1ce6fa448..c18411b4b5959 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -324,11 +324,11 @@ describe('UnifiedFieldList ', () => { ).toBe('41.5%'); expect(wrapper.find('[data-test-subj="testing-statsFooter"]').first().text()).toBe( - '1624 records' + 'Calculated from 1624 records' ); expect(wrapper.text()).toBe( - 'Top values"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%1624 records' + 'Top values"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%Calculated from 1624 records' ); }); @@ -402,7 +402,7 @@ describe('UnifiedFieldList ', () => { expect(loadFieldStats).toHaveBeenCalledTimes(1); expect(wrapper.text()).toBe( - 'Examples"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%1624 records' + 'Examples"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%Calculated from 1624 records' ); }); @@ -506,10 +506,10 @@ describe('UnifiedFieldList ', () => { expect(wrapper.find('[data-test-subj="testing-topValues"]')).toHaveLength(0); expect(wrapper.find('[data-test-subj="testing-histogram"]')).toHaveLength(1); expect(wrapper.find('[data-test-subj="testing-statsFooter"]').first().text()).toBe( - '13 records' + 'Calculated from 13 records' ); - expect(wrapper.text()).toBe('Time distribution13 records'); + expect(wrapper.text()).toBe('Time distributionCalculated from 13 records'); }); it('should render Top Values & Distribution field stats correctly for a number field', async () => { @@ -594,7 +594,7 @@ describe('UnifiedFieldList ', () => { expect(loadFieldStats).toHaveBeenCalledTimes(1); expect(wrapper.text()).toBe( - 'Toggle either theTop valuesDistribution1273.9%1326.1%Based on 23% of 100 records' + 'Toggle either theTop valuesDistribution1273.9%1326.1%Calculated from sample of 23 records' ); }); }); diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 3781bfa31aa8b..4a66120c20c2f 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -20,14 +20,8 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import DateMath from '@kbn/datemath'; -import { - EuiButtonGroup, - EuiLoadingSpinner, - EuiSpacer, - EuiText, - EuiTitle, - EuiToolTip, -} from '@elastic/eui'; +import { EuiButtonGroup, EuiLoadingSpinner, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; import { Axis, Chart, @@ -241,47 +235,39 @@ const FieldStatsComponent: React.FC = ({ let title = <>; function combineWithTitleAndFooter(el: React.ReactElement) { - const percentage = - sampledDocuments && totalDocuments - ? Math.round((sampledDocuments / totalDocuments) * 100) - : 0; const countsElement = totalDocuments ? ( - {sampledDocuments && percentage < 100 && ( - <> - - - {i18n.translate('unifiedFieldList.fieldStats.basedOnPercentageOfLabel', { - defaultMessage: 'Based on {percentage}% of', - values: { - percentage, - }, - })} - - {' '} - + {sampledDocuments && sampledDocuments < totalDocuments ? ( + + {fieldFormats + .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) + .convert(sampledDocuments)} + + ), + }} + /> + ) : ( + + {fieldFormats + .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) + .convert(totalDocuments)} + + ), + }} + /> )} - - {fieldFormats - .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) - .convert(totalDocuments)} - {' '} - {i18n.translate('unifiedFieldList.fieldStats.ofRecordsLabel', { - defaultMessage: '{totalDocuments, plural, one {record} other {records}}', - values: { - totalDocuments, - }, - })} ) : ( <> From b16c2e6e556de0da6dd0279351b8b52a01bd138b Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 6 Sep 2022 09:33:22 +0200 Subject: [PATCH 87/92] [Discover] Update logic for picking a multifield --- .../application/main/components/sidebar/discover_field.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index dd45bc8b79e00..10138dec8b4cb 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -453,7 +453,11 @@ function DiscoverFieldComponent({ const renderPopover = () => { const dateRange = data?.query?.timefilter.timefilter.getAbsoluteTime(); - const fieldForStats = multiFields ? multiFields[0].field : field; // TODO: how to handle multifields? + // prioritize an aggregatable multi field if available or take the parent field + const fieldForStats = + (multiFields?.length && + multiFields.find((multiField) => multiField.field.aggregatable)?.field) || + field; const showLegacyFieldStats = services.uiSettings.get(SHOW_LEGACY_FIELD_TOP_VALUES); return ( From 32bc2db5fe96dab21e7d30d7d913452d334432aa Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 7 Sep 2022 13:20:44 +0200 Subject: [PATCH 88/92] [Discover] Fix how percentage is calculated for Examples view (non-aggregatable fields) --- .../utils/field_examples_calculator.test.ts | 79 +++-------------- .../common/utils/field_examples_calculator.ts | 84 +++++++++---------- .../common/utils/field_stats_utils.ts | 4 +- 3 files changed, 55 insertions(+), 112 deletions(-) diff --git a/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts b/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts index 24de6dc04ae54..01df564a3d82b 100644 --- a/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts +++ b/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts @@ -9,12 +9,7 @@ import { keys, clone, uniq, filter, map, flatten } from 'lodash'; import type { DataView } from '@kbn/data-views-plugin/public'; import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub'; -import { - getFieldExampleBuckets, - countMissing, - groupValues, - getFieldValues, -} from './field_examples_calculator'; +import { getFieldExampleBuckets, groupValues, getFieldValues } from './field_examples_calculator'; const hitsAsValues: Array> = [ { @@ -121,27 +116,8 @@ const hits = hitsAsValues.map((value) => ({ })); describe('fieldExamplesCalculator', function () { - it('should have a _countMissing that counts nulls & undefineds in an array', function () { - const values = [ - ['foo', 'bar'], - 'foo', - 'foo', - undefined, - ['foo', 'bar'], - 'bar', - 'baz', - null, - null, - null, - 'foo', - undefined, - ]; - expect(countMissing(values)).toBe(5); - }); - describe('groupValues', function () { - let grouped: { groups: Record; valuesCount: number }; - let params: any; + let grouped: { groups: Record; sampledValues: number }; let values: any; beforeEach(function () { values = [ @@ -149,7 +125,7 @@ describe('fieldExamplesCalculator', function () { 'foo', 'foo', undefined, - ['foo', 'bar'], + ['foo', 'bar', 'bar'], 'bar', 'baz', null, @@ -158,29 +134,27 @@ describe('fieldExamplesCalculator', function () { 'foo', undefined, ]; - params = {}; - grouped = groupValues(values, params); + grouped = groupValues(values); }); it('should have a groupValues that counts values', function () { expect(grouped.groups).toBeInstanceOf(Object); - expect(grouped.valuesCount).toBe(9); + expect(grouped.sampledValues).toBe(9); }); it('should throw an error if any value is a plain object', function () { expect(function () { - groupValues([{}, true, false], params); + groupValues([{}, true, false]); }).toThrowError(); }); it('should handle values with dots in them', function () { values = ['0', '0.........', '0.......,.....']; - params = {}; - grouped = groupValues(values, params); + grouped = groupValues(values); expect(grouped.groups[values[0]].count).toBe(1); expect(grouped.groups[values[1]].count).toBe(1); expect(grouped.groups[values[2]].count).toBe(1); - expect(grouped.valuesCount).toBe(3); + expect(grouped.sampledValues).toBe(3); }); it('should have a a key for value in the array when not grouping array terms', function () { @@ -195,29 +169,7 @@ describe('fieldExamplesCalculator', function () { expect(grouped.groups.foo.count).toBe(5); expect(grouped.groups.bar.count).toBe(3); expect(grouped.groups.baz.count).toBe(1); - expect(grouped.valuesCount).toBe(9); - }); - - describe('grouped array terms', function () { - beforeEach(function () { - params.grouped = true; - grouped = groupValues(values, params); - }); - - it('should group array terms when passed params.grouped', function () { - expect(keys(grouped.groups).length).toBe(4); - expect(grouped.groups['foo,bar']).toBeInstanceOf(Object); - }); - - it('should contain the original array as the value', function () { - expect(grouped.groups['foo,bar'].value).toEqual(['foo', 'bar']); - }); - - it('should count the pairs separately from the values they contain', function () { - expect(grouped.groups['foo,bar'].count).toBe(2); - expect(grouped.groups.foo.count).toBe(3); - expect(grouped.groups.bar.count).toBe(1); - }); + expect(grouped.sampledValues).toBe(9); }); }); @@ -284,21 +236,16 @@ describe('fieldExamplesCalculator', function () { }); it('counts the total hits', function () { - expect(getFieldExampleBuckets(params).total).toBe(params.hits.length); - }); - - it('counts the hits the field exists in', function () { - params.field = dataView.fields.getByName('phpmemory'); - expect(getFieldExampleBuckets(params).exists).toBe(5); + expect(getFieldExampleBuckets(params).sampledDocuments).toBe(params.hits.length); }); it('counts total number of values', function () { params.field = dataView.fields.getByName('@tags'); - expect(getFieldExampleBuckets(params).valuesCount).toBe(3); + expect(getFieldExampleBuckets(params).sampledValues).toBe(3); params.field = dataView.fields.getByName('extension'); - expect(getFieldExampleBuckets(params).valuesCount).toBe(params.hits.length); + expect(getFieldExampleBuckets(params).sampledValues).toBe(params.hits.length); params.field = dataView.fields.getByName('phpmemory'); - expect(getFieldExampleBuckets(params).valuesCount).toBe(5); + expect(getFieldExampleBuckets(params).sampledValues).toBe(5); }); }); }); diff --git a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts index de466611041b9..8f0fa76f055a4 100644 --- a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts +++ b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts @@ -8,7 +8,7 @@ // Adapted from src/plugins/discover/public/application/main/components/sidebar/lib/field_calculator.js -import { map, sortBy, without, each, defaults, isObject } from 'lodash'; +import { map, sortBy, defaults, isObject, pick } from 'lodash'; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { DataView, DataViewField } from '@kbn/data-plugin/common'; import { flattenHit } from '@kbn/data-plugin/common'; @@ -19,7 +19,6 @@ interface FieldValueCountsParams { hits: estypes.SearchHit[]; dataView: DataView; field: DataViewField; - grouped?: boolean; count?: number; } @@ -35,37 +34,27 @@ export const canFetchFieldExamples = (field: DataViewField): boolean => { export function getFieldExampleBuckets(params: FieldValueCountsParams) { params = defaults(params, { count: 5, - grouped: false, }); if (!canFetchFieldExamples(params.field)) { throw new Error('Analysis is not available this field type'); } - const allValues = getFieldValues(params.hits, params.field, params.dataView); - const missing = countMissing(allValues); - - const { groups, valuesCount } = groupValues(allValues, params); - const exampleBuckets = map( - sortBy(groups, 'count').reverse().slice(0, params.count), - function (bucket) { - return { - key: bucket.value, - count: bucket.count, - }; - } - ); + const records = getFieldValues(params.hits, params.field, params.dataView); + const { groups, sampledValues } = groupValues(records); + const buckets = sortBy(groups, ['count', 'order']) + .reverse() + .slice(0, params.count) + .map((bucket) => pick(bucket, ['key', 'count'])); - if (params.hits.length - missing === 0) { + if (!sampledValues) { throw new Error('No data for this field in the first found records'); } return { - total: params.hits.length, - exists: params.hits.length - missing, - missing, - valuesCount, - buckets: exampleBuckets, + buckets, + sampledValues, + sampledDocuments: params.hits.length, }; } @@ -79,43 +68,50 @@ export function getFieldValues( }); } -// returns a count of fields in the array that are undefined or null -export function countMissing(array: FieldHitValue[]): number { - return array.length - without(array, undefined, null).length; -} - -export function groupValues( - allValues: FieldHitValue[], - params: FieldValueCountsParams -): { groups: Record; valuesCount: number } { - const groups: Record = {}; - let k; +export function groupValues(records: FieldHitValue[]): { + groups: Record; + sampledValues: number; +} { + const groups: Record = {}; + let sampledValues = 0; // counts in each value's occurrence but only once per a record - allValues.forEach(function (value) { - if (isObject(value) && !Array.isArray(value)) { + records.forEach(function (recordValues) { + if (isObject(recordValues) && !Array.isArray(recordValues)) { throw new Error('Analysis is not available for object fields.'); } - if (Array.isArray(value) && !params.grouped) { - k = value; + let order = 0; // will be used for ordering terms with the same 'count' + let values: any[]; + const visitedValuesMap: Record = {}; + + if (Array.isArray(recordValues)) { + values = recordValues; } else { - k = value == null ? undefined : [value]; + values = recordValues == null ? [] : [recordValues]; } - each(k, function (key) { - if (groups.hasOwnProperty(key)) { - groups[key].count++; + values.forEach((value) => { + if (visitedValuesMap[value]) { + // already counted in groups + return; + } + + if (groups.hasOwnProperty(value)) { + groups[value].count++; } else { - groups[key] = { - value: params.grouped ? value : key, + groups[value] = { + key: value, count: 1, + order: order++, }; } + visitedValuesMap[value] = true; + sampledValues++; }); }); return { groups, - valuesCount: Object.values(groups).reduce((sum, group) => sum + group.count, 0), + sampledValues, }; } diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index b4f66ebcbe367..afdee14e013dd 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -361,8 +361,8 @@ export async function getSimpleExamples( return { totalDocuments: getHitsTotal(simpleExamplesResult), - sampledDocuments: fieldExampleBuckets.total, - sampledValues: fieldExampleBuckets.valuesCount, + sampledDocuments: fieldExampleBuckets.sampledDocuments, + sampledValues: fieldExampleBuckets.sampledValues, topValues: { buckets: fieldExampleBuckets.buckets, }, From 473ec1d2b7ff35c121763e1904953a7c73238171 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 9 Sep 2022 10:34:44 +0200 Subject: [PATCH 89/92] [Discover] Update copy and uncomment console error --- .../common/utils/field_stats_utils.ts | 2 +- .../components/field_stats/field_stats.test.tsx | 12 ++++++------ .../public/components/field_stats/field_stats.tsx | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index afdee14e013dd..ffda68fb02a00 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -368,7 +368,7 @@ export async function getSimpleExamples( }, }; } catch (error) { - // console.error(error) + console.error(error); // eslint-disable-line no-console return {}; } } diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx index c18411b4b5959..a0fb6db2bbb97 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -324,11 +324,11 @@ describe('UnifiedFieldList ', () => { ).toBe('41.5%'); expect(wrapper.find('[data-test-subj="testing-statsFooter"]').first().text()).toBe( - 'Calculated from 1624 records' + 'Calculated from 1624 records.' ); expect(wrapper.text()).toBe( - 'Top values"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%Calculated from 1624 records' + 'Top values"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%Calculated from 1624 records.' ); }); @@ -402,7 +402,7 @@ describe('UnifiedFieldList ', () => { expect(loadFieldStats).toHaveBeenCalledTimes(1); expect(wrapper.text()).toBe( - 'Examples"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%Calculated from 1624 records' + 'Examples"success"41.5%"info"37.1%"security"10.1%"warning"5.0%"error"3.4%"login"2.7%Calculated from 1624 records.' ); }); @@ -506,10 +506,10 @@ describe('UnifiedFieldList ', () => { expect(wrapper.find('[data-test-subj="testing-topValues"]')).toHaveLength(0); expect(wrapper.find('[data-test-subj="testing-histogram"]')).toHaveLength(1); expect(wrapper.find('[data-test-subj="testing-statsFooter"]').first().text()).toBe( - 'Calculated from 13 records' + 'Calculated from 13 records.' ); - expect(wrapper.text()).toBe('Time distributionCalculated from 13 records'); + expect(wrapper.text()).toBe('Time distributionCalculated from 13 records.'); }); it('should render Top Values & Distribution field stats correctly for a number field', async () => { @@ -594,7 +594,7 @@ describe('UnifiedFieldList ', () => { expect(loadFieldStats).toHaveBeenCalledTimes(1); expect(wrapper.text()).toBe( - 'Toggle either theTop valuesDistribution1273.9%1326.1%Calculated from sample of 23 records' + 'Toggle either theTop valuesDistribution1273.9%1326.1%Calculated from 23 sample records.' ); }); }); diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 4a66120c20c2f..f1c80e519a00f 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -240,7 +240,7 @@ const FieldStatsComponent: React.FC = ({ {sampledDocuments && sampledDocuments < totalDocuments ? ( = ({ ) : ( Date: Fri, 9 Sep 2022 19:58:40 +0200 Subject: [PATCH 90/92] [Discover] Add "no data" message and update field type check in examples --- .../utils/field_examples_calculator.test.ts | 40 +++++++++---------- .../common/utils/field_examples_calculator.ts | 19 +++------ .../common/utils/field_stats_utils.ts | 21 ++++++---- .../field_stats/field_stats.test.tsx | 4 +- .../components/field_stats/field_stats.tsx | 28 ++++++++++--- 5 files changed, 63 insertions(+), 49 deletions(-) diff --git a/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts b/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts index 01df564a3d82b..60957ce7da278 100644 --- a/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts +++ b/src/plugins/unified_field_list/common/utils/field_examples_calculator.test.ts @@ -175,14 +175,14 @@ describe('fieldExamplesCalculator', function () { describe('getFieldValues', function () { it('Should return an array of values for _source fields', function () { - const extensions = getFieldValues(hits, dataView.fields.getByName('extension')!, dataView); - expect(extensions).toBeInstanceOf(Array); + const values = getFieldValues(hits, dataView.fields.getByName('extension')!, dataView); + expect(values).toBeInstanceOf(Array); expect( - filter(extensions, function (v) { + filter(values, function (v) { return v.includes('html'); }).length ).toBe(8); - expect(uniq(flatten(clone(extensions))).sort()).toEqual(['gif', 'html', 'php', 'png']); + expect(uniq(flatten(clone(values))).sort()).toEqual(['gif', 'html', 'php', 'png']); }); it('Should return an array of values for core meta fields', function () { @@ -204,35 +204,31 @@ describe('fieldExamplesCalculator', function () { }); it('counts the top 3 values', function () { - const extensions = getFieldExampleBuckets(params); - expect(extensions).toBeInstanceOf(Object); - expect(extensions.buckets).toBeInstanceOf(Array); - expect(extensions.buckets.length).toBe(3); - expect(map(extensions.buckets, 'key')).toEqual(['html', 'php', 'gif']); + const result = getFieldExampleBuckets(params); + expect(result).toBeInstanceOf(Object); + expect(result.buckets).toBeInstanceOf(Array); + expect(result.buckets.length).toBe(3); + expect(map(result.buckets, 'key')).toEqual(['html', 'php', 'gif']); }); it('fails to analyze geo and attachment types', function () { params.field = dataView.fields.getByName('point'); - expect(() => getFieldExampleBuckets(params)).toThrowError( - 'Analysis is not available this field type' - ); + expect(() => getFieldExampleBuckets(params)).toThrowError(); params.field = dataView.fields.getByName('area'); - expect(() => getFieldExampleBuckets(params)).toThrowError( - 'Analysis is not available this field type' - ); + expect(() => getFieldExampleBuckets(params)).toThrowError(); params.field = dataView.fields.getByName('request_body'); - expect(() => getFieldExampleBuckets(params)).toThrowError( - 'Analysis is not available this field type' - ); + expect(() => getFieldExampleBuckets(params)).toThrowError(); + + params.field = dataView.fields.getByName('_score'); + expect(() => getFieldExampleBuckets(params)).toThrowError(); }); it('fails to analyze fields that are in the mapping, but not the hits', function () { - params.field = dataView.fields.getByName('ip'); - expect(() => getFieldExampleBuckets(params)).toThrowError( - 'No data for this field in the first found records' - ); + params.field = dataView.fields.getByName('machine.os'); + expect(getFieldExampleBuckets(params).buckets).toHaveLength(0); + expect(getFieldExampleBuckets(params).sampledValues).toBe(0); }); it('counts the total hits', function () { diff --git a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts index 8f0fa76f055a4..72f61dd6f210f 100644 --- a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts +++ b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts @@ -22,13 +22,8 @@ interface FieldValueCountsParams { count?: number; } -export const canFetchFieldExamples = (field: DataViewField): boolean => { - return !( - field.type === 'geo_point' || - field.type === 'geo_shape' || - field.type === 'attachment' || - field.type === 'unknown' - ); +export const canProvideExamplesForField = (field: DataViewField): boolean => { + return ['string', 'text', 'keyword', 'version', 'ip', 'number'].includes(field.type); }; export function getFieldExampleBuckets(params: FieldValueCountsParams) { @@ -36,8 +31,10 @@ export function getFieldExampleBuckets(params: FieldValueCountsParams) { count: 5, }); - if (!canFetchFieldExamples(params.field)) { - throw new Error('Analysis is not available this field type'); + if (!canProvideExamplesForField(params.field)) { + throw new Error( + `Analysis is not available this field type: "${params.field.type}". Field name: "${params.field.name}"` + ); } const records = getFieldValues(params.hits, params.field, params.dataView); @@ -47,10 +44,6 @@ export function getFieldExampleBuckets(params: FieldValueCountsParams) { .slice(0, params.count) .map((bucket) => pick(bucket, ['key', 'count'])); - if (!sampledValues) { - throw new Error('No data for this field in the first found records'); - } - return { buckets, sampledValues, diff --git a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts index ffda68fb02a00..0c35863a70114 100644 --- a/src/plugins/unified_field_list/common/utils/field_stats_utils.ts +++ b/src/plugins/unified_field_list/common/utils/field_stats_utils.ts @@ -11,7 +11,7 @@ import DateMath from '@kbn/datemath'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import type { ESSearchResponse } from '@kbn/core/types/elasticsearch'; import type { FieldStatsResponse } from '../types'; -import { getFieldExampleBuckets, canFetchFieldExamples } from './field_examples_calculator'; +import { getFieldExampleBuckets, canProvideExamplesForField } from './field_examples_calculator'; export type SearchHandler = ({ aggs, @@ -104,10 +104,12 @@ export async function fetchAndCalculateFieldStats({ size?: number; }) { if (!field.aggregatable) { - return await getSimpleExamples(searchHandler, field, dataView); + return canProvideExamplesForField(field) + ? await getSimpleExamples(searchHandler, field, dataView) + : {}; } - if (!canProvideStatsForField(field)) { + if (!canProvideAggregatedStatsForField(field)) { return {}; } @@ -126,7 +128,7 @@ export async function fetchAndCalculateFieldStats({ return await getStringSamples(searchHandler, field, size); } -export function canProvideStatsForField(field: DataViewField): boolean { +function canProvideAggregatedStatsForField(field: DataViewField): boolean { return !( field.type === 'document' || field.type.includes('range') || @@ -137,6 +139,13 @@ export function canProvideStatsForField(field: DataViewField): boolean { ); } +export function canProvideStatsForField(field: DataViewField): boolean { + return ( + (field.aggregatable && canProvideAggregatedStatsForField(field)) || + (!field.aggregatable && canProvideExamplesForField(field)) + ); +} + export async function getNumberHistogram( aggSearchWithBody: SearchHandler, field: DataViewField, @@ -339,10 +348,6 @@ export async function getSimpleExamples( dataView: DataView ): Promise> { try { - if (!canFetchFieldExamples(field)) { - return {}; - } - const fieldRef = getFieldRef(field); const simpleExamplesBody = { diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx index a0fb6db2bbb97..7bd416779dd5b 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -226,7 +226,9 @@ describe('UnifiedFieldList ', () => { expect(loadFieldStats).toHaveBeenCalled(); - expect(wrapper.text()).toBe('Analysis is not available for this field.'); + expect(wrapper.text()).toBe( + "This field is not available for visualizations because it doesn't have any data." + ); }); it('should render Top Values field stats correctly for a keyword field', async () => { diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index f1c80e519a00f..d62a08e9a3e0f 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -293,16 +293,34 @@ const FieldStatsComponent: React.FC = ({ ); } + if (!canProvideStatsForField(field)) { + const messageNoAnalysis = ( + + ); + + return overrideMissingContent + ? overrideMissingContent({ + noDataFound: false, + element: messageNoAnalysis, + }) + : messageNoAnalysis; + } + if ( (!histogram || histogram.buckets.length === 0) && (!topValues || topValues.buckets.length === 0) ) { - const message = ( + const messageNoData = ( @@ -310,10 +328,10 @@ const FieldStatsComponent: React.FC = ({ return overrideMissingContent ? overrideMissingContent({ - noDataFound: canProvideStatsForField(field), // TODO: should we have different messaging? - element: message, + noDataFound: true, + element: messageNoData, }) - : message; + : messageNoData; } if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { From 890acef25d039138e934a18ccb3b3c68881e8e47 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 14 Sep 2022 13:22:48 +0200 Subject: [PATCH 91/92] [Discover] Update type checks and no-data copy --- .../common/utils/field_examples_calculator.ts | 3 ++ .../field_stats/field_stats.test.tsx | 41 +++++++++++++++++-- .../components/field_stats/field_stats.tsx | 30 +++++++++----- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts index 72f61dd6f210f..6021146b2f960 100644 --- a/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts +++ b/src/plugins/unified_field_list/common/utils/field_examples_calculator.ts @@ -23,6 +23,9 @@ interface FieldValueCountsParams { } export const canProvideExamplesForField = (field: DataViewField): boolean => { + if (field.name === '_score') { + return false; + } return ['string', 'text', 'keyword', 'version', 'ip', 'number'].includes(field.type); }; diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx index 7bd416779dd5b..40e7ede5964e2 100644 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.test.tsx @@ -92,6 +92,13 @@ describe('UnifiedFieldList ', () => { aggregatable: true, searchable: true, }, + { + name: 'geo_shape', + displayName: 'geo_shape', + type: 'geo_shape', + aggregatable: true, + searchable: true, + }, ], getFormatterForField: jest.fn(() => ({ convert: jest.fn((s: unknown) => JSON.stringify(s)), @@ -219,16 +226,42 @@ describe('UnifiedFieldList ', () => { expect(wrapper.text()).toBe('Analysis is not available for this field.'); }); - it('should render nothing if no data is found', async () => { + it('should render a message if no data is found', async () => { const wrapper = await mountWithIntl(); await wrapper.update(); expect(loadFieldStats).toHaveBeenCalled(); - expect(wrapper.text()).toBe( - "This field is not available for visualizations because it doesn't have any data." - ); + expect(wrapper.text()).toBe('No field data for the current search.'); + }); + + it('should render a message if no data is found in sample', async () => { + let resolveFunction: (arg: unknown) => void; + + (loadFieldStats as jest.Mock).mockImplementation(() => { + return new Promise((resolve) => { + resolveFunction = resolve; + }); + }); + + const wrapper = mountWithIntl(); + + await wrapper.update(); + + await act(async () => { + resolveFunction!({ + totalDocuments: 10000, + sampledDocuments: 5000, + sampledValues: 0, + }); + }); + + await wrapper.update(); + + expect(loadFieldStats).toHaveBeenCalledTimes(1); + + expect(wrapper.text()).toBe('No field data for the current sample of 5000 records.'); }); it('should render Top Values field stats correctly for a keyword field', async () => { diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index d62a08e9a3e0f..2e63a1858166c 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -314,17 +314,27 @@ const FieldStatsComponent: React.FC = ({ (!histogram || histogram.buckets.length === 0) && (!topValues || topValues.buckets.length === 0) ) { - const messageNoData = ( - - ); + 'No field data for the current sample of {sampledDocumentsFormatted} {sampledDocuments, plural, one {record} other {records}}.', + values: { + sampledDocuments, + sampledDocumentsFormatted: fieldFormats + .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) + .convert(sampledDocuments), + }, + })} + /> + ) : ( + + ); return overrideMissingContent ? overrideMissingContent({ From 2eca3c319102a1695ee9c6555ce3379b485f844d Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 15 Sep 2022 10:24:49 +0200 Subject: [PATCH 92/92] [Discover] Update copy --- docs/management/advanced-options.asciidoc | 3 +-- src/plugins/discover/server/ui_settings.ts | 4 ++-- .../public/components/field_stats/field_stats.test.tsx | 2 +- .../public/components/field_stats/field_stats.tsx | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index dd3623272b4e4..af036a6e9b432 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -321,8 +321,7 @@ the minimum and maximum values of a numeric field or a map of a geo field. Controls the display of multi-fields in the expanded document view. [[discover:showLegacyFieldTopValues]]`discover:showLegacyFieldTopValues`:: -This setting will calculate Top Values for a field based only on the loaded records on Discover page. -To use the new and more accurate view, turn off this option. +To calculate the top values for a field in the sidebar using 500 instead of 5,000 records per shard, turn on this option. [[discover-sort-defaultorder]]`discover:sort:defaultOrder`:: The default sort direction for time-based data views. diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index 749a3b34cbff4..3f7ddf34331ca 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -127,13 +127,13 @@ export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record', () => { expect(loadFieldStats).toHaveBeenCalledTimes(1); - expect(wrapper.text()).toBe('No field data for the current sample of 5000 records.'); + expect(wrapper.text()).toBe('No field data for 5000 sample records.'); }); it('should render Top Values field stats correctly for a keyword field', async () => { diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index 2e63a1858166c..c70f1df820252 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -319,7 +319,7 @@ const FieldStatsComponent: React.FC = ({