diff --git a/packages/kbn-test-jest-helpers/src/enzyme_helpers.tsx b/packages/kbn-test-jest-helpers/src/enzyme_helpers.tsx index 8388ed55eb514f..9f4e691600c064 100644 --- a/packages/kbn-test-jest-helpers/src/enzyme_helpers.tsx +++ b/packages/kbn-test-jest-helpers/src/enzyme_helpers.tsx @@ -15,7 +15,7 @@ import { I18nProvider, InjectedIntl, intlShape, __IntlProvider } from '@kbn/i18n-react'; import { mount, ReactWrapper, render, shallow } from 'enzyme'; -import React, { ReactElement, ValidationMap } from 'react'; +import React, { ComponentType, ReactElement, ValidationMap } from 'react'; import { act as reactAct } from 'react-dom/test-utils'; // Use fake component to extract `intl` property to use in tests. @@ -94,6 +94,8 @@ export function mountWithIntl( attachTo?: HTMLElement; context?: any; childContextTypes?: ValidationMap; + wrappingComponent?: ComponentType | undefined; + wrappingComponentProps?: {} | undefined; } = {} ) { const options = getOptions(context, childContextTypes, props); diff --git a/src/plugins/chart_expressions/expression_metric/public/components/currency_codes.test.ts b/src/plugins/chart_expressions/expression_metric/public/components/currency_codes.test.ts deleted file mode 100644 index 03266f31894605..00000000000000 --- a/src/plugins/chart_expressions/expression_metric/public/components/currency_codes.test.ts +++ /dev/null @@ -1,33 +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 { getCurrencyCode } from './currency_codes'; -// @ts-ignore -import numeralLanguages from '@elastic/numeral/languages'; - -describe('getCurrencyCode', () => { - const allLanguages = [ - ['en', '$'], - ...numeralLanguages.map((language: { id: string; lang: { currency: { symbol: string } } }) => { - const { - id, - lang: { - currency: { symbol }, - }, - } = language; - return [id, symbol]; - }), - ]; - - it.each(allLanguages)( - 'should have currency code for locale "%s" and currency "%s"', - (locale, symbol) => { - expect(getCurrencyCode(locale, symbol)).toBeDefined(); - } - ); -}); diff --git a/src/plugins/chart_expressions/expression_metric/public/components/currency_codes.ts b/src/plugins/chart_expressions/expression_metric/public/components/currency_codes.ts deleted file mode 100644 index 8bc288a3ac5902..00000000000000 --- a/src/plugins/chart_expressions/expression_metric/public/components/currency_codes.ts +++ /dev/null @@ -1,46 +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. - */ - -// NOTE: needs to be kept in line with https://github.com/elastic/numeral-js/blob/kibana-fork/languages.js + USD -const currencyCodeMap: Record = { - 'en-$': 'USD', - 'be-nl-€': 'EUR', - 'chs-¥': 'CNY', - 'cs-kč': 'CZK', - 'da-dk-dkk': 'DKK', - 'de-ch-chf': 'CHF', - 'de-€': 'EUR', - 'en-gb-£': 'GBP', - 'es-es-€': 'EUR', - 'es-$': '', - 'et-€': 'EUR', - 'fi-€': 'EUR', - 'fr-ca-$': 'CAD', - 'fr-ch-chf': 'CHF', - 'fr-€': 'EUR', - 'hu-ft': 'HUF', - 'it-€': 'EUR', - 'ja-¥': 'JPY', - 'nl-nl-€': 'EUR', - 'pl-pln': 'PLN', - 'pt-br-r$': 'BRL', - 'pt-pt-€': 'EUR', - 'ru-ua-₴': 'UAH', - 'ru-руб.': 'RUB', - 'sk-€': 'EUR', - 'th-฿': 'THB', - 'tr-₺': 'TRY', - 'uk-ua-₴': 'UAH', -}; - -/** - * Returns currency code for use with the Intl API. - */ -export const getCurrencyCode = (localeId: string, currencySymbol: string) => { - return currencyCodeMap[`${localeId.trim()}-${currencySymbol.trim()}`.toLowerCase()]; -}; diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx index c4b130aa3e5074..d32b0ccfadf6f0 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Datatable } from '@kbn/expressions-plugin/common'; +import { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common'; import { MetricVis, MetricVisComponentProps } from './metric_vis'; import { LayoutDirection, @@ -21,7 +21,7 @@ import { } from '@elastic/charts'; import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import { SerializableRecord } from '@kbn/utility-types'; -import numeral from '@elastic/numeral'; +import type { IUiSettingsClient } from '@kbn/core/public'; import { HtmlAttributes } from 'csstype'; import { CustomPaletteState } from '@kbn/charts-plugin/common/expressions/palette/types'; import { DimensionsVisParam } from '../../common'; @@ -29,19 +29,17 @@ import { euiThemeVars } from '@kbn/ui-theme'; import { DEFAULT_TRENDLINE_NAME } from '../../common/constants'; import faker from 'faker'; -const mockDeserialize = jest.fn((params) => { - const converter = - params.id === 'terms' - ? (val: string) => (val === '__other__' ? 'Other' : val) - : params.id === 'string' - ? (val: string) => (val === '' ? '(empty)' : val) - : () => 'formatted duration'; - return { getConverterFor: jest.fn(() => converter) }; +const mockDeserialize = jest.fn(({ id }: { id: string }) => { + const convertFn = (v: unknown) => `${id}-${v}`; + return { getConverterFor: () => convertFn }; }); const mockGetColorForValue = jest.fn(() => undefined); -const mockLookupCurrentLocale = jest.fn(() => 'en'); +const CURRENCY_DEFAULT_FORMAT = '$0.0'; + +const mockFormatSettingLookup = jest.fn(() => CURRENCY_DEFAULT_FORMAT); +const mockIsOverridden = jest.fn(); jest.mock('../services', () => ({ getFormatService: () => { @@ -53,13 +51,13 @@ jest.mock('../services', () => ({ get: jest.fn(() => ({ getColorForValue: mockGetColorForValue })), }), getThemeService: () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { getThemeService } = require('../__mocks__/theme_service'); + const { getThemeService } = jest.requireActual('../__mocks__/theme_service'); return getThemeService(); }, getUiSettingsService: () => { return { - get: mockLookupCurrentLocale, + get: mockFormatSettingLookup, + isOverridden: mockIsOverridden, }; }, })); @@ -70,17 +68,6 @@ jest.mock('@kbn/field-formats-plugin/common', () => ({ }, })); -jest.mock('@elastic/numeral', () => { - const actualNumeral = jest.requireActual('@elastic/numeral'); - actualNumeral.language = jest.fn(() => 'en'); - actualNumeral.languageData = jest.fn(() => ({ - currency: { - symbol: '$', - }, - })); - return actualNumeral; -}); - type Props = MetricVisComponentProps; const dayOfWeekColumnId = 'col-0-0'; @@ -220,6 +207,7 @@ const defaultProps = { fireEvent: () => {}, filterable: true, renderMode: 'view', + uiSettings: {} as unknown as IUiSettingsClient, } as Pick; describe('MetricVisComponent', function () { @@ -292,7 +280,7 @@ describe('MetricVisComponent', function () { expect(configNoPrefix!.extra).toEqual( {table.columns.find((col) => col.id === minPriceColumnId)!.name} - {' ' + 13.63} + {` number-13.6328125`} ); @@ -305,7 +293,7 @@ describe('MetricVisComponent', function () { expect(configWithPrefix!.extra).toEqual( {'secondary prefix'} - {' ' + 13.63} + {` number-13.6328125`} ); @@ -314,7 +302,7 @@ describe('MetricVisComponent', function () { "color": "#f5f7fa", "extra": secondary prefix - 13.63 + number-13.6328125 , "icon": [Function], "subtitle": "subtitle", @@ -441,7 +429,7 @@ describe('MetricVisComponent', function () { "extra": , "icon": undefined, "subtitle": "Median products.base_price", - "title": "Friday", + "title": "terms-Friday", "value": 28.984375, "valueFormatter": [Function], }, @@ -450,7 +438,7 @@ describe('MetricVisComponent', function () { "extra": , "icon": undefined, "subtitle": "Median products.base_price", - "title": "Wednesday", + "title": "terms-Wednesday", "value": 28.984375, "valueFormatter": [Function], }, @@ -459,7 +447,7 @@ describe('MetricVisComponent', function () { "extra": , "icon": undefined, "subtitle": "Median products.base_price", - "title": "Saturday", + "title": "terms-Saturday", "value": 25.984375, "valueFormatter": [Function], }, @@ -468,7 +456,7 @@ describe('MetricVisComponent', function () { "extra": , "icon": undefined, "subtitle": "Median products.base_price", - "title": "Sunday", + "title": "terms-Sunday", "value": 25.784375, "valueFormatter": [Function], }, @@ -477,7 +465,7 @@ describe('MetricVisComponent', function () { "extra": , "icon": undefined, "subtitle": "Median products.base_price", - "title": "Thursday", + "title": "terms-Thursday", "value": 25.348011363636363, "valueFormatter": [Function], }, @@ -508,23 +496,23 @@ describe('MetricVisComponent', function () { Array [ howdy - 13.63 + number-13.6328125 , howdy - 13.64 + number-13.639539930555555 , howdy - 13.34 + number-13.34375 , howdy - 13.49 + number-13.4921875 , howdy - 13.34 + number-13.34375 , ] `); @@ -606,7 +594,7 @@ describe('MetricVisComponent', function () { "extra": , "icon": undefined, "subtitle": "Median products.base_price", - "title": "Friday", + "title": "terms-Friday", "value": 28.984375, "valueFormatter": [Function], }, @@ -615,7 +603,7 @@ describe('MetricVisComponent', function () { "extra": , "icon": undefined, "subtitle": "Median products.base_price", - "title": "Wednesday", + "title": "terms-Wednesday", "value": 28.984375, "valueFormatter": [Function], }, @@ -624,7 +612,7 @@ describe('MetricVisComponent', function () { "extra": , "icon": undefined, "subtitle": "Median products.base_price", - "title": "Saturday", + "title": "terms-Saturday", "value": 25.984375, "valueFormatter": [Function], }, @@ -633,7 +621,7 @@ describe('MetricVisComponent', function () { "extra": , "icon": undefined, "subtitle": "Median products.base_price", - "title": "Sunday", + "title": "terms-Sunday", "value": 25.784375, "valueFormatter": [Function], }, @@ -642,7 +630,7 @@ describe('MetricVisComponent', function () { "extra": , "icon": undefined, "subtitle": "Median products.base_price", - "title": "Thursday", + "title": "terms-Thursday", "value": 25.348011363636363, "valueFormatter": [Function], }, @@ -653,7 +641,7 @@ describe('MetricVisComponent', function () { "extra": , "icon": undefined, "subtitle": "Median products.base_price", - "title": "Other", + "title": "terms-__other__", "value": 24.984375, "valueFormatter": [Function], }, @@ -696,7 +684,7 @@ describe('MetricVisComponent', function () { "icon": undefined, "progressBarDirection": "vertical", "subtitle": "Median products.base_price", - "title": "Friday", + "title": "terms-Friday", "value": 28.984375, "valueFormatter": [Function], }, @@ -707,7 +695,7 @@ describe('MetricVisComponent', function () { "icon": undefined, "progressBarDirection": "vertical", "subtitle": "Median products.base_price", - "title": "Wednesday", + "title": "terms-Wednesday", "value": 28.984375, "valueFormatter": [Function], }, @@ -718,7 +706,7 @@ describe('MetricVisComponent', function () { "icon": undefined, "progressBarDirection": "vertical", "subtitle": "Median products.base_price", - "title": "Saturday", + "title": "terms-Saturday", "value": 25.984375, "valueFormatter": [Function], }, @@ -729,7 +717,7 @@ describe('MetricVisComponent', function () { "icon": undefined, "progressBarDirection": "vertical", "subtitle": "Median products.base_price", - "title": "Sunday", + "title": "terms-Sunday", "value": 25.784375, "valueFormatter": [Function], }, @@ -740,7 +728,7 @@ describe('MetricVisComponent', function () { "icon": undefined, "progressBarDirection": "vertical", "subtitle": "Median products.base_price", - "title": "Thursday", + "title": "terms-Thursday", "value": 25.348011363636363, "valueFormatter": [Function], }, @@ -753,7 +741,7 @@ describe('MetricVisComponent', function () { "icon": undefined, "progressBarDirection": "vertical", "subtitle": "Median products.base_price", - "title": "Other", + "title": "terms-__other__", "value": 24.984375, "valueFormatter": [Function], }, @@ -762,6 +750,7 @@ describe('MetricVisComponent', function () { `); }); it('should configure trendlines if provided', () => { + // Raw values here, not formatted const trends: Record = { Friday: [ { x: faker.random.number(), y: faker.random.number() }, @@ -793,7 +782,7 @@ describe('MetricVisComponent', function () { { x: faker.random.number(), y: faker.random.number() }, { x: faker.random.number(), y: faker.random.number() }, ], - Other: [ + __other__: [ { x: faker.random.number(), y: faker.random.number() }, { x: faker.random.number(), y: faker.random.number() }, { x: faker.random.number(), y: faker.random.number() }, @@ -825,7 +814,8 @@ describe('MetricVisComponent', function () { .props().data![0] as MetricWTrend[]; data?.forEach((tileConfig) => { - expect(tileConfig.trend).toEqual(trends[tileConfig.title!]); + // title has been formatted, so clean it up before using as index + expect(tileConfig.trend).toEqual(trends[tileConfig.title!.replace('terms-', '')]); expect(tileConfig.trendShape).toEqual('area'); }); }); @@ -1325,10 +1315,13 @@ describe('MetricVisComponent', function () { }); describe('metric value formatting', () => { + function nonNullable(v: T): v is NonNullable { + return v != null; + } const getFormattedMetrics = ( value: number | string, - secondaryValue: number | string, - fieldFormatter: SerializedFieldFormat + secondaryValue: number | string | undefined, + fieldFormatter: SerializedFieldFormat | undefined ) => { const config: Props['config'] = { metric: { @@ -1337,7 +1330,7 @@ describe('MetricVisComponent', function () { }, dimensions: { metric: '1', - secondaryMetric: '2', + secondaryMetric: secondaryValue ? '2' : undefined, }, }; @@ -1352,12 +1345,14 @@ describe('MetricVisComponent', function () { name: '', meta: { type: 'number', params: fieldFormatter }, }, - { - id: '2', - name: '', - meta: { type: 'number', params: fieldFormatter }, - }, - ], + secondaryValue + ? { + id: '2', + name: '', + meta: { type: 'number', params: fieldFormatter }, + } + : undefined, + ].filter(nonNullable) as DatatableColumn[], rows: [{ '1': value, '2': secondaryValue }], }} {...defaultProps} @@ -1373,123 +1368,98 @@ describe('MetricVisComponent', function () { return { primary: valueFormatter(primaryMetric), secondary: extra?.props.children[1] }; }; - it('correctly formats plain numbers', () => { - const { primary, secondary } = getFormattedMetrics(394.2393, 983123.984, { id: 'number' }); - expect(primary).toBe('394.24'); - expect(secondary).toBe('983.12K'); - }); - - it('correctly formats strings', () => { - const { primary, secondary } = getFormattedMetrics('', '', { id: 'string' }); - expect(primary).toBe('(empty)'); - expect(secondary).toBe('(empty)'); - }); - - it('correctly formats currency', () => { - const { primary, secondary } = getFormattedMetrics(1000.839, 11.2, { id: 'currency' }); - expect(primary).toBe('$1.00K'); - expect(secondary).toBe('$11.20'); + it.each` + id | pattern | finalPattern + ${'number'} | ${'0'} | ${'0'} + ${'currency'} | ${'$0'} | ${'$0'} + ${'percent'} | ${'0%'} | ${'0%'} + `( + 'applies $id custom field format pattern when passed over', + ({ id, pattern, finalPattern }) => { + getFormattedMetrics(394.2393, 983123.984, { id, params: { pattern } }); + expect(mockDeserialize).toHaveBeenCalledTimes(2); + expect(mockDeserialize).toHaveBeenCalledWith({ id, params: { pattern: finalPattern } }); + } + ); - mockLookupCurrentLocale.mockReturnValueOnce('be-nl'); - // @ts-expect-error - (numeral.languageData as jest.Mock).mockReturnValueOnce({ - currency: { - symbol: '€', - }, - }); + it.each` + id + ${'number'} + ${'percent'} + `( + 'does not apply the metric compact format if user customized default settings pattern for $id', + ({ id }) => { + mockIsOverridden.mockReturnValueOnce(true); + getFormattedMetrics(394.2393, 983123.984, { id }); + expect(mockDeserialize).toHaveBeenCalledTimes(2); + expect(mockDeserialize).toHaveBeenCalledWith({ id }); + } + ); - const { primary: primaryEuro } = getFormattedMetrics(1000.839, 0, { - id: 'currency', + it('applies a custom duration configuration to the formatter', () => { + getFormattedMetrics(394.2393, 983123.984, { id: 'duration' }); + expect(mockDeserialize).toHaveBeenCalledTimes(2); + expect(mockDeserialize).toHaveBeenCalledWith({ + id: 'duration', + params: { outputFormat: 'humanizePrecise', outputPrecision: 1, useShortSuffix: true }, }); - expect(primaryEuro).toBe('1,00 тыс. €'); - // check that we restored the numeral.js state - expect(numeral.language).toHaveBeenLastCalledWith('en'); }); - it('correctly formats percentages', () => { - const { primary, secondary } = getFormattedMetrics(0.23939, 11.2, { id: 'percent' }); - expect(primary).toBe('23.94%'); - expect(secondary).toBe('1.12K%'); + it('does not override duration custom configuration when set', () => { + getFormattedMetrics(394.2393, 983123.984, { + id: 'duration', + params: { useShortSuffix: false }, + }); + expect(mockDeserialize).toHaveBeenCalledTimes(2); + expect(mockDeserialize).toHaveBeenCalledWith({ + id: 'duration', + params: { outputFormat: 'humanizePrecise', outputPrecision: 1, useShortSuffix: false }, + }); }); - it('correctly formats bytes', () => { - const base = 1024; - - const { primary: bytesValue } = getFormattedMetrics(base - 1, 0, { id: 'bytes' }); - expect(bytesValue).toBe('1,023 B'); - - const { primary: kiloBytesValue } = getFormattedMetrics(Math.pow(base, 1), 0, { + it('does not tweak bytes format when passed', () => { + getFormattedMetrics(394.2393, 983123.984, { id: 'bytes', }); - expect(kiloBytesValue).toBe('1 KB'); - - const { primary: megaBytesValue } = getFormattedMetrics(Math.pow(base, 2), 0, { + expect(mockDeserialize).toHaveBeenCalledTimes(2); + expect(mockDeserialize).toHaveBeenCalledWith({ id: 'bytes', }); - expect(megaBytesValue).toBe('1 MB'); + }); - const { primary: moreThanPetaValue } = getFormattedMetrics(Math.pow(base, 6), 0, { + it('does not tweak bit format when passed', () => { + getFormattedMetrics(394.2393, 983123.984, { + id: 'bytes', + params: { pattern: '0.0bitd' }, + }); + expect(mockDeserialize).toHaveBeenCalledTimes(2); + expect(mockDeserialize).toHaveBeenCalledWith({ id: 'bytes', + params: { pattern: '0.0bitd' }, }); - expect(moreThanPetaValue).toBe('1 EB'); }); - it('correctly formats bits (decimal)', () => { - const base = 1000; - const bitFormat = { + it('does not tweak legacy bits format when passed', () => { + const legacyBitFormat = { id: 'number', - params: { pattern: '0.0bitd' }, + params: { pattern: `0,0bitd` }, }; - - const { primary: bytesValue } = getFormattedMetrics(base - 1, 0, bitFormat); - expect(bytesValue).toBe('999 bit'); - - const { primary: kiloBytesValue } = getFormattedMetrics(Math.pow(base, 1), 0, bitFormat); - expect(kiloBytesValue).toBe('1 kbit'); - - const { primary: megaBytesValue } = getFormattedMetrics(Math.pow(base, 2), 0, bitFormat); - expect(megaBytesValue).toBe('1 Mbit'); - - const { primary: moreThanPetaValue } = getFormattedMetrics(Math.pow(base, 6), 0, bitFormat); - expect(moreThanPetaValue).toBe('1 Ebit'); + getFormattedMetrics(394.2393, 983123.984, legacyBitFormat); + expect(mockDeserialize).toHaveBeenCalledTimes(2); + expect(mockDeserialize).toHaveBeenCalledWith(legacyBitFormat); }); - it('correctly formats durations', () => { - const { primary, secondary } = getFormattedMetrics(1, 1, { - id: 'duration', - params: { - // the following params should be preserved - inputFormat: 'minutes', - // the following params should be overridden - outputFormat: 'precise', - outputPrecision: 2, - useShortSuffix: false, - }, - }); - - expect(primary).toBe('formatted duration'); - expect(secondary).toBe('formatted duration'); - expect(mockDeserialize).toHaveBeenCalledTimes(2); - expect(mockDeserialize).toHaveBeenCalledWith({ - id: 'duration', - params: { - inputFormat: 'minutes', - outputFormat: 'humanizePrecise', - outputPrecision: 1, - useShortSuffix: true, - }, - }); + it('calls the formatter only once when no secondary value is passed', () => { + getFormattedMetrics(394.2393, undefined, { id: 'number' }); + expect(mockDeserialize).toHaveBeenCalledTimes(1); }); - it('ignores suffix formatting', () => { - const { primary, secondary } = getFormattedMetrics(0.23939, 11.2, { - id: 'suffix', - params: { - id: 'percent', - }, - }); - expect(primary).toBe('23.94%'); - expect(secondary).toBe('1.12K%'); + it('still call the numeric formatter when no format is passed', () => { + const { primary, secondary } = getFormattedMetrics(394.2393, 983123.984, undefined); + expect(mockDeserialize).toHaveBeenCalledTimes(2); + expect(mockDeserialize).toHaveBeenCalledWith({ id: 'number' }); + expect(primary).toBe('number-394.2393'); + expect(secondary).toBe('number-983123.984'); }); }); diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx index d20cccb46617f6..e4c9f77b94e941 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx @@ -8,7 +8,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import { Chart, @@ -31,8 +30,10 @@ import type { RenderMode, } from '@kbn/expressions-plugin/common'; import { CustomPaletteState } from '@kbn/charts-plugin/public'; -import { FORMATS_UI_SETTINGS, type SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; -import type { FieldFormatConvertFunction } from '@kbn/field-formats-plugin/common'; +import { + FieldFormatConvertFunction, + SerializedFieldFormat, +} from '@kbn/field-formats-plugin/common'; import { CUSTOM_PALETTE } from '@kbn/coloring'; import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; @@ -41,104 +42,36 @@ import { AllowedSettingsOverrides } from '@kbn/charts-plugin/common'; import { getOverridesFor } from '@kbn/chart-expressions-common'; import { DEFAULT_TRENDLINE_NAME } from '../../common/constants'; import { VisParams } from '../../common'; -import { - getPaletteService, - getThemeService, - getFormatService, - getUiSettingsService, -} from '../services'; -import { getCurrencyCode } from './currency_codes'; +import { getPaletteService, getThemeService, getFormatService } from '../services'; import { getDataBoundsForPalette } from '../utils'; export const defaultColor = euiThemeVars.euiColorLightestShade; -function getFormatId(serializedFieldFormat: SerializedFieldFormat | undefined): string | undefined { - if (serializedFieldFormat?.id === 'suffix') { - return `${serializedFieldFormat.params?.id || ''}`; - } - if (/bitd/.test(`${serializedFieldFormat?.params?.pattern || ''}`)) { - return 'bit'; - } - return serializedFieldFormat?.id; -} - -const getMetricFormatter = ( - accessor: ExpressionValueVisDimension | string, - columns: Datatable['columns'] -) => { - const serializedFieldFormat = getFormatByAccessor(accessor, columns); - const formatId = getFormatId(serializedFieldFormat) || 'number'; - - if ( - !['number', 'currency', 'percent', 'bytes', 'bit', 'duration', 'string', 'null'].includes( - formatId - ) - ) { - throw new Error( - i18n.translate('expressionMetricVis.errors.unsupportedColumnFormat', { - defaultMessage: 'Metric visualization expression - Unsupported column format: "{id}"', - values: { - id: formatId, - }, - }) - ); - } - - // this formats are coming when formula is empty - if (formatId === 'string') { - return getFormatService().deserialize(serializedFieldFormat).getConverterFor('text'); - } - +function enhanceFieldFormat(serializedFieldFormat: SerializedFieldFormat | undefined) { + const formatId = serializedFieldFormat?.id || 'number'; if (formatId === 'duration') { - const formatter = getFormatService().deserialize({ + return { ...serializedFieldFormat, params: { - ...serializedFieldFormat!.params, + // by default use the compact precise format outputFormat: 'humanizePrecise', outputPrecision: 1, useShortSuffix: true, + // but if user configured something else, use it + ...serializedFieldFormat!.params, }, - }); - return formatter.getConverterFor('text'); - } - - const uiSettings = getUiSettingsService(); - - const locale = uiSettings.get(FORMATS_UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE) || 'en'; - - const intlOptions: Intl.NumberFormatOptions = { - maximumFractionDigits: 2, - }; - - if (['number', 'currency', 'percent'].includes(formatId)) { - intlOptions.notation = 'compact'; - } - - if (formatId === 'currency') { - const currentNumeralLang = numeral.language(); - numeral.language(locale); - - const { - currency: { symbol: currencySymbol }, - // @ts-expect-error - } = numeral.languageData(); - - // restore previous value - numeral.language(currentNumeralLang); - - intlOptions.currency = getCurrencyCode(locale, currencySymbol); - intlOptions.style = 'currency'; - } - - if (formatId === 'percent') { - intlOptions.style = 'percent'; + }; } + return serializedFieldFormat ?? { id: formatId }; +} - return ['bit', 'bytes'].includes(formatId) - ? (rawValue: number) => { - return numeral(rawValue).format(`0,0[.]00 ${formatId === 'bytes' ? 'b' : 'bitd'}`); - } - : new Intl.NumberFormat(locale, intlOptions).format; +const getMetricFormatter = ( + accessor: ExpressionValueVisDimension | string, + columns: Datatable['columns'] +) => { + const serializedFieldFormat = getFormatByAccessor(accessor, columns); + const enhancedFieldFormat = enhanceFieldFormat(serializedFieldFormat); + return getFormatService().deserialize(enhancedFieldFormat).getConverterFor('text'); }; const getColor = ( @@ -197,6 +130,22 @@ export const MetricVis = ({ filterable, overrides, }: MetricVisComponentProps) => { + const chartTheme = getThemeService().useChartsTheme(); + const onRenderChange = useCallback( + (isRendered) => { + if (isRendered) { + renderComplete(); + } + }, + [renderComplete] + ); + + const [scrollChildHeight, setScrollChildHeight] = useState('100%'); + const scrollContainerRef = useRef(null); + const scrollDimensions = useResizeObserver(scrollContainerRef.current); + + const baseTheme = getThemeService().useChartsBaseTheme(); + const primaryMetricColumn = getColumnByAccessor(config.dimensions.metric, data.columns)!; const formatPrimaryMetric = getMetricFormatter(config.dimensions.metric, data.columns); @@ -295,58 +244,46 @@ export const MetricVis = ({ }); if (config.metric.minTiles) { - while (metricConfigs.length < config.metric.minTiles) metricConfigs.push(undefined); + while (metricConfigs.length < config.metric.minTiles) { + metricConfigs.push(undefined); + } } - const grid: MetricSpec['data'] = []; const { metric: { maxCols }, } = config; - for (let i = 0; i < metricConfigs.length; i += maxCols) { - grid.push(metricConfigs.slice(i, i + maxCols)); - } - - const chartTheme = getThemeService().useChartsTheme(); - const onRenderChange = useCallback( - (isRendered) => { - if (isRendered) { - renderComplete(); - } - }, - [renderComplete] - ); - - let pixelHeight; - let pixelWidth; - if (renderMode === 'edit') { - // In the editor, we constrain the maximum size of the tiles for aesthetic reasons - const maxTileSideLength = metricConfigs.flat().length > 1 ? 200 : 300; - pixelHeight = grid.length * maxTileSideLength; - pixelWidth = grid[0]?.length * maxTileSideLength; - } - - const [scrollChildHeight, setScrollChildHeight] = useState('100%'); - const scrollContainerRef = useRef(null); - const scrollDimensions = useResizeObserver(scrollContainerRef.current); - - const baseTheme = getThemeService().useChartsBaseTheme(); + const numRows = metricConfigs.length / maxCols; const minHeight = chartTheme.metric?.minHeight ?? baseTheme.metric.minHeight; useEffect(() => { - const minimumRequiredVerticalSpace = minHeight * grid.length; + const minimumRequiredVerticalSpace = minHeight * numRows; setScrollChildHeight( (scrollDimensions.height ?? -Infinity) > minimumRequiredVerticalSpace ? '100%' : `${minimumRequiredVerticalSpace}px` ); - }, [grid.length, minHeight, scrollDimensions.height]); + }, [numRows, minHeight, scrollDimensions.height]); const { theme: settingsThemeOverrides = {}, ...settingsOverrides } = getOverridesFor( overrides, 'settings' ) as Partial; + const grid: MetricSpec['data'] = []; + for (let i = 0; i < metricConfigs.length; i += maxCols) { + grid.push(metricConfigs.slice(i, i + maxCols)); + } + + let pixelHeight; + let pixelWidth; + if (renderMode === 'edit') { + // In the editor, we constrain the maximum size of the tiles for aesthetic reasons + const maxTileSideLength = metricConfigs.flat().length > 1 ? 200 : 300; + pixelHeight = grid.length * maxTileSideLength; + pixelWidth = grid[0]?.length * maxTileSideLength; + } + return (
{ + const colRef = breakdownByColumn ?? primaryMetricColumn; + const rowLength = grid[0].length; events.forEach((event) => { if (isMetricElementEvent(event)) { - const colIdx = breakdownByColumn - ? data.columns.findIndex((col) => col === breakdownByColumn) - : data.columns.findIndex((col) => col === primaryMetricColumn); - const rowLength = grid[0].length; + const colIdx = data.columns.findIndex((col) => col === colRef); fireEvent( buildFilterEvent( event.rowIndex * rowLength + event.columnIndex, diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts index 892783e41788cb..ff90e1d40d30c3 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts @@ -221,10 +221,10 @@ describe('format_column', () => { expect(result.columns[0].meta.params).toEqual({ id: 'wrapper', params: { + formatOverride: true, wrapperParam: 123, id: 'number', params: { formatOverride: true, pattern: '0,0.00000' }, - formatOverride: true, pattern: '0,0.00000', }, }); @@ -265,4 +265,65 @@ describe('format_column', () => { const result = await fn(datatable, { columnId: 'test', format: 'number' }); expect(result.columns[1]).toEqual(extraColumn); }); + + it('does support compact format', async () => { + const result = await fn(datatable, { + columnId: 'test', + format: 'number', + compact: true, + }); + expect(result.columns[0].meta).toEqual({ + type: 'number', + params: { + id: 'number', + params: { pattern: '0,0.00a', formatOverride: true }, + }, + }); + }); + + it('does support a Lens custom format', async () => { + const result = await fn(datatable, { + columnId: 'test', + format: 'custom', + pattern: '00:00', + }); + expect(result.columns[0].meta).toEqual({ + type: 'number', + params: { + id: 'number', + params: { pattern: '00:00', formatOverride: true }, + }, + }); + }); + + it('does support both decimals and compact format', async () => { + const result = await fn(datatable, { + columnId: 'test', + format: 'number', + decimals: 5, + compact: true, + }); + expect(result.columns[0].meta).toEqual({ + type: 'number', + params: { + id: 'number', + params: { pattern: '0,0.00000a', formatOverride: true }, + }, + }); + }); + + it("does not apply the custom pattern unless it's a custom format", async () => { + const result = await fn(datatable, { + columnId: 'test', + format: 'number', + pattern: '00:00', + }); + expect(result.columns[0].meta).toEqual({ + type: 'number', + params: { + id: 'number', + params: { pattern: '0,0.00', formatOverride: true }, + }, + }); + }); }); diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts index ce70a4d9f8ff34..84e83aa2a9d18b 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts @@ -21,9 +21,28 @@ function withParams(col: DatatableColumn, params: Record) { return { ...col, meta: { ...col.meta, params } }; } +function getSafeFormatId(format: string) { + return supportedFormats[format].formatId !== 'custom' + ? supportedFormats[format].formatId + : 'number'; +} + +function getPatternFromFormat( + format: string, + decimals: number | undefined, + compact: boolean | undefined, + pattern: string | undefined +) { + const basePattern = supportedFormats[format].decimalsToPattern(decimals, compact); + if (supportedFormats[format].formatId === 'custom') { + return pattern ?? basePattern; + } + return basePattern; +} + export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( input, - { format, columnId, decimals, suffix, parentFormat }: FormatColumnArgs + { format, columnId, decimals, compact, suffix, pattern, parentFormat }: FormatColumnArgs ) => ({ ...input, columns: input.columns @@ -32,9 +51,10 @@ export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( if (!parentFormat) { if (supportedFormats[format]) { const serializedFormat: SerializedFieldFormat = { - id: supportedFormats[format].formatId, + // Lens custom formatter is still a number format, different from the Kibana custom one + id: getSafeFormatId(format), params: { - pattern: supportedFormats[format].decimalsToPattern(decimals), + pattern: getPatternFromFormat(format, decimals, compact, pattern), formatOverride: true, }, }; @@ -58,7 +78,7 @@ export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( if (format && supportedFormats[format]) { const customParams = { - pattern: supportedFormats[format].decimalsToPattern(decimals), + pattern: getPatternFromFormat(format, decimals, compact, pattern), formatOverride: true, }; // Some parent formatters are multi-fields and wrap the custom format into a "paramsPerField" @@ -68,7 +88,7 @@ export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( id: parentFormatId, params: { ...col.meta.params?.params, - id: supportedFormats[format].formatId, + id: getSafeFormatId(format), ...parentFormatParams, // some wrapper formatters require params to be flatten out (i.e. terms) while others // require them to be in the params property (i.e. ranges) @@ -87,7 +107,7 @@ export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( id: parentFormatId, params: { ...col.meta.params?.params, - id: supportedFormats[format].formatId, + id: getSafeFormatId(format), // some wrapper formatters require params to be flatten out (i.e. terms) while others // require them to be in the params property (i.e. ranges) // so for now duplicate diff --git a/x-pack/plugins/lens/common/expressions/format_column/index.ts b/x-pack/plugins/lens/common/expressions/format_column/index.ts index 2a6721ad993b7c..7acbe3237c0e8f 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/index.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/index.ts @@ -12,6 +12,8 @@ export interface FormatColumnArgs { columnId: string; decimals?: number; suffix?: string; + compact?: boolean; + pattern?: string; parentFormat?: string; } @@ -42,6 +44,14 @@ export const formatColumn: FormatColumnExpressionFunction = { types: ['string'], help: '', }, + compact: { + types: ['boolean'], + help: '', + }, + pattern: { + types: ['string'], + help: '', + }, }, inputTypes: ['datatable'], async fn(...args) { diff --git a/x-pack/plugins/lens/common/expressions/format_column/supported_formats.ts b/x-pack/plugins/lens/common/expressions/format_column/supported_formats.ts index f63e5a3df13f64..9c1ef439bad314 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/supported_formats.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/supported_formats.ts @@ -7,29 +7,29 @@ export const supportedFormats: Record< string, - { decimalsToPattern: (decimals?: number) => string; formatId: string } + { decimalsToPattern: (decimals?: number, compact?: boolean) => string; formatId: string } > = { number: { formatId: 'number', - decimalsToPattern: (decimals = 2) => { + decimalsToPattern: (decimals = 2, compact?: boolean) => { if (decimals === 0) { - return `0,0`; + return `0,0${compact ? 'a' : ''}`; } - return `0,0.${'0'.repeat(decimals)}`; + return `0,0.${'0'.repeat(decimals)}${compact ? 'a' : ''}`; }, }, percent: { formatId: 'percent', - decimalsToPattern: (decimals = 2) => { + decimalsToPattern: (decimals = 2, compact?: boolean) => { if (decimals === 0) { - return `0,0%`; + return `0,0${compact ? 'a' : ''}%`; } - return `0,0.${'0'.repeat(decimals)}%`; + return `0,0.${'0'.repeat(decimals)}${compact ? 'a' : ''}%`; }, }, bytes: { formatId: 'bytes', - decimalsToPattern: (decimals = 2) => { + decimalsToPattern: (decimals = 2, compact?: boolean) => { if (decimals === 0) { return `0,0b`; } @@ -37,12 +37,16 @@ export const supportedFormats: Record< }, }, bits: { - formatId: 'number', - decimalsToPattern: (decimals = 2) => { + formatId: 'bytes', + decimalsToPattern: (decimals = 2, compact?: boolean) => { if (decimals === 0) { return `0,0bitd`; } return `0,0.${'0'.repeat(decimals)}bitd`; }, }, + custom: { + formatId: 'custom', + decimalsToPattern: () => '', + }, }; diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx index 688e828a7bbd0a..9d7ff324f62d96 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx @@ -105,7 +105,6 @@ export function DimensionEditor(props: DimensionEditorProps) { isFullscreen, supportStaticValue, enableFormatSelector = true, - formatSelectorOptions, layerType, paramEditorCustomProps, } = props; @@ -1223,11 +1222,7 @@ export function DimensionEditor(props: DimensionEditorProps) { !isFullscreen && selectedColumn && (selectedColumn.dataType === 'number' || selectedColumn.operationType === 'range') ? ( - + ) : null}
diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx index 269d82c6c6e460..234bc93e4798b3 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx @@ -24,7 +24,7 @@ import { FormBasedDimensionEditorComponent, FormBasedDimensionEditorProps, } from './dimension_panel'; -import { mountWithIntl as mount, shallowWithIntl as shallow } from '@kbn/test-jest-helpers'; +import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; import { IUiSettingsClient, HttpSetup, CoreStart, NotificationsStart } from '@kbn/core/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { useExistingFieldsReader } from '@kbn/unified-field-list-plugin/public/hooks/use_existing_fields'; @@ -46,6 +46,9 @@ import { TimeShift } from './time_shift'; import { ReducedTimeRange } from './reduced_time_range'; import { DimensionEditor } from './dimension_editor'; import { AdvancedOptions } from './advanced_options'; +import { coreMock } from '@kbn/core/public/mocks'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { LensAppServices } from '../../../app_plugin/types'; jest.mock('./reference_editor', () => ({ ReferenceEditor: () => null, @@ -154,6 +157,18 @@ const bytesColumn: GenericIndexPatternColumn = { params: { format: { id: 'bytes' } }, }; +const services = coreMock.createStart() as unknown as LensAppServices; + +function mountWithServices(component: React.ReactElement) { + return mount(component, { + // This is an elegant way to wrap a component in Enzyme + // preserving the root at the component level rather than + // at the wrapper one + wrappingComponent: KibanaContextProvider, + wrappingComponentProps: { services }, + }); +} + /** * The datasource exposes four main pieces of code which are tested at * an integration test level. The main reason for this fairly high level @@ -254,7 +269,6 @@ describe('FormBasedDimensionEditor', () => { supportStaticValue: false, toggleFullscreen: jest.fn(), enableFormatSelector: true, - formatSelectorOptions: undefined, }; jest.clearAllMocks(); @@ -271,7 +285,7 @@ describe('FormBasedDimensionEditor', () => { it('should call the filterOperations function', () => { const filterOperations = jest.fn().mockReturnValue(true); - wrapper = shallow( + wrapper = mountWithServices( ); @@ -279,7 +293,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should show field select', () => { - wrapper = mount(); + wrapper = mountWithServices(); expect( wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') @@ -287,7 +301,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should not show field select on fieldless operation', () => { - wrapper = mount( + wrapper = mountWithServices( { }); it('should not show any choices if the filter returns false', () => { - wrapper = mount( + wrapper = mountWithServices( { }); it('should list all field names and document as a whole in prioritized order', () => { - wrapper = mount(); + wrapper = mountWithServices(); const options = wrapper .find(EuiComboBox) @@ -359,7 +373,7 @@ describe('FormBasedDimensionEditor', () => { }; }); - wrapper = mount(); + wrapper = mountWithServices(); const options = wrapper .find(EuiComboBox) @@ -370,7 +384,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should indicate fields which are incompatible for the operation of the current column', () => { - wrapper = mount( + wrapper = mountWithServices( { }); it('should indicate operations which are incompatible for the field of the current column', () => { - wrapper = mount( + wrapper = mountWithServices( { }); it('should indicate when a transition is invalid due to filterOperations', () => { - wrapper = mount( + wrapper = mountWithServices( { }); it('should not display hidden operation types', () => { - wrapper = mount(); + wrapper = mountWithServices(); const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; @@ -452,7 +466,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should indicate that reference-based operations are not compatible when they are incomplete', () => { - wrapper = mount( + wrapper = mountWithServices( { }); it('should indicate that reference-based operations are compatible sometimes', () => { - wrapper = mount( + wrapper = mountWithServices( { it('should keep the operation when switching to another field compatible with this operation', () => { const initialState: FormBasedPrivateState = getStateWithColumns({ col1: bytesColumn }); - wrapper = mount(); + wrapper = mountWithServices( + + ); const comboBox = wrapper .find(EuiComboBox) @@ -571,7 +587,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should switch operations when selecting a field that requires another operation', () => { - wrapper = mount(); + wrapper = mountWithServices(); const comboBox = wrapper .find(EuiComboBox) @@ -605,7 +621,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should keep the field when switching to another operation compatible for this field', () => { - wrapper = mount( + wrapper = mountWithServices( { }); it('should not set the state if selecting the currently active operation', () => { - wrapper = mount(); + wrapper = mountWithServices(); act(() => { wrapper @@ -649,7 +665,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should update label and custom label flag on label input changes', () => { - wrapper = mount(); + wrapper = mountWithServices(); act(() => { wrapper @@ -677,7 +693,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should not keep the label as long as it is the default label', () => { - wrapper = mount( + wrapper = mountWithServices( { }); it('should keep the label on operation change if it is custom', () => { - wrapper = mount( + wrapper = mountWithServices( { }); it('should remove customLabel flag if label is set to default', () => { - wrapper = mount( + wrapper = mountWithServices( { describe('transient invalid state', () => { it('should set the state if selecting an operation incompatible with the current field', () => { - wrapper = mount(); + wrapper = mountWithServices(); act(() => { wrapper @@ -808,11 +824,13 @@ describe('FormBasedDimensionEditor', () => { }); it('should show error message in invalid state', () => { - wrapper = mount(); + wrapper = mountWithServices(); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + }); expect( wrapper.find('[data-test-subj="indexPattern-field-selection-row"]').first().prop('error') @@ -820,29 +838,37 @@ describe('FormBasedDimensionEditor', () => { }); it('should leave error state if a compatible operation is selected', () => { - wrapper = mount(); + wrapper = mountWithServices(); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); + }); expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); }); it('should leave error state if the original operation is re-selected', () => { - wrapper = mount(); + wrapper = mountWithServices(); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); + }); expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); }); @@ -850,19 +876,25 @@ describe('FormBasedDimensionEditor', () => { it('should leave error state when switching from incomplete state to fieldless operation', () => { // @ts-expect-error window['__react-beautiful-dnd-disable-dev-warnings'] = true; // issue with enzyme & react-beautiful-dnd throwing errors: https://github.com/atlassian/react-beautiful-dnd/issues/1593 - wrapper = mount(); + wrapper = mountWithServices(); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + }); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-filters"]').simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-filters"]') + .simulate('click'); + }); expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); }); it('should leave error state when re-selecting the original fieldless function', () => { - wrapper = mount( + wrapper = mountWithServices( { /> ); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + }); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-filters"]').simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-filters"]') + .simulate('click'); + }); expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); }); it('should indicate fields compatible with selected operation', () => { - wrapper = mount(); + wrapper = mountWithServices(); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + }); const options = wrapper .find(EuiComboBox) @@ -910,9 +950,15 @@ describe('FormBasedDimensionEditor', () => { }); it('should select compatible operation if field not compatible with selected operation', () => { - wrapper = mount(); + wrapper = mountWithServices( + + ); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-average"]') + .simulate('click'); + }); expect(setState.mock.calls[0]).toEqual([ expect.any(Function), @@ -970,14 +1016,16 @@ describe('FormBasedDimensionEditor', () => { references: ['ref'], }, }); - wrapper = mount( + wrapper = mountWithServices( ); // Transition to a field operation (incompatible) - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-average incompatible"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-average incompatible"]') + .simulate('click'); + }); // Now check that the dimension gets cleaned up on state update expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); @@ -995,7 +1043,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should select the Records field when count is selected on non-existing column', () => { - wrapper = mount( + wrapper = mountWithServices( { /> ); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + }); const newColumnState = setState.mock.calls[0][0](state).layers.first.columns.col2; expect(newColumnState.operationType).toEqual('count'); @@ -1011,7 +1061,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should indicate document and field compatibility with selected document operation', () => { - wrapper = mount( + wrapper = mountWithServices( { /> ); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + }); const options = wrapper .find(EuiComboBox) @@ -1047,11 +1099,13 @@ describe('FormBasedDimensionEditor', () => { }); it('should set datasource state if compatible field is selected for operation', () => { - wrapper = mount(); + wrapper = mountWithServices(); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + }); const comboBox = wrapper .find(EuiComboBox) @@ -1115,8 +1169,10 @@ describe('FormBasedDimensionEditor', () => { } it('should default to None if time scaling is not set', () => { - wrapper = mount(); - findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + wrapper = mountWithServices(); + act(() => { + findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + }); expect(wrapper.find('[data-test-subj="indexPattern-time-scaling-enable"]')).toHaveLength(1); expect( wrapper @@ -1127,8 +1183,12 @@ describe('FormBasedDimensionEditor', () => { }); it('should show current time scaling if set', () => { - wrapper = mount(); - findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + wrapper = mountWithServices( + + ); + act(() => { + findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + }); expect( wrapper .find('[data-test-subj="indexPattern-time-scaling-unit"]') @@ -1139,14 +1199,18 @@ describe('FormBasedDimensionEditor', () => { it('should allow to set time scaling initially', () => { const props = getProps({}); - wrapper = mount(); - findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); - wrapper - .find('[data-test-subj="indexPattern-time-scaling-unit"]') - .find(EuiSelect) - .prop('onChange')!({ - target: { value: 's' }, - } as ChangeEvent); + wrapper = mountWithServices(); + act(() => { + findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + }); + act(() => { + wrapper + .find('[data-test-subj="indexPattern-time-scaling-unit"]') + .find(EuiSelect) + .prop('onChange')!({ + target: { value: 's' }, + } as ChangeEvent); + }); expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, @@ -1172,8 +1236,10 @@ describe('FormBasedDimensionEditor', () => { operationType: 'sum', label: 'Sum of bytes per hour', }); - wrapper = mount(); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + wrapper = mountWithServices(); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + }); expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, @@ -1199,8 +1265,12 @@ describe('FormBasedDimensionEditor', () => { operationType: 'sum', label: 'Sum of bytes per hour', }); - wrapper = mount(); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); + wrapper = mountWithServices(); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-average"]') + .simulate('click'); + }); expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, @@ -1221,11 +1291,17 @@ describe('FormBasedDimensionEditor', () => { it('should allow to change time scaling', () => { const props = getProps({ timeScale: 's', label: 'Count of records per second' }); - wrapper = mount(); - findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + wrapper = mountWithServices(); + act(() => { + findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + }); - wrapper.find('[data-test-subj="indexPattern-time-scaling-unit"] select').simulate('change', { - target: { value: 'h' }, + act(() => { + wrapper + .find('[data-test-subj="indexPattern-time-scaling-unit"] select') + .simulate('change', { + target: { value: 'h' }, + }); }); expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); @@ -1248,9 +1324,13 @@ describe('FormBasedDimensionEditor', () => { it('should not adjust label if it is custom', () => { const props = getProps({ timeScale: 's', customLabel: true, label: 'My label' }); - wrapper = mount(); - wrapper.find('[data-test-subj="indexPattern-time-scaling-unit"] select').simulate('change', { - target: { value: 'h' }, + wrapper = mountWithServices(); + act(() => { + wrapper + .find('[data-test-subj="indexPattern-time-scaling-unit"] select') + .simulate('change', { + target: { value: 'h' }, + }); }); expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ @@ -1314,15 +1394,17 @@ describe('FormBasedDimensionEditor', () => { }), columnId: 'col2', }; - wrapper = mount(); - findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + wrapper = mountWithServices(); + act(() => { + findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + }); expect( wrapper.find('[data-test-subj="indexPattern-dimension-reducedTimeRange-row"]') ).toHaveLength(0); }); it('should show current reduced time range if set', () => { - wrapper = mount( + wrapper = mountWithServices( ); expect( @@ -1332,11 +1414,15 @@ describe('FormBasedDimensionEditor', () => { it('should allow to set reduced time range initially', () => { const props = getProps({}); - wrapper = mount(); - findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); - wrapper.find(ReducedTimeRange).find(EuiComboBox).prop('onChange')!([ - { value: '1h', label: '' }, - ]); + wrapper = mountWithServices(); + act(() => { + findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + }); + act(() => { + wrapper.find(ReducedTimeRange).find(EuiComboBox).prop('onChange')!([ + { value: '1h', label: '' }, + ]); + }); expect((props.setState as jest.Mock).mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1360,8 +1446,10 @@ describe('FormBasedDimensionEditor', () => { operationType: 'sum', label: 'Sum of bytes per hour', }); - wrapper = mount(); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + wrapper = mountWithServices(); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + }); expect((props.setState as jest.Mock).mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1382,7 +1470,7 @@ describe('FormBasedDimensionEditor', () => { const props = getProps({ timeShift: '1d', }); - wrapper = mount(); + wrapper = mountWithServices(); wrapper.find(ReducedTimeRange).find(EuiComboBox).prop('onCreateOption')!('7m', []); expect((props.setState as jest.Mock).mock.calls[0][0](props.state)).toEqual({ ...props.state, @@ -1404,7 +1492,7 @@ describe('FormBasedDimensionEditor', () => { const props = getProps({ reducedTimeRange: '5 months', }); - wrapper = mount(); + wrapper = mountWithServices(); expect(wrapper.find(ReducedTimeRange).find(EuiComboBox).prop('isInvalid')).toBeTruthy(); @@ -1461,7 +1549,7 @@ describe('FormBasedDimensionEditor', () => { }), columnId: 'col2', }; - wrapper = mount( + wrapper = mountWithServices( { }} /> ); - findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + act(() => { + findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + }); expect(wrapper.find('[data-test-subj="indexPattern-time-shift-enable"]')).toHaveLength(1); expect(wrapper.find(TimeShift)).toHaveLength(0); }); it('should show custom options if time shift is available', () => { - wrapper = shallow(); + wrapper = mountWithServices(); expect( wrapper .find(DimensionEditor) - .dive() .find(AdvancedOptions) - .dive() .find('[data-test-subj="indexPattern-time-shift-enable"]') ).toHaveLength(1); }); it('should show current time shift if set', () => { - wrapper = mount(); + wrapper = mountWithServices( + + ); expect(wrapper.find(TimeShift).find(EuiComboBox).prop('selectedOptions')[0].value).toEqual( '1d' ); @@ -1498,9 +1588,13 @@ describe('FormBasedDimensionEditor', () => { it('should allow to set time shift initially', () => { const props = getProps({}); - wrapper = mount(); - findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); - wrapper.find(TimeShift).find(EuiComboBox).prop('onChange')!([{ value: '1h', label: '' }]); + wrapper = mountWithServices(); + act(() => { + findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + }); + act(() => { + wrapper.find(TimeShift).find(EuiComboBox).prop('onChange')!([{ value: '1h', label: '' }]); + }); expect((props.setState as jest.Mock).mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1524,8 +1618,10 @@ describe('FormBasedDimensionEditor', () => { operationType: 'sum', label: 'Sum of bytes per hour', }); - wrapper = mount(); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + wrapper = mountWithServices(); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + }); expect((props.setState as jest.Mock).mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1546,7 +1642,7 @@ describe('FormBasedDimensionEditor', () => { const props = getProps({ timeShift: '1d', }); - wrapper = mount(); + wrapper = mountWithServices(); wrapper.find(TimeShift).find(EuiComboBox).prop('onCreateOption')!('1h', []); expect((props.setState as jest.Mock).mock.calls[0][0](props.state)).toEqual({ ...props.state, @@ -1568,7 +1664,7 @@ describe('FormBasedDimensionEditor', () => { const props = getProps({ timeShift: '5 months', }); - wrapper = mount(); + wrapper = mountWithServices(); expect(wrapper.find(TimeShift).find(EuiComboBox).prop('isInvalid')).toBeTruthy(); @@ -1585,7 +1681,7 @@ describe('FormBasedDimensionEditor', () => { const props = getProps({ timeShift: 'startAt(2022-11-02T00:00:00.000Z)', }); - wrapper = mount(); + wrapper = mountWithServices(); expect(wrapper.find(TimeShift).find(EuiComboBox).prop('isInvalid')).toBeTruthy(); @@ -1629,7 +1725,7 @@ describe('FormBasedDimensionEditor', () => { } it('should not show custom options if time scaling is not available', () => { - wrapper = mount( + wrapper = mountWithServices( { }); it('should show custom options if filtering is available', () => { - wrapper = mount(); - findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + wrapper = mountWithServices(); + act(() => { + findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click'); + }); expect( wrapper.find('[data-test-subj="indexPattern-filter-by-enable"]').hostNodes() ).toHaveLength(1); }); it('should show current filter if set', () => { - wrapper = mount( + wrapper = mountWithServices( @@ -1677,8 +1775,10 @@ describe('FormBasedDimensionEditor', () => { operationType: 'sum', label: 'Sum of bytes per hour', }); - wrapper = mount(); - wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + wrapper = mountWithServices(); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + }); expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, @@ -1701,7 +1801,7 @@ describe('FormBasedDimensionEditor', () => { filter: { language: 'kuery', query: 'a: b' }, }); - wrapper = mount(); + wrapper = mountWithServices(); act(() => { const { updateLayer, columnId, layer } = wrapper.find(Filtering).props(); @@ -1728,7 +1828,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should render invalid field if field reference is broken', () => { - wrapper = mount( + wrapper = mountWithServices( { }); it('should support selecting the operation before the field', () => { - wrapper = mount(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); - + wrapper = mountWithServices( + + ); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); + }); expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: false }]); expect(setState.mock.calls[0][0](defaultProps.state)).toEqual({ ...state, @@ -1805,7 +1907,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should select operation directly if only one field is possible', () => { - wrapper = mount( + wrapper = mountWithServices( { }} /> ); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); - + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); + }); expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](state)).toEqual({ ...state, @@ -1843,10 +1945,12 @@ describe('FormBasedDimensionEditor', () => { }); it('should select operation directly if only document is possible', () => { - wrapper = mount(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); - + wrapper = mountWithServices( + + ); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + }); expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](state)).toEqual({ ...state, @@ -1867,7 +1971,9 @@ describe('FormBasedDimensionEditor', () => { }); it('should indicate compatible fields when selecting the operation first', () => { - wrapper = mount(); + wrapper = mountWithServices( + + ); act(() => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); @@ -1892,7 +1998,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should indicate document compatibility when document operation is selected', () => { - wrapper = mount( + wrapper = mountWithServices( { }); it('should not update when selecting the current field again', () => { - wrapper = mount(); + wrapper = mountWithServices(); const comboBox = wrapper .find(EuiComboBox) @@ -1935,7 +2041,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should show all operations that are not filtered out', () => { - wrapper = mount( + wrapper = mountWithServices( !op.isBucketed && op.dataType === 'number'} @@ -1968,7 +2074,9 @@ describe('FormBasedDimensionEditor', () => { // Prevents field format from being loaded setState.mockImplementation(() => {}); - wrapper = mount(); + wrapper = mountWithServices( + + ); const comboBox = wrapper .find(EuiComboBox) @@ -2006,7 +2114,9 @@ describe('FormBasedDimensionEditor', () => { const initialState: FormBasedPrivateState = getStateWithColumns({ col1: bytesColumn, }); - wrapper = mount(); + wrapper = mountWithServices( + + ); act(() => { wrapper .find('button[data-test-subj="lns-indexPatternDimension-min"]') @@ -2024,7 +2134,7 @@ describe('FormBasedDimensionEditor', () => { }); it('should keep the latest valid dimension when removing the selection in field combobox', () => { - wrapper = mount(); + wrapper = mountWithServices(); act(() => { wrapper @@ -2048,7 +2158,7 @@ describe('FormBasedDimensionEditor', () => { }, }); - wrapper = mount( + wrapper = mountWithServices( ); @@ -2092,7 +2202,7 @@ describe('FormBasedDimensionEditor', () => { }, }, }); - wrapper = mount( + wrapper = mountWithServices( ); @@ -2133,7 +2243,7 @@ describe('FormBasedDimensionEditor', () => { }, }); - wrapper = mount( + wrapper = mountWithServices( ); @@ -2165,13 +2275,15 @@ describe('FormBasedDimensionEditor', () => { it('should hide the top level field selector when switching from non-reference to reference', () => { (generateId as jest.Mock).mockReturnValue(`second`); - wrapper = mount(); + wrapper = mountWithServices(); expect(wrapper.find('ReferenceEditor')).toHaveLength(0); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-differences incompatible"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-differences incompatible"]') + .simulate('click'); + }); expect(wrapper.find('ReferenceEditor')).toHaveLength(1); }); @@ -2188,15 +2300,17 @@ describe('FormBasedDimensionEditor', () => { }, }); - wrapper = mount( + wrapper = mountWithServices( ); expect(wrapper.find('ReferenceEditor')).toHaveLength(1); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-average incompatible"]') - .simulate('click'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-average incompatible"]') + .simulate('click'); + }); expect(wrapper.find('ReferenceEditor')).toHaveLength(0); }); @@ -2212,7 +2326,7 @@ describe('FormBasedDimensionEditor', () => { }, }); - wrapper = mount( + wrapper = mountWithServices( ); @@ -2237,7 +2351,7 @@ describe('FormBasedDimensionEditor', () => { }), }; - wrapper = mount( + wrapper = mountWithServices( { }), }; - wrapper = mount(); + wrapper = mountWithServices( + + ); expect(wrapper.find('[data-test-subj="lens-dimensionTabs"]').exists()).toBeFalsy(); }); @@ -2316,7 +2432,7 @@ describe('FormBasedDimensionEditor', () => { }, }); - wrapper = mount( + wrapper = mountWithServices( ); @@ -2337,7 +2453,7 @@ describe('FormBasedDimensionEditor', () => { }, }); - wrapper = mount( + wrapper = mountWithServices( ); @@ -2356,7 +2472,7 @@ describe('FormBasedDimensionEditor', () => { }, }); - wrapper = mount( + wrapper = mountWithServices( { it('should select the quick function tab by default', () => { const stateWithNoColumn: FormBasedPrivateState = getStateWithColumns({}); - wrapper = mount( + wrapper = mountWithServices( ); @@ -2387,7 +2503,7 @@ describe('FormBasedDimensionEditor', () => { it('should select the static value tab when supported by default', () => { const stateWithNoColumn: FormBasedPrivateState = getStateWithColumns({}); - wrapper = mount( + wrapper = mountWithServices( { }, }); - wrapper = mount( + wrapper = mountWithServices( { const original = jest.requireActual('lodash'); @@ -35,46 +40,75 @@ const getDefaultProps = () => ({ onChange: jest.fn(), selectedColumn: bytesColumn, }); + +function createMockServices(): LensAppServices { + const services = coreMock.createStart(); + services.uiSettings.get.mockImplementation(() => '0.0'); + return { + ...services, + docLinks: { + links: { + indexPatterns: { fieldFormattersNumber: '' }, + }, + }, + } as unknown as LensAppServices; +} + +function mountWithServices(component: React.ReactElement) { + const WrappingComponent: React.FC<{ + children: React.ReactNode; + }> = ({ children }) => { + return ( + + {children} + + ); + }; + return mount({component}); +} describe('FormatSelector', () => { it('updates the format decimals', () => { const props = getDefaultProps(); - const component = shallow(); + const component = mountWithServices(); act(() => { component .find('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .simulate('change', { - currentTarget: { value: '10' }, - }); + .find(EuiFieldNumber) + .prop('onChange')!({ + currentTarget: { value: '10' }, + } as React.ChangeEvent); }); expect(props.onChange).toBeCalledWith({ id: 'bytes', params: { decimals: 10 } }); }); it('updates the format decimals to upper range when input exceeds the range', () => { const props = getDefaultProps(); - const component = shallow(); + const component = mountWithServices(); act(() => { component .find('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .simulate('change', { - currentTarget: { value: '100' }, - }); + .find(EuiFieldNumber) + .prop('onChange')!({ + currentTarget: { value: '100' }, + } as React.ChangeEvent); }); expect(props.onChange).toBeCalledWith({ id: 'bytes', params: { decimals: 15 } }); }); it('updates the format decimals to lower range when input is smaller than range', () => { const props = getDefaultProps(); - const component = shallow(); + const component = mountWithServices(); act(() => { component .find('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .simulate('change', { - currentTarget: { value: '-2' }, - }); + .find(EuiFieldNumber) + .prop('onChange')!({ + currentTarget: { value: '-2' }, + } as React.ChangeEvent); }); expect(props.onChange).toBeCalledWith({ id: 'bytes', params: { decimals: 0 } }); }); it('updates the suffix', async () => { const props = getDefaultProps(); - const component = mount(); + const component = mountWithServices(); await act(async () => { component .find('[data-test-subj="indexPattern-dimension-formatSuffix"]') @@ -86,18 +120,4 @@ describe('FormatSelector', () => { component.update(); expect(props.onChange).toBeCalledWith({ id: 'bytes', params: { suffix: 'GB' } }); }); - describe('options', () => { - it('can disable the extra options', () => { - const props = getDefaultProps(); - const component = mount( - - ); - expect(component.exists('[data-test-subj="indexPattern-dimension-formatDecimals"]')).toBe( - false - ); - expect(component.exists('[data-test-subj="indexPattern-dimension-formatSuffix"]')).toBe( - false - ); - }); - }); }); diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.tsx index fc2c4bfa8ae27f..2656e00edad692 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.tsx @@ -7,32 +7,59 @@ import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiComboBox, EuiSpacer, EuiRange, EuiFieldText } from '@elastic/eui'; +import { + EuiFormRow, + EuiComboBox, + EuiSpacer, + EuiRange, + EuiFieldText, + EuiSwitch, + EuiCode, +} from '@elastic/eui'; import { useDebouncedValue } from '@kbn/visualization-ui-components/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { LensAppServices } from '../../../app_plugin/types'; import { GenericIndexPatternColumn } from '../form_based'; import { isColumnFormatted } from '../operations/definitions/helpers'; +import { ValueFormatConfig } from '../operations/definitions/column_types'; -const supportedFormats: Record = { +const supportedFormats: Record< + string, + { title: string; defaultDecimals?: number; supportsCompact: boolean } +> = { number: { title: i18n.translate('xpack.lens.indexPattern.numberFormatLabel', { defaultMessage: 'Number', }), + supportsCompact: true, }, percent: { title: i18n.translate('xpack.lens.indexPattern.percentFormatLabel', { defaultMessage: 'Percent', }), + supportsCompact: true, }, bytes: { title: i18n.translate('xpack.lens.indexPattern.bytesFormatLabel', { defaultMessage: 'Bytes (1024)', }), + supportsCompact: false, }, bits: { title: i18n.translate('xpack.lens.indexPattern.bitsFormatLabel', { defaultMessage: 'Bits (1000)', }), defaultDecimals: 0, + supportsCompact: false, + }, + custom: { + title: i18n.translate('xpack.lens.indexPattern.customFormatLabel', { + defaultMessage: 'Custom format', + }), + defaultDecimals: 0, + supportsCompact: false, }, }; @@ -57,29 +84,29 @@ const suffixLabel = i18n.translate('xpack.lens.indexPattern.suffixLabel', { defaultMessage: 'Suffix', }); -export interface FormatSelectorOptions { - disableExtraOptions?: boolean; -} +const compactLabel = i18n.translate('xpack.lens.indexPattern.compactLabel', { + defaultMessage: 'Compact values', +}); + +type FormatParams = NonNullable; +type FormatParamsKeys = keyof FormatParams; interface FormatSelectorProps { selectedColumn: GenericIndexPatternColumn; - onChange: (newFormat?: { id: string; params?: Record }) => void; - options?: FormatSelectorOptions; + onChange: (newFormat?: { id: string; params?: FormatParams }) => void; } const RANGE_MIN = 0; const RANGE_MAX = 15; -export function FormatSelector(props: FormatSelectorProps) { - const { selectedColumn, onChange } = props; - const currentFormat = isColumnFormatted(selectedColumn) - ? selectedColumn.params?.format - : undefined; - - const [decimals, setDecimals] = useState(currentFormat?.params?.decimals ?? 2); - - const onChangeSuffix = useCallback( - (suffix: string) => { +function useDebouncedInputforParam( + name: T, + defaultValue: FormatParams[T], + currentFormat: ValueFormatConfig | undefined, + onChange: FormatSelectorProps['onChange'] +) { + const onChangeParam = useCallback( + (value: FormatParams[T]) => { if (!currentFormat) { return; } @@ -87,20 +114,55 @@ export function FormatSelector(props: FormatSelectorProps) { id: currentFormat.id, params: { ...currentFormat.params, - suffix, - }, + [name]: value, + } as FormatParams, }); }, - [currentFormat, onChange] + [currentFormat, name, onChange] ); - const { handleInputChange: setSuffix, inputValue: suffix } = useDebouncedValue( + const { handleInputChange: setter, inputValue: value } = useDebouncedValue( { - onChange: onChangeSuffix, - value: currentFormat?.params?.suffix ?? '', + onChange: onChangeParam, + value: currentFormat?.params?.[name] || defaultValue, }, { allowFalsyValue: true } ); + return { setter, value }; +} + +export function FormatSelector(props: FormatSelectorProps) { + const { uiSettings } = useKibana().services; + const { selectedColumn, onChange } = props; + const currentFormat = isColumnFormatted(selectedColumn) + ? selectedColumn.params?.format + : undefined; + + const [decimals, setDecimals] = useState(currentFormat?.params?.decimals ?? 2); + + const { setter: setSuffix, value: suffix } = useDebouncedInputforParam( + 'suffix' as const, + '', + currentFormat, + onChange + ); + + const { setter: setCompact, value: compact } = useDebouncedInputforParam( + 'compact' as const, + false, + currentFormat, + onChange + ); + + const defaultNumeralPatternInKibana = uiSettings.get( + FORMATS_UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN + ); + const { setter: setPattern, value: pattern } = useDebouncedInputforParam( + 'pattern' as const, + defaultNumeralPatternInKibana, + currentFormat, + onChange + ); const selectedFormat = currentFormat?.id ? supportedFormats[currentFormat.id] : undefined; const stableOptions = useMemo( @@ -150,7 +212,22 @@ export function FormatSelector(props: FormatSelectorProps) { return ( <> - + {defaultNumeralPatternInKibana}, + }} + /> + ) : null + } + >
- {currentFormat && !props.options?.disableExtraOptions ? ( + {currentFormat && currentFormat.id !== 'custom' ? ( <> ) : null} + {selectedFormat?.supportsCompact ? ( + <> + + setCompact(!compact)} + data-test-subj="lns-indexpattern-dimension-formatCompact" + /> + + ) : null}
+ {currentFormat?.id === 'custom' ? ( + + { + setPattern(e.target.value); + }} + /> + + ) : null} ); } diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/column_types.ts index a0931a23f4e5ad..e10955b3c54ba1 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/column_types.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/column_types.ts @@ -25,6 +25,8 @@ export interface ValueFormatConfig { params?: { decimals: number; suffix?: string; + compact?: boolean; + pattern?: string; }; } diff --git a/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts b/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts index 5581494846fc22..17b934d3d2d877 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts @@ -337,6 +337,14 @@ function getExpressionForLayer( format?.params && 'suffix' in format.params && format.params.suffix ? [format.params.suffix] : [], + compact: + format?.params && 'compact' in format.params && format.params.compact + ? [format.params.compact] + : [], + pattern: + format?.params && 'pattern' in format.params && format.params.pattern + ? [format.params.pattern] + : [], parentFormat: parentFormat ? [JSON.stringify(parentFormat)] : [], }, }; diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx index 1ac8da1d241f6e..dab66cac44fa57 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx @@ -331,7 +331,7 @@ export function getTextBasedDatasource({ }; }, - toExpression: (state, layerId, indexPatterns) => { + toExpression: (state, layerId, indexPatterns, dateRange, searchSessionId) => { return toExpression(state, layerId); }, getSelectedFields(state) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index cad30547f71ac6..a7314f2c1b6b45 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -820,7 +820,6 @@ export function LayerPanel( supportStaticValue: Boolean(activeGroup.supportStaticValue), paramEditorCustomProps: activeGroup.paramEditorCustomProps, enableFormatSelector: activeGroup.enableFormatSelector !== false, - formatSelectorOptions: activeGroup.formatSelectorOptions, layerType: activeVisualization.getLayerType(layerId, visualizationState), indexPatterns: dataViews.indexPatterns, activeData: layerVisualizationConfigProps.activeData, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index 99a61cd99cb305..4f3dc224cb4ec1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -7,7 +7,7 @@ import { Ast, fromExpression } from '@kbn/interpreter'; import type { DateRange } from '../../../common/types'; import { DatasourceStates } from '../../state_management'; -import { Visualization, DatasourceMap, DatasourceLayers, IndexPatternMap } from '../../types'; +import type { Visualization, DatasourceMap, DatasourceLayers, IndexPatternMap } from '../../types'; export function getDatasourceExpressionsByLayers( datasourceMap: DatasourceMap, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index d6575e3940490d..2555f42a6fb51b 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -62,7 +62,6 @@ import { LENS_EDIT_PAGESIZE_ACTION, } from './visualizations/datatable/components/constants'; import type { LensInspector } from './lens_inspector_service'; -import type { FormatSelectorOptions } from './datasources/form_based/dimension_panel/format_selector'; import type { DataViewsState } from './state_management/types'; import type { IndexPatternServiceAPI } from './data_views_service/service'; import type { Document } from './persistence/saved_object_store'; @@ -669,7 +668,6 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro paramEditorCustomProps?: ParamEditorCustomProps; enableFormatSelector: boolean; dataSectionExtra?: React.ReactNode; - formatSelectorOptions: FormatSelectorOptions | undefined; }; export type DatasourceDimensionTriggerProps = DatasourceDimensionProps; @@ -845,7 +843,6 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { isMetricDimension?: boolean; paramEditorCustomProps?: ParamEditorCustomProps; enableFormatSelector?: boolean; - formatSelectorOptions?: FormatSelectorOptions; // only relevant if supportFieldFormat is true labels?: { buttonAriaLabel: string; buttonLabel: string }; }; diff --git a/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap b/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap index 904f33f1113643..8ad5a53ee57b26 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap +++ b/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap @@ -15,9 +15,6 @@ Object { "enableDimensionEditor": true, "enableFormatSelector": true, "filterOperations": [Function], - "formatSelectorOptions": Object { - "disableExtraOptions": true, - }, "groupId": "metric", "groupLabel": "Primary metric", "isMetricDimension": true, @@ -37,9 +34,6 @@ Object { "enableDimensionEditor": true, "enableFormatSelector": true, "filterOperations": [Function], - "formatSelectorOptions": Object { - "disableExtraOptions": true, - }, "groupId": "secondaryMetric", "groupLabel": "Secondary metric", "isMetricDimension": true, @@ -58,9 +52,6 @@ Object { "enableDimensionEditor": true, "enableFormatSelector": false, "filterOperations": [Function], - "formatSelectorOptions": Object { - "disableExtraOptions": true, - }, "groupId": "max", "groupLabel": "Maximum value", "groupTooltip": "If the maximum value is specified, the minimum value is fixed at zero.", @@ -82,9 +73,6 @@ Object { "enableDimensionEditor": true, "enableFormatSelector": true, "filterOperations": [Function], - "formatSelectorOptions": Object { - "disableExtraOptions": true, - }, "groupId": "breakdownBy", "groupLabel": "Break down by", "supportsMoreColumns": false, diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx index b0666a99df7ca0..4f18899a118dfb 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx @@ -33,7 +33,6 @@ import { GROUP_ID, LENS_METRIC_ID } from './constants'; import { DimensionEditor, DimensionEditorAdditionalSection } from './dimension_editor'; import { Toolbar } from './toolbar'; import { generateId } from '../../id_generator'; -import { FormatSelectorOptions } from '../../datasources/form_based/dimension_panel/format_selector'; import { toExpression } from './to_expression'; import { nonNullable } from '../../utils'; @@ -116,10 +115,6 @@ const getMetricLayerConfiguration = ( const isBucketed = (op: OperationMetadata) => op.isBucketed; - const formatterOptions: FormatSelectorOptions = { - disableExtraOptions: true, - }; - return { groups: [ { @@ -146,7 +141,6 @@ const getMetricLayerConfiguration = ( isMetricDimension: true, enableDimensionEditor: true, enableFormatSelector: true, - formatSelectorOptions: formatterOptions, requiredMinDimensionCount: 1, }, { @@ -172,7 +166,6 @@ const getMetricLayerConfiguration = ( isMetricDimension: true, enableDimensionEditor: true, enableFormatSelector: true, - formatSelectorOptions: formatterOptions, }, { groupId: GROUP_ID.MAX, @@ -194,7 +187,6 @@ const getMetricLayerConfiguration = ( filterOperations: isSupportedMetric, enableDimensionEditor: true, enableFormatSelector: false, - formatSelectorOptions: formatterOptions, supportStaticValue: true, prioritizedOperation: 'max', groupTooltip: i18n.translate('xpack.lens.metric.maxTooltip', { @@ -219,7 +211,6 @@ const getMetricLayerConfiguration = ( filterOperations: isBucketed, enableDimensionEditor: true, enableFormatSelector: true, - formatSelectorOptions: formatterOptions, }, ], }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 1f1d4f052b4e81..60078bc0dbb9c2 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2752,7 +2752,6 @@ "expressionMetric.functions.metricHelpText": "Affiche un nombre sur une étiquette.", "expressionMetric.renderer.metric.displayName": "Indicateur", "expressionMetric.renderer.metric.helpDescription": "Présenter un nombre sur une étiquette", - "expressionMetricVis.errors.unsupportedColumnFormat": "Expression de visualisation de l'indicateur – Format de colonne non pris en charge : \"{id}\"", "expressionMetricVis.trendA11yTitle": "{dataTitle} sur la durée.", "expressionMetricVis.function.breakdownBy.help": "La dimension contenant les étiquettes des sous-catégories.", "expressionMetricVis.function.color.help": "Fournit une couleur de visualisation statique. Remplacé par la palette.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8148e0ecb75ad8..3013be90cb78f1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2752,7 +2752,6 @@ "expressionMetric.functions.metricHelpText": "ラベルの上に数字を表示します。", "expressionMetric.renderer.metric.displayName": "メトリック", "expressionMetric.renderer.metric.helpDescription": "ラベルの上に数字をレンダリングします", - "expressionMetricVis.errors.unsupportedColumnFormat": "メトリック視覚化式 - サポートされていない列形式:\"{id}\"", "expressionMetricVis.trendA11yTitle": "一定時間の{dataTitle}。", "expressionMetricVis.function.breakdownBy.help": "サブカテゴリのラベルを含むディメンション。", "expressionMetricVis.function.color.help": "静的ビジュアライゼーション色を提供します。パレットで上書きされます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index edef2f0c2b2b3d..2075349d0f1eae 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2751,7 +2751,6 @@ "expressionMetric.functions.metricHelpText": "在标签上显示数字。", "expressionMetric.renderer.metric.displayName": "指标", "expressionMetric.renderer.metric.helpDescription": "在标签上呈现数字", - "expressionMetricVis.errors.unsupportedColumnFormat": "指标可视化表达式 - 不支持的列格式:“{id}”", "expressionMetricVis.trendA11yTitle": "时移 {dataTitle}。", "expressionMetricVis.function.breakdownBy.help": "包含子类别标签的维度。", "expressionMetricVis.function.color.help": "提供静态可视化颜色。已由调色板覆盖。", diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index e01b9939ecbf2f..a79b9e2e07a266 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -388,8 +388,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { [ { metric: 'hostsCount', value: '6' }, - { metric: 'cpu', value: '0.8%' }, - { metric: 'memory', value: '16.81%' }, + { metric: 'cpu', value: '1%' }, + { metric: 'memory', value: '17%' }, { metric: 'tx', value: 'N/A' }, { metric: 'rx', value: 'N/A' }, ].forEach(({ metric, value }) => { @@ -540,8 +540,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await Promise.all( [ { metric: 'hostsCount', value: '3' }, - { metric: 'cpu', value: '0.8%' }, - { metric: 'memory', value: '16.25%' }, + { metric: 'cpu', value: '1%' }, + { metric: 'memory', value: '16%' }, { metric: 'tx', value: 'N/A' }, { metric: 'rx', value: 'N/A' }, ].map(async ({ metric, value }) => { diff --git a/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts b/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts index 5b8bd835f935e2..cfe9cc7e5f3045 100644 --- a/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts +++ b/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts @@ -139,12 +139,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.waitForVisualization('mtrVis'); const metricData = await PageObjects.lens.getMetricVisualizationData(); - expect(metricData[0].value).to.eql('5.73K'); + expect(metricData[0].value).to.eql('5,727.322'); expect(metricData[0].title).to.eql('Average of bytes'); await PageObjects.lens.save('New Lens from Modal', false, false, false, 'new'); await PageObjects.dashboard.waitForRenderComplete(); - expect(metricData[0].value).to.eql('5.73K'); + expect(metricData[0].value).to.eql('5,727.322'); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.eql(1); @@ -166,7 +166,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.save('Lens with adhoc data view'); await PageObjects.lens.waitForVisualization('mtrVis'); const metricData = await PageObjects.lens.getMetricVisualizationData(); - expect(metricData[0].value).to.eql('5.73K'); + expect(metricData[0].value).to.eql('5,727.322'); expect(metricData[0].title).to.eql('Average of bytes'); }); diff --git a/x-pack/test/functional/apps/lens/group2/text_based_languages.ts b/x-pack/test/functional/apps/lens/group2/text_based_languages.ts index 27f4b8a03001e6..506f9137834d40 100644 --- a/x-pack/test/functional/apps/lens/group2/text_based_languages.ts +++ b/x-pack/test/functional/apps/lens/group2/text_based_languages.ts @@ -130,12 +130,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.removeDimension('lnsMetric_breakdownByDimensionPanel'); await PageObjects.lens.waitForVisualization('mtrVis'); const metricData = await PageObjects.lens.getMetricVisualizationData(); - expect(metricData[0].value).to.eql('5.7K'); + expect(metricData[0].value).to.eql('5,699.406'); expect(metricData[0].title).to.eql('average'); await PageObjects.lens.save('New text based languages viz', false, false, false, 'new'); await PageObjects.dashboard.waitForRenderComplete(); - expect(metricData[0].value).to.eql('5.7K'); + expect(metricData[0].value).to.eql('5,699.406'); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.eql(1); diff --git a/x-pack/test/functional/apps/lens/group6/metric.ts b/x-pack/test/functional/apps/lens/group6/metric.ts index f5ef312c39d2dd..ff2b075312c4ce 100644 --- a/x-pack/test/functional/apps/lens/group6/metric.ts +++ b/x-pack/test/functional/apps/lens/group6/metric.ts @@ -127,13 +127,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await PageObjects.lens.waitForVisualization('mtrVis'); + const data = await PageObjects.lens.getMetricVisualizationData(); - expect(await PageObjects.lens.getMetricVisualizationData()).to.eql([ + const expectedData = [ { title: '97.220.3.248', subtitle: 'Average of bytes', - extraText: 'Average of bytes 19.76K', - value: '19.76K', + extraText: 'Average of bytes 19,755', + value: '19,755', color: 'rgba(245, 247, 250, 1)', showingTrendline: false, showingBar: false, @@ -141,8 +142,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { { title: '169.228.188.120', subtitle: 'Average of bytes', - extraText: 'Average of bytes 18.99K', - value: '18.99K', + extraText: 'Average of bytes 18,994', + value: '18,994', color: 'rgba(245, 247, 250, 1)', showingTrendline: false, showingBar: false, @@ -150,8 +151,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { { title: '78.83.247.30', subtitle: 'Average of bytes', - extraText: 'Average of bytes 17.25K', - value: '17.25K', + extraText: 'Average of bytes 17,246', + value: '17,246', color: 'rgba(245, 247, 250, 1)', showingTrendline: false, showingBar: false, @@ -159,8 +160,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { { title: '226.82.228.233', subtitle: 'Average of bytes', - extraText: 'Average of bytes 15.69K', - value: '15.69K', + extraText: 'Average of bytes 15,687', + value: '15,687', color: 'rgba(245, 247, 250, 1)', showingTrendline: false, showingBar: false, @@ -168,8 +169,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { { title: '93.28.27.24', subtitle: 'Average of bytes', - extraText: 'Average of bytes 15.61K', - value: '15.61K', + extraText: 'Average of bytes 15,614.333', + value: '15,614.333', color: 'rgba(245, 247, 250, 1)', showingTrendline: false, showingBar: false, @@ -177,13 +178,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { { title: 'Other', subtitle: 'Average of bytes', - extraText: 'Average of bytes 5.72K', - value: '5.72K', + extraText: 'Average of bytes 5,722.775', + value: '5,722.775', color: 'rgba(245, 247, 250, 1)', showingTrendline: false, showingBar: false, }, - ]); + ]; + expect(data).to.eql(expectedData); }); it('should enable bar with max dimension', async () => { @@ -342,5 +344,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.waitForVisualization('mtrVis'); }); + + it('does carry custom formatting when transitioning from other visualization', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.switchToVisualization('lnsLegacyMetric'); + // await PageObjects.lens.clickLegacyMetric(); + await PageObjects.lens.configureDimension({ + dimension: 'lns-empty-dimension', + operation: 'average', + field: 'bytes', + keepOpen: true, + }); + await PageObjects.lens.editDimensionFormat('Number', { decimals: 3, prefix: ' blah' }); + await PageObjects.lens.closeDimensionEditor(); + + await PageObjects.lens.switchToVisualization('lnsMetric', 'Metric'); + await PageObjects.lens.waitForVisualization('mtrVis'); + const [{ value }] = await PageObjects.lens.getMetricVisualizationData(); + expect(value).contain('blah'); + + // Extract the numeric decimals from the value without any compact suffix like k or m + const decimals = (value?.split(`.`)[1] || '').match(/(\d)+/)?.[0]; + expect(decimals).have.length(3); + }); }); } diff --git a/x-pack/test/functional/apps/lens/open_in_lens/agg_based/goal.ts b/x-pack/test/functional/apps/lens/open_in_lens/agg_based/goal.ts index d3dc518ceab065..a83dc421b4a099 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/agg_based/goal.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/agg_based/goal.ts @@ -78,7 +78,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'Average machine.ram', subtitle: undefined, extraText: '', - value: '131.04M%', + value: '131,040,360.81%', color: 'rgba(245, 247, 250, 1)', showingBar: true, showingTrendline: false, @@ -152,7 +152,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'ios', subtitle: 'Average machine.ram', extraText: '', - value: '65.05M%', + value: '65,047,486.03', color: 'rgba(245, 247, 250, 1)', showingBar: true, showingTrendline: false, @@ -161,7 +161,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'osx', subtitle: 'Average machine.ram', extraText: '', - value: '66.14M%', + value: '66,144,823.35', color: 'rgba(245, 247, 250, 1)', showingBar: true, showingTrendline: false, @@ -170,7 +170,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'win 7', subtitle: 'Average machine.ram', extraText: '', - value: '65.93M%', + value: '65,933,477.76', color: 'rgba(245, 247, 250, 1)', showingBar: true, showingTrendline: false, @@ -179,7 +179,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'win 8', subtitle: 'Average machine.ram', extraText: '', - value: '65.16M%', + value: '65,157,898.23', color: 'rgba(245, 247, 250, 1)', showingBar: true, showingTrendline: false, @@ -188,7 +188,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'win xp', subtitle: 'Average machine.ram', extraText: '', - value: '65.37M%', + value: '65,365,950.93', color: 'rgba(245, 247, 250, 1)', showingBar: true, showingTrendline: false, diff --git a/x-pack/test/functional/apps/lens/open_in_lens/agg_based/metric.ts b/x-pack/test/functional/apps/lens/open_in_lens/agg_based/metric.ts index cd26a217dcca1e..62b4c1acd6e161 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/agg_based/metric.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/agg_based/metric.ts @@ -48,7 +48,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'Count', subtitle: undefined, extraText: '', - value: '14.01K', + value: '14,005', color: 'rgba(245, 247, 250, 1)', showingBar: false, showingTrendline: false, @@ -78,7 +78,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'Average machine.ram', subtitle: undefined, extraText: '', - value: '13.1B', + value: '13,104,036,080.615', color: 'rgba(245, 247, 250, 1)', showingBar: false, showingTrendline: false, @@ -108,7 +108,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'Overall Max of Count', subtitle: undefined, extraText: '', - value: '1.44K', + value: '1,437', color: 'rgba(245, 247, 250, 1)', showingBar: false, showingTrendline: false, @@ -162,7 +162,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'osx', subtitle: 'Average machine.ram', extraText: '', - value: '13.23B', + value: '13,228,964,670.613', color: 'rgba(245, 247, 250, 1)', showingBar: false, showingTrendline: false, @@ -171,7 +171,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'win 7', subtitle: 'Average machine.ram', extraText: '', - value: '13.19B', + value: '13,186,695,551.251', color: 'rgba(245, 247, 250, 1)', showingBar: false, showingTrendline: false, @@ -180,7 +180,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'win xp', subtitle: 'Average machine.ram', extraText: '', - value: '13.07B', + value: '13,073,190,186.423', color: 'rgba(245, 247, 250, 1)', showingBar: false, showingTrendline: false, @@ -189,7 +189,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'win 8', subtitle: 'Average machine.ram', extraText: '', - value: '13.03B', + value: '13,031,579,645.108', color: 'rgba(245, 247, 250, 1)', showingBar: false, showingTrendline: false, @@ -198,7 +198,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { title: 'ios', subtitle: 'Average machine.ram', extraText: '', - value: '13.01B', + value: '13,009,497,206.823', color: 'rgba(245, 247, 250, 1)', showingBar: false, showingTrendline: false, diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 00ead1e0111f6c..7bbcc5dc73321a 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -775,8 +775,16 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont async editDimensionLabel(label: string) { await testSubjects.setValue('column-label-edit', label, { clearWithKeyboard: true }); }, - async editDimensionFormat(format: string) { + async editDimensionFormat(format: string, options?: { decimals?: number; prefix?: string }) { await this.selectOptionFromComboBox('indexPattern-dimension-format', format); + if (options?.decimals != null) { + await testSubjects.setValue('indexPattern-dimension-formatDecimals', `${options.decimals}`); + // press tab key to remove the range popover of the EUI field + await PageObjects.common.pressTabKey(); + } + if (options?.prefix != null) { + await testSubjects.setValue('indexPattern-dimension-formatSuffix', options.prefix); + } }, async editDimensionColor(color: string) { const colorPickerInput = await testSubjects.find('~indexPattern-dimension-colorPicker');