diff --git a/docs/apm/advanced-queries.asciidoc b/docs/apm/advanced-queries.asciidoc index 971d543bbb445..ed77ebb4c4930 100644 --- a/docs/apm/advanced-queries.asciidoc +++ b/docs/apm/advanced-queries.asciidoc @@ -21,22 +21,22 @@ TIP: Read the {kibana-ref}/kuery-query.html[Kibana Query Language Enhancements] [float] [[discover-advanced-queries]] -=== Querying in the Discover app +=== Querying in Discover -It may also be helpful to view your APM data in the {kibana-ref}/discover.html[Discover app]. -Querying documents in Discover works the same way as querying in the APM app, -and all of the example APM app queries can also be used in the Discover app. +It may also be helpful to view your APM data in {kibana-ref}/discover.html[*Discover*]. +Querying documents in *Discover* works the same way as querying in the APM app, +and all of the example APM app queries can also be used in *Discover*. [float] -==== Example Discover app query +==== Example Discover query -One example where you may want to make use of the Discover app, +One example where you may want to make use of *Discover*, is for viewing _all_ transactions for an endpoint, instead of just a sample. TIP: Starting in v7.6, you can view 10 samples per bucket in the APM app, instead of just one. Use the APM app to find a transaction name and time bucket that you're interested in learning more about. -Then, switch to the Discover app and make a search: +Then, switch to *Discover* and make a search: ["source","sh"] ----- diff --git a/docs/getting-started/tutorial-discovering.asciidoc b/docs/getting-started/tutorial-discovering.asciidoc index 48e5bed6a4ba7..bbffb2187f0cf 100644 --- a/docs/getting-started/tutorial-discovering.asciidoc +++ b/docs/getting-started/tutorial-discovering.asciidoc @@ -1,7 +1,7 @@ [[tutorial-discovering]] === Discover your data -Using the Discover application, you can enter +Using *Discover*, you can enter an {ref}/query-dsl-query-string-query.html#query-string-syntax[Elasticsearch query] to search your data and filter the results. @@ -12,7 +12,7 @@ You might need to click *New* in the menu bar to refresh the data. . Click the caret to the right of the current index pattern, and select `ba*`. + -By default, all fields are shown for each matching document. +By default, all fields are shown for each matching document. . In the search field, enter the following string: + diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 424e5ab0bf4d5..9187e207ed0d6 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -69,7 +69,6 @@ export { isStringType, isType, isValidInterval, - isValidJson, METRIC_TYPES, OptionedParamType, parentPipelineType, diff --git a/src/legacy/core_plugins/data/public/search/aggs/index.ts b/src/legacy/core_plugins/data/public/search/aggs/index.ts index 8d6fbeacd606a..75d632a0f931f 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/index.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/index.ts @@ -53,7 +53,7 @@ export { toAbsoluteDates } from './buckets/lib/date_utils'; export { convertIPRangeToString } from './buckets/ip_range'; export { aggTypeFilters, propFilter } from './filter'; export { OptionedParamType } from './param_types/optioned'; -export { isValidJson, isValidInterval } from './utils'; +export { isValidInterval } from './utils'; export { BUCKET_TYPES } from './buckets/bucket_agg_types'; export { METRIC_TYPES } from './metrics/metric_agg_types'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/utils.test.tsx b/src/legacy/core_plugins/data/public/search/aggs/utils.test.tsx deleted file mode 100644 index c0662c98755a3..0000000000000 --- a/src/legacy/core_plugins/data/public/search/aggs/utils.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { isValidJson } from './utils'; - -const input = { - valid: '{ "test": "json input" }', - invalid: 'strings are not json', -}; - -describe('AggType utils', () => { - describe('isValidJson', () => { - it('should return true when empty string', () => { - expect(isValidJson('')).toBeTruthy(); - }); - - it('should return true when undefine', () => { - expect(isValidJson(undefined as any)).toBeTruthy(); - }); - - it('should return false when invalid string', () => { - expect(isValidJson(input.invalid)).toBeFalsy(); - }); - - it('should return true when valid string', () => { - expect(isValidJson(input.valid)).toBeTruthy(); - }); - - it('should return false if a number', () => { - expect(isValidJson('0')).toBeFalsy(); - }); - }); -}); diff --git a/src/legacy/core_plugins/data/public/search/aggs/utils.ts b/src/legacy/core_plugins/data/public/search/aggs/utils.ts index 67ea373f438fb..9fcd3f7930b06 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/utils.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/utils.ts @@ -20,35 +20,6 @@ import { leastCommonInterval } from 'ui/vis/lib/least_common_interval'; import { isValidEsInterval } from '../../../common'; -/** - * Check a string if it's a valid JSON. - * - * @param {string} value a string that should be validated - * @returns {boolean} true if value is a valid JSON or if value is an empty string, or a string with whitespaces, otherwise false - */ -export function isValidJson(value: string): boolean { - if (!value || value.length === 0) { - return true; - } - - const trimmedValue = value.trim(); - - if (trimmedValue.length === 0) { - return true; - } - - if (trimmedValue[0] === '{' || trimmedValue[0] === '[') { - try { - JSON.parse(trimmedValue); - return true; - } catch (e) { - return false; - } - } else { - return false; - } -} - export function isValidInterval(value: string, baseInterval?: string) { if (baseInterval) { return _parseWithBase(value, baseInterval); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/raw_json.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/raw_json.tsx index 32939c420155f..b6a6da09fb20e 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/raw_json.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/raw_json.tsx @@ -17,65 +17,91 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; -import { EuiFormRow, EuiIconTip, EuiTextArea } from '@elastic/eui'; +import { EuiFormRow, EuiIconTip, EuiCodeEditor, EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { isValidJson } from '../../legacy_imports'; import { AggParamEditorProps } from '../agg_param_props'; function RawJsonParamEditor({ - agg, showValidation, value = '', setValidity, setValue, setTouched, }: AggParamEditorProps) { - const label = ( - <> - {' '} - - + const [isFieldValid, setFieldValidity] = useState(true); + const [editorReady, setEditorReady] = useState(false); + + const editorTooltipText = useMemo( + () => + i18n.translate('visDefaultEditor.controls.jsonInputTooltip', { + defaultMessage: + "Any JSON formatted properties you add here will be merged with the elasticsearch aggregation definition for this section. For example 'shard_size' on a terms aggregation.", + }), + [] ); - const isValid = isValidJson(value); - const onChange = (ev: React.ChangeEvent) => { - const textValue = ev.target.value; - setValue(textValue); - setValidity(isValidJson(textValue)); - }; + const jsonEditorLabelText = useMemo( + () => + i18n.translate('visDefaultEditor.controls.jsonInputLabel', { + defaultMessage: 'JSON input', + }), + [] + ); - useEffect(() => { - setValidity(isValid); - }, [isValid]); + const label = useMemo( + () => ( + <> + {jsonEditorLabelText}{' '} + + + ), + [jsonEditorLabelText, editorTooltipText] + ); + + const onEditorValidate = useCallback( + (annotations: unknown[]) => { + // The first onValidate returned from EuiCodeEditor is a false negative + if (editorReady) { + const validity = annotations.length === 0; + setFieldValidity(validity); + setValidity(validity); + } else { + setEditorReady(true); + } + }, + [setValidity, editorReady] + ); return ( - + <> + + +

{editorTooltipText}

+
+
); } diff --git a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts index 5c02b50286a95..33a5c0fe660c4 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts @@ -42,7 +42,7 @@ export { parentPipelineType } from 'ui/agg_types'; export { siblingPipelineType } from 'ui/agg_types'; export { isType, isStringType } from 'ui/agg_types'; export { OptionedValueProp, OptionedParamEditorProps, OptionedParamType } from 'ui/agg_types'; -export { isValidJson, isValidInterval } from 'ui/agg_types'; +export { isValidInterval } from 'ui/agg_types'; export { AggParamOption } from 'ui/agg_types'; export { CidrMask } from 'ui/agg_types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap index 037989a86af01..2b7c03084ec65 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap @@ -52,59 +52,16 @@ exports[`CategoryAxisPanel component should init with the default set of props 1 value={true} /> `; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap index 56f35ae021173..e9cd2b737b879 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap @@ -31,22 +31,6 @@ exports[`ChartOptions component should init with the default set of props 1`] = diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap index f589a69eecbc3..0b673a819f666 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap @@ -89,7 +89,6 @@ exports[`ValueAxesPanel component should init with the default set of props 1`] size="m" /> diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx index 69622bb3666a6..91cdcd0f456b1 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx @@ -21,14 +21,12 @@ import React from 'react'; import { shallow } from 'enzyme'; import { CategoryAxisPanel, CategoryAxisPanelProps } from './category_axis_panel'; import { Axis } from '../../../types'; -import { Positions, getPositions } from '../../../utils/collections'; +import { Positions } from '../../../utils/collections'; import { LabelOptions } from './label_options'; -import { categoryAxis } from './mocks'; +import { categoryAxis, vis } from './mocks'; jest.mock('ui/new_platform'); -const positions = getPositions(); - describe('CategoryAxisPanel component', () => { let setCategoryAxis: jest.Mock; let onPositionChanged: jest.Mock; @@ -42,16 +40,10 @@ describe('CategoryAxisPanel component', () => { defaultProps = { axis, - vis: { - type: { - editorConfig: { - collections: { positions }, - }, - }, - }, + vis, onPositionChanged, setCategoryAxis, - } as any; + }; }); it('should init with the default set of props', () => { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx index c1da70f5c17c2..049df0cdd77be 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx @@ -23,21 +23,25 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; -import { BasicVislibParams, Axis } from '../../../types'; +import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; +import { Axis } from '../../../types'; import { SelectOption, SwitchOption } from '../../common'; -import { LabelOptions } from './label_options'; +import { LabelOptions, SetAxisLabel } from './label_options'; import { Positions } from '../../../utils/collections'; -export interface CategoryAxisPanelProps extends VisOptionsProps { +export interface CategoryAxisPanelProps { axis: Axis; onPositionChanged: (position: Positions) => void; setCategoryAxis: (value: Axis) => void; + vis: VisOptionsProps['vis']; } -function CategoryAxisPanel(props: CategoryAxisPanelProps) { - const { axis, onPositionChanged, vis, setCategoryAxis } = props; - +function CategoryAxisPanel({ + axis, + onPositionChanged, + vis, + setCategoryAxis, +}: CategoryAxisPanelProps) { const setAxis = useCallback( (paramName: T, value: Axis[T]) => { const updatedAxis = { @@ -57,6 +61,17 @@ function CategoryAxisPanel(props: CategoryAxisPanelProps) { [setAxis, onPositionChanged] ); + const setAxisLabel: SetAxisLabel = useCallback( + (paramName, value) => { + const labels = { + ...axis.labels, + [paramName]: value, + }; + setAxis('labels', labels); + }, + [axis.labels, setAxis] + ); + return ( @@ -89,7 +104,13 @@ function CategoryAxisPanel(props: CategoryAxisPanelProps) { setValue={setAxis} /> - {axis.show && } + {axis.show && ( + + )} ); } diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx index 9679728a2a3d1..c913fd4f35713 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx @@ -22,21 +22,11 @@ import { shallow } from 'enzyme'; import { ChartOptions, ChartOptionsParams } from './chart_options'; import { SeriesParam } from '../../../types'; import { LineOptions } from './line_options'; -import { - ChartTypes, - ChartModes, - getInterpolationModes, - getChartTypes, - getChartModes, -} from '../../../utils/collections'; -import { valueAxis, seriesParam } from './mocks'; +import { ChartTypes, ChartModes } from '../../../utils/collections'; +import { valueAxis, seriesParam, vis } from './mocks'; jest.mock('ui/new_platform'); -const interpolationModes = getInterpolationModes(); -const chartTypes = getChartTypes(); -const chartModes = getChartModes(); - describe('ChartOptions component', () => { let setParamByIndex: jest.Mock; let changeValueAxis: jest.Mock; @@ -51,19 +41,11 @@ describe('ChartOptions component', () => { defaultProps = { index: 0, chart, - vis: { - type: { - editorConfig: { - collections: { interpolationModes, chartTypes, chartModes }, - }, - }, - }, - stateParams: { - valueAxes: [valueAxis], - }, + vis, + valueAxes: [valueAxis], setParamByIndex, changeValueAxis, - } as any; + }; }); it('should init with the default set of props', () => { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx index 399028a1128a9..bc12e04e29468 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx @@ -22,8 +22,8 @@ import React, { useMemo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; -import { BasicVislibParams, SeriesParam, ValueAxis } from '../../../types'; +import { Vis } from 'src/legacy/core_plugins/visualizations/public'; +import { SeriesParam, ValueAxis } from '../../../types'; import { ChartTypes } from '../../../utils/collections'; import { SelectOption } from '../../common'; import { LineOptions } from './line_options'; @@ -31,17 +31,19 @@ import { SetParamByIndex, ChangeValueAxis } from './'; export type SetChart = (paramName: T, value: SeriesParam[T]) => void; -export interface ChartOptionsParams extends VisOptionsProps { +export interface ChartOptionsParams { chart: SeriesParam; index: number; changeValueAxis: ChangeValueAxis; setParamByIndex: SetParamByIndex; + valueAxes: ValueAxis[]; + vis: Vis; } function ChartOptions({ chart, index, - stateParams, + valueAxes, vis, changeValueAxis, setParamByIndex, @@ -62,7 +64,7 @@ function ChartOptions({ const valueAxesOptions = useMemo( () => [ - ...stateParams.valueAxes.map(({ id, name }: ValueAxis) => ({ + ...valueAxes.map(({ id, name }: ValueAxis) => ({ text: name, value: id, })), @@ -73,7 +75,7 @@ function ChartOptions({ value: 'new', }, ], - [stateParams.valueAxes] + [valueAxes] ); return ( diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx index a112b9a3db708..a93ee454a7afd 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx @@ -42,7 +42,7 @@ describe('CustomExtentsOptions component', () => { setMultipleValidity = jest.fn(); defaultProps = { - axis: { ...valueAxis }, + axisScale: { ...valueAxis.scale }, setValueAxis, setValueAxisScale, setMultipleValidity, @@ -57,7 +57,7 @@ describe('CustomExtentsOptions component', () => { describe('boundsMargin', () => { it('should set validity as true when value is positive', () => { - defaultProps.axis.scale.boundsMargin = 5; + defaultProps.axisScale.boundsMargin = 5; mount(); expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, true); @@ -66,17 +66,17 @@ describe('CustomExtentsOptions component', () => { it('should set validity as true when value is empty', () => { const comp = mount(); comp.setProps({ - axis: { ...valueAxis, scale: { ...valueAxis.scale, boundsMargin: undefined } }, + axisScale: { ...valueAxis.scale, boundsMargin: undefined }, }); expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, true); }); it('should set validity as false when value is negative', () => { - defaultProps.axis.scale.defaultYExtents = true; + defaultProps.axisScale.defaultYExtents = true; const comp = mount(); comp.setProps({ - axis: { ...valueAxis, scale: { ...valueAxis.scale, boundsMargin: -1 } }, + axisScale: { ...valueAxis.scale, boundsMargin: -1 }, }); expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, false); @@ -91,7 +91,7 @@ describe('CustomExtentsOptions component', () => { }); it('should hide bounds margin input when defaultYExtents is false', () => { - defaultProps.axis.scale = { ...defaultProps.axis.scale, defaultYExtents: false }; + defaultProps.axisScale = { ...defaultProps.axisScale, defaultYExtents: false }; const comp = shallow(); expect(comp.find({ paramName: BOUNDS_MARGIN }).exists()).toBeFalsy(); @@ -102,7 +102,7 @@ describe('CustomExtentsOptions component', () => { comp.find({ paramName: DEFAULT_Y_EXTENTS }).prop('setValue')(DEFAULT_Y_EXTENTS, true); expect(setMultipleValidity).not.toBeCalled(); - expect(setValueAxis).toBeCalledWith(SCALE, defaultProps.axis.scale); + expect(setValueAxis).toBeCalledWith(SCALE, defaultProps.axisScale); }); it('should reset boundsMargin when value is false', () => { @@ -110,7 +110,7 @@ describe('CustomExtentsOptions component', () => { comp.find({ paramName: DEFAULT_Y_EXTENTS }).prop('setValue')(DEFAULT_Y_EXTENTS, false); const newScale = { - ...defaultProps.axis.scale, + ...defaultProps.axisScale, boundsMargin: undefined, defaultYExtents: false, }; @@ -126,7 +126,7 @@ describe('CustomExtentsOptions component', () => { }); it('should hide YExtents when value is false', () => { - defaultProps.axis.scale = { ...defaultProps.axis.scale, setYExtents: false }; + defaultProps.axisScale = { ...defaultProps.axisScale, setYExtents: false }; const comp = shallow(); expect(comp.find(YExtents).exists()).toBeFalsy(); @@ -136,7 +136,7 @@ describe('CustomExtentsOptions component', () => { const comp = shallow(); comp.find({ paramName: SET_Y_EXTENTS }).prop('setValue')(SET_Y_EXTENTS, true); - expect(setValueAxis).toBeCalledWith(SCALE, defaultProps.axis.scale); + expect(setValueAxis).toBeCalledWith(SCALE, defaultProps.axisScale); }); it('should reset min and max when value is false', () => { @@ -144,7 +144,7 @@ describe('CustomExtentsOptions component', () => { comp.find({ paramName: SET_Y_EXTENTS }).prop('setValue')(SET_Y_EXTENTS, false); const newScale = { - ...defaultProps.axis.scale, + ...defaultProps.axisScale, min: undefined, max: undefined, setYExtents: false, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx index e322e2863a186..53b2ffa55a941 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx @@ -26,14 +26,14 @@ import { YExtents } from './y_extents'; import { SetScale } from './value_axis_options'; export interface CustomExtentsOptionsProps { - axis: ValueAxis; + axisScale: ValueAxis['scale']; setMultipleValidity(paramName: string, isValid: boolean): void; setValueAxis(paramName: T, value: ValueAxis[T]): void; setValueAxisScale: SetScale; } function CustomExtentsOptions({ - axis, + axisScale, setMultipleValidity, setValueAxis, setValueAxisScale, @@ -44,7 +44,7 @@ function CustomExtentsOptions({ ); const isBoundsMarginValid = - !axis.scale.defaultYExtents || !axis.scale.boundsMargin || axis.scale.boundsMargin >= 0; + !axisScale.defaultYExtents || !axisScale.boundsMargin || axisScale.boundsMargin >= 0; const setBoundsMargin = useCallback( (paramName: 'boundsMargin', value: number | '') => @@ -54,25 +54,25 @@ function CustomExtentsOptions({ const onDefaultYExtentsChange = useCallback( (paramName: 'defaultYExtents', value: boolean) => { - const scale = { ...axis.scale, [paramName]: value }; + const scale = { ...axisScale, [paramName]: value }; if (!scale.defaultYExtents) { delete scale.boundsMargin; } setValueAxis('scale', scale); }, - [setValueAxis, axis.scale] + [axisScale, setValueAxis] ); const onSetYExtentsChange = useCallback( (paramName: 'setYExtents', value: boolean) => { - const scale = { ...axis.scale, [paramName]: value }; + const scale = { ...axisScale, [paramName]: value }; if (!scale.setYExtents) { delete scale.min; delete scale.max; } setValueAxis('scale', scale); }, - [setValueAxis, axis.scale] + [axisScale, setValueAxis] ); useEffect(() => { @@ -91,11 +91,11 @@ function CustomExtentsOptions({ } )} paramName="defaultYExtents" - value={axis.scale.defaultYExtents} + value={axisScale.defaultYExtents} setValue={onDefaultYExtentsChange} /> - {axis.scale.defaultYExtents && ( + {axisScale.defaultYExtents && ( <> @@ -121,13 +121,13 @@ function CustomExtentsOptions({ defaultMessage: 'Set axis extents', })} paramName="setYExtents" - value={axis.scale.setYExtents} + value={axisScale.setYExtents} setValue={onSetYExtentsChange} /> - {axis.scale.setYExtents && ( + {axisScale.setYExtents && ( diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx index 32c21008c2a3a..82b64e4185ed2 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx @@ -304,7 +304,13 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) return isTabSelected ? ( <> - + ) removeValueAxis={removeValueAxis} onValueAxisPositionChanged={onValueAxisPositionChanged} setParamByIndex={setParamByIndex} - {...props} + setMultipleValidity={props.setMultipleValidity} + seriesParams={stateParams.seriesParams} + valueAxes={stateParams.valueAxes} + vis={vis} /> ) : null; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx index 91d9987c77f3b..48fcbdf8f9082 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx @@ -21,32 +21,26 @@ import React from 'react'; import { shallow } from 'enzyme'; import { LabelOptions, LabelOptionsProps } from './label_options'; import { TruncateLabelsOption } from '../../common'; -import { valueAxis, categoryAxis } from './mocks'; +import { valueAxis } from './mocks'; jest.mock('ui/new_platform'); const FILTER = 'filter'; const ROTATE = 'rotate'; const DISABLED = 'disabled'; -const CATEGORY_AXES = 'categoryAxes'; describe('LabelOptions component', () => { - let setValue: jest.Mock; + let setAxisLabel: jest.Mock; let defaultProps: LabelOptionsProps; beforeEach(() => { - setValue = jest.fn(); + setAxisLabel = jest.fn(); defaultProps = { - axis: { ...valueAxis }, - axesName: CATEGORY_AXES, - index: 0, - stateParams: { - categoryAxes: [{ ...categoryAxis }], - valueAxes: [{ ...valueAxis }], - } as any, - setValue, - } as any; + axisLabels: { ...valueAxis.labels }, + axisFilterCheckboxName: '', + setAxisLabel, + }; }); it('should init with the default set of props', () => { @@ -64,7 +58,7 @@ describe('LabelOptions component', () => { }); it('should disable other fields when axis.labels.show is false', () => { - defaultProps.axis.labels.show = false; + defaultProps.axisLabels.show = false; const comp = shallow(); expect(comp.find({ paramName: FILTER }).prop(DISABLED)).toBeTruthy(); @@ -76,25 +70,20 @@ describe('LabelOptions component', () => { const comp = shallow(); comp.find({ paramName: ROTATE }).prop('setValue')(ROTATE, '5'); - const newAxes = [{ ...categoryAxis, labels: { ...categoryAxis.labels, rotate: 5 } }]; - expect(setValue).toBeCalledWith(CATEGORY_AXES, newAxes); + expect(setAxisLabel).toBeCalledWith('rotate', 5); }); it('should set filter value', () => { const comp = shallow(); - expect(defaultProps.stateParams.categoryAxes[0].labels.filter).toBeTruthy(); comp.find({ paramName: FILTER }).prop('setValue')(FILTER, false); - const newAxes = [{ ...categoryAxis, labels: { ...categoryAxis.labels, filter: false } }]; - expect(setValue).toBeCalledWith(CATEGORY_AXES, newAxes); + expect(setAxisLabel).toBeCalledWith(FILTER, false); }); it('should set value for valueAxes', () => { - defaultProps.axesName = 'valueAxes'; const comp = shallow(); comp.find(TruncateLabelsOption).prop('setValue')('truncate', 10); - const newAxes = [{ ...valueAxis, labels: { ...valueAxis.labels, truncate: 10 } }]; - expect(setValue).toBeCalledWith('valueAxes', newAxes); + expect(setAxisLabel).toBeCalledWith('truncate', 10); }); }); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx index 2dc5889090dca..b6b54193e9f4a 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx @@ -23,33 +23,21 @@ import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; -import { BasicVislibParams, Axis } from '../../../types'; +import { Axis } from '../../../types'; import { SelectOption, SwitchOption, TruncateLabelsOption } from '../../common'; import { getRotateOptions } from '../../../utils/collections'; -export interface LabelOptionsProps extends VisOptionsProps { - axis: Axis; - axesName: 'categoryAxes' | 'valueAxes'; - index: number; +export type SetAxisLabel = ( + paramName: T, + value: Axis['labels'][T] +) => void; +export interface LabelOptionsProps { + axisLabels: Axis['labels']; + axisFilterCheckboxName: string; + setAxisLabel: SetAxisLabel; } -function LabelOptions({ stateParams, setValue, axis, axesName, index }: LabelOptionsProps) { - const setAxisLabel = useCallback( - (paramName: T, value: Axis['labels'][T]) => { - const axes = [...stateParams[axesName]]; - axes[index] = { - ...axes[index], - labels: { - ...axes[index].labels, - [paramName]: value, - }, - }; - setValue(axesName, axes); - }, - [axesName, index, setValue, stateParams] - ); - +function LabelOptions({ axisLabels, axisFilterCheckboxName, setAxisLabel }: LabelOptionsProps) { const setAxisLabelRotate = useCallback( (paramName: 'rotate', value: Axis['labels']['rotate']) => { setAxisLabel(paramName, Number(value)); @@ -77,20 +65,18 @@ function LabelOptions({ stateParams, setValue, axis, axesName, index }: LabelOpt defaultMessage: 'Show labels', })} paramName="show" - value={axis.labels.show} + value={axisLabels.show} setValue={setAxisLabel} /> @@ -99,20 +85,20 @@ function LabelOptions({ stateParams, setValue, axis, axesName, index }: LabelOpt diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx index 98ef8a094a260..1d29d39bfcb7f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx @@ -21,14 +21,12 @@ import React from 'react'; import { shallow } from 'enzyme'; import { LineOptions, LineOptionsParams } from './line_options'; import { NumberInputOption } from '../../common'; -import { getInterpolationModes } from '../../../utils/collections'; -import { seriesParam } from './mocks'; +import { seriesParam, vis } from './mocks'; jest.mock('ui/new_platform'); const LINE_WIDTH = 'lineWidth'; const DRAW_LINES = 'drawLinesBetweenPoints'; -const interpolationModes = getInterpolationModes(); describe('LineOptions component', () => { let setChart: jest.Mock; @@ -39,15 +37,9 @@ describe('LineOptions component', () => { defaultProps = { chart: { ...seriesParam }, - vis: { - type: { - editorConfig: { - collections: { interpolationModes }, - }, - }, - }, + vis, setChart, - } as any; + }; }); it('should init with the default set of props', () => { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts index 7955bf79c24eb..58c75629f1fa1 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Vis } from 'src/legacy/core_plugins/visualizations/public'; import { Axis, ValueAxis, SeriesParam, Style } from '../../../types'; import { ChartTypes, @@ -25,6 +26,10 @@ import { ScaleTypes, Positions, AxisTypes, + getScaleTypes, + getAxisModes, + getPositions, + getInterpolationModes, } from '../../../utils/collections'; const defaultValueAxisId = 'ValueAxis-1'; @@ -84,4 +89,17 @@ const seriesParam: SeriesParam = { valueAxis: defaultValueAxisId, }; -export { defaultValueAxisId, categoryAxis, valueAxis, seriesParam }; +const positions = getPositions(); +const axisModes = getAxisModes(); +const scaleTypes = getScaleTypes(); +const interpolationModes = getInterpolationModes(); + +const vis = ({ + type: { + editorConfig: { + collections: { scaleTypes, axisModes, positions, interpolationModes }, + }, + }, +} as any) as Vis; + +export { defaultValueAxisId, categoryAxis, valueAxis, seriesParam, vis }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/series_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/series_panel.tsx index db28256816f8d..44e7a4cfb0088 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/series_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/series_panel.tsx @@ -23,19 +23,20 @@ import { EuiPanel, EuiTitle, EuiSpacer, EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from '../../../../../vis_default_editor/public'; -import { BasicVislibParams } from '../../../types'; +import { Vis } from 'src/legacy/core_plugins/visualizations/public'; +import { ValueAxis, SeriesParam } from '../../../types'; import { ChartOptions } from './chart_options'; import { SetParamByIndex, ChangeValueAxis } from './'; -export interface SeriesPanelProps extends VisOptionsProps { +export interface SeriesPanelProps { changeValueAxis: ChangeValueAxis; setParamByIndex: SetParamByIndex; + seriesParams: SeriesParam[]; + valueAxes: ValueAxis[]; + vis: Vis; } -function SeriesPanel(props: SeriesPanelProps) { - const { stateParams } = props; - +function SeriesPanel({ seriesParams, ...chartProps }: SeriesPanelProps) { return ( @@ -48,7 +49,7 @@ function SeriesPanel(props: SeriesPanelProps) { - {stateParams.seriesParams.map((chart, index) => ( + {seriesParams.map((chart, index) => ( - + ))} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx index 7524c7a13435b..141273fa6bc3f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx @@ -21,16 +21,12 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ValueAxesPanel, ValueAxesPanelProps } from './value_axes_panel'; import { ValueAxis, SeriesParam } from '../../../types'; -import { Positions, getScaleTypes, getAxisModes, getPositions } from '../../../utils/collections'; +import { Positions } from '../../../utils/collections'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { valueAxis, seriesParam } from './mocks'; +import { valueAxis, seriesParam, vis } from './mocks'; jest.mock('ui/new_platform'); -const positions = getPositions(); -const axisModes = getAxisModes(); -const scaleTypes = getScaleTypes(); - describe('ValueAxesPanel component', () => { let setParamByIndex: jest.Mock; let onValueAxisPositionChanged: jest.Mock; @@ -66,24 +62,16 @@ describe('ValueAxesPanel component', () => { }; defaultProps = { - stateParams: { - seriesParams: [seriesParamCount, seriesParamAverage], - valueAxes: [axisLeft, axisRight], - }, - vis: { - type: { - editorConfig: { - collections: { scaleTypes, axisModes, positions }, - }, - }, - }, + seriesParams: [seriesParamCount, seriesParamAverage], + valueAxes: [axisLeft, axisRight], + vis, isCategoryAxisHorizontal: false, setParamByIndex, onValueAxisPositionChanged, addValueAxis, removeValueAxis, setMultipleValidity, - } as any; + }; }); it('should init with the default set of props', () => { @@ -93,7 +81,7 @@ describe('ValueAxesPanel component', () => { }); it('should not allow to remove the last value axis', () => { - defaultProps.stateParams.valueAxes = [axisLeft]; + defaultProps.valueAxes = [axisLeft]; const comp = mountWithIntl(); expect(comp.find('[data-test-subj="removeValueAxisBtn"] button').exists()).toBeFalsy(); }); @@ -133,7 +121,7 @@ describe('ValueAxesPanel component', () => { }); it('should show when multiple series match value axis', () => { - defaultProps.stateParams.seriesParams[1].valueAxis = 'ValueAxis-1'; + defaultProps.seriesParams[1].valueAxis = 'ValueAxis-1'; const comp = mountWithIntl(); expect( comp @@ -144,7 +132,7 @@ describe('ValueAxesPanel component', () => { }); it('should not show when no series match value axis', () => { - defaultProps.stateParams.seriesParams[0].valueAxis = 'ValueAxis-2'; + defaultProps.seriesParams[0].valueAxis = 'ValueAxis-2'; const comp = mountWithIntl(); expect( comp diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx index 4aa2aee083a67..30d80ed595fe7 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx @@ -31,31 +31,35 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { BasicVislibParams, ValueAxis } from '../../../types'; +import { Vis } from 'src/legacy/core_plugins/visualizations/public'; +import { SeriesParam, ValueAxis } from '../../../types'; import { ValueAxisOptions } from './value_axis_options'; import { SetParamByIndex } from './'; -import { ValidationVisOptionsProps } from '../../common'; -export interface ValueAxesPanelProps extends ValidationVisOptionsProps { +export interface ValueAxesPanelProps { isCategoryAxisHorizontal: boolean; addValueAxis: () => ValueAxis; removeValueAxis: (axis: ValueAxis) => void; onValueAxisPositionChanged: (index: number, value: ValueAxis['position']) => void; setParamByIndex: SetParamByIndex; + seriesParams: SeriesParam[]; + valueAxes: ValueAxis[]; + vis: Vis; + setMultipleValidity: (paramName: string, isValid: boolean) => void; } function ValueAxesPanel(props: ValueAxesPanelProps) { - const { stateParams, addValueAxis, removeValueAxis } = props; + const { addValueAxis, removeValueAxis, seriesParams, valueAxes } = props; const getSeries = useCallback( (axis: ValueAxis) => { - const isFirst = stateParams.valueAxes[0].id === axis.id; - const series = stateParams.seriesParams.filter( + const isFirst = valueAxes[0].id === axis.id; + const series = seriesParams.filter( serie => serie.valueAxis === axis.id || (isFirst && !serie.valueAxis) ); return series.map(serie => serie.data.label).join(', '); }, - [stateParams.valueAxes, stateParams.seriesParams] + [seriesParams, valueAxes] ); const removeButtonTooltip = useMemo( @@ -131,7 +135,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { - {stateParams.valueAxes.map((axis, index) => ( + {valueAxes.map((axis, index) => ( <> - + ))} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx index bd512e9365783..955867e66d09f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx @@ -20,24 +20,15 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ValueAxisOptions, ValueAxisOptionsParams } from './value_axis_options'; -import { Axis } from '../../../types'; +import { ValueAxis } from '../../../types'; import { TextInputOption } from '../../common'; import { LabelOptions } from './label_options'; -import { - ScaleTypes, - Positions, - getScaleTypes, - getAxisModes, - getPositions, -} from '../../../utils/collections'; -import { valueAxis, categoryAxis } from './mocks'; +import { ScaleTypes, Positions } from '../../../utils/collections'; +import { valueAxis, vis } from './mocks'; jest.mock('ui/new_platform'); const POSITION = 'position'; -const positions = getPositions(); -const axisModes = getAxisModes(); -const scaleTypes = getScaleTypes(); interface PositionOption { text: string; @@ -50,7 +41,7 @@ describe('ValueAxisOptions component', () => { let onValueAxisPositionChanged: jest.Mock; let setMultipleValidity: jest.Mock; let defaultProps: ValueAxisOptionsParams; - let axis: Axis; + let axis: ValueAxis; beforeEach(() => { setParamByIndex = jest.fn(); @@ -61,22 +52,13 @@ describe('ValueAxisOptions component', () => { defaultProps = { axis, index: 0, - stateParams: { - categoryAxes: [{ ...categoryAxis }], - valueAxes: [axis], - }, - vis: { - type: { - editorConfig: { - collections: { scaleTypes, axisModes, positions }, - }, - }, - }, + valueAxis, + vis, isCategoryAxisHorizontal: false, setParamByIndex, onValueAxisPositionChanged, setMultipleValidity, - } as any; + }; }); it('should init with the default set of props', () => { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx index d094a1d422385..0e78bf2f31ef6 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx @@ -21,15 +21,11 @@ import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiAccordion, EuiHorizontalRule } from '@elastic/eui'; -import { BasicVislibParams, ValueAxis } from '../../../types'; +import { Vis } from 'src/legacy/core_plugins/visualizations/public'; +import { ValueAxis } from '../../../types'; import { Positions } from '../../../utils/collections'; -import { - SelectOption, - SwitchOption, - TextInputOption, - ValidationVisOptionsProps, -} from '../../common'; -import { LabelOptions } from './label_options'; +import { SelectOption, SwitchOption, TextInputOption } from '../../common'; +import { LabelOptions, SetAxisLabel } from './label_options'; import { CustomExtentsOptions } from './custom_extents_options'; import { isAxisHorizontal } from './utils'; import { SetParamByIndex } from './'; @@ -39,25 +35,27 @@ export type SetScale = ( value: ValueAxis['scale'][T] ) => void; -export interface ValueAxisOptionsParams extends ValidationVisOptionsProps { +export interface ValueAxisOptionsParams { axis: ValueAxis; index: number; isCategoryAxisHorizontal: boolean; onValueAxisPositionChanged: (index: number, value: ValueAxis['position']) => void; setParamByIndex: SetParamByIndex; + valueAxis: ValueAxis; + vis: Vis; + setMultipleValidity: (paramName: string, isValid: boolean) => void; } -function ValueAxisOptions(props: ValueAxisOptionsParams) { - const { - axis, - index, - isCategoryAxisHorizontal, - stateParams, - vis, - onValueAxisPositionChanged, - setParamByIndex, - } = props; - +function ValueAxisOptions({ + axis, + index, + isCategoryAxisHorizontal, + valueAxis, + vis, + onValueAxisPositionChanged, + setParamByIndex, + setMultipleValidity, +}: ValueAxisOptionsParams) { const setValueAxis = useCallback( (paramName: T, value: ValueAxis[T]) => setParamByIndex('valueAxes', index, paramName, value), @@ -67,25 +65,37 @@ function ValueAxisOptions(props: ValueAxisOptionsParams) { const setValueAxisTitle = useCallback( (paramName: T, value: ValueAxis['title'][T]) => { const title = { - ...stateParams.valueAxes[index].title, + ...valueAxis.title, [paramName]: value, }; setParamByIndex('valueAxes', index, 'title', title); }, - [setParamByIndex, index, stateParams.valueAxes] + [valueAxis.title, setParamByIndex, index] ); const setValueAxisScale: SetScale = useCallback( (paramName, value) => { const scale = { - ...stateParams.valueAxes[index].scale, + ...valueAxis.scale, [paramName]: value, }; setParamByIndex('valueAxes', index, 'scale', scale); }, - [setParamByIndex, index, stateParams.valueAxes] + [valueAxis.scale, setParamByIndex, index] + ); + + const setAxisLabel: SetAxisLabel = useCallback( + (paramName, value) => { + const labels = { + ...valueAxis.labels, + [paramName]: value, + }; + + setParamByIndex('valueAxes', index, 'labels', labels); + }, + [valueAxis.labels, setParamByIndex, index] ); const onPositionChanged = useCallback( @@ -175,7 +185,11 @@ function ValueAxisOptions(props: ValueAxisOptionsParams) { setValue={setValueAxisTitle} /> - + ) : ( @@ -204,9 +218,10 @@ function ValueAxisOptions(props: ValueAxisOptionsParams) { <> diff --git a/src/legacy/ui/public/agg_types/index.ts b/src/legacy/ui/public/agg_types/index.ts index db64bd025b8cb..d066e61df18e9 100644 --- a/src/legacy/ui/public/agg_types/index.ts +++ b/src/legacy/ui/public/agg_types/index.ts @@ -73,7 +73,6 @@ export { isStringType, isType, isValidInterval, - isValidJson, OptionedParamType, parentPipelineType, propFilter, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 8785957dbd98e..399f09bd3e776 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import uuid from 'uuid'; +import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; +import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_types'; +import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler'; +import { getAllCompositeData } from '../../../utils/get_all_composite_data'; import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; import { MetricExpressionParams, @@ -15,6 +19,16 @@ import { } from './types'; import { AlertServices, PluginSetupContract } from '../../../../../alerting/server'; +interface Aggregation { + aggregatedIntervals: { buckets: Array<{ aggregatedValue: { value: number } }> }; +} + +interface CompositeAggregationsResponse { + groupings: { + buckets: Aggregation[]; + }; +} + const FIRED_ACTIONS = { id: 'metrics.threshold.fired', name: i18n.translate('xpack.infra.metrics.alerting.threshold.fired', { @@ -22,11 +36,41 @@ const FIRED_ACTIONS = { }), }; -async function getMetric( - { callCluster }: AlertServices, - { metric, aggType, timeUnit, timeSize, indexPattern }: MetricExpressionParams +const getCurrentValueFromAggregations = (aggregations: Aggregation) => { + const { buckets } = aggregations.aggregatedIntervals; + const { value } = buckets[buckets.length - 1].aggregatedValue; + return value; +}; + +const getParsedFilterQuery: ( + filterQuery: string | undefined +) => Record = filterQuery => { + if (!filterQuery) return {}; + try { + return JSON.parse(filterQuery).bool; + } catch (e) { + return { + query_string: { + query: filterQuery, + analyze_wildcard: true, + }, + }; + } +}; + +const getMetric: ( + services: AlertServices, + params: MetricExpressionParams, + groupBy: string | undefined, + filterQuery: string | undefined +) => Promise> = async function( + { callCluster }, + { metric, aggType, timeUnit, timeSize, indexPattern }, + groupBy, + filterQuery ) { const interval = `${timeSize}${timeUnit}`; + const aggregations = aggType === 'rate' ? networkTraffic('aggregatedValue', metric) @@ -38,6 +82,38 @@ async function getMetric( }, }; + const baseAggs = { + aggregatedIntervals: { + date_histogram: { + field: '@timestamp', + fixed_interval: interval, + }, + aggregations, + }, + }; + + const aggs = groupBy + ? { + groupings: { + composite: { + size: 10, + sources: [ + { + groupBy: { + terms: { + field: groupBy, + }, + }, + }, + ], + }, + aggs: baseAggs, + }, + } + : baseAggs; + + const parsedFilterQuery = getParsedFilterQuery(filterQuery); + const searchBody = { query: { bool: { @@ -48,34 +124,49 @@ async function getMetric( gte: `now-${interval}`, }, }, + }, + { exists: { field: metric, }, }, ], + ...parsedFilterQuery, }, }, size: 0, - aggs: { - aggregatedIntervals: { - date_histogram: { - field: '@timestamp', - fixed_interval: interval, - }, - aggregations, - }, - }, + aggs, }; + if (groupBy) { + const bucketSelector = ( + response: InfraDatabaseSearchResponse<{}, CompositeAggregationsResponse> + ) => response.aggregations?.groupings?.buckets || []; + const afterKeyHandler = createAfterKeyHandler( + 'aggs.groupings.composite.after', + response => response.aggregations?.groupings?.after_key + ); + const compositeBuckets = (await getAllCompositeData( + body => callCluster('search', { body, index: indexPattern }), + searchBody, + bucketSelector, + afterKeyHandler + )) as Array; + return compositeBuckets.reduce( + (result, bucket) => ({ + ...result, + [bucket.key.groupBy]: getCurrentValueFromAggregations(bucket), + }), + {} + ); + } + const result = await callCluster('search', { body: searchBody, index: indexPattern, }); - - const { buckets } = result.aggregations.aggregatedIntervals; - const { value } = buckets[buckets.length - 1].aggregatedValue; - return value; -} + return { '*': getCurrentValueFromAggregations(result.aggregations) }; +}; const comparatorMap = { [Comparator.BETWEEN]: (value: number, [a, b]: number[]) => @@ -112,39 +203,54 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet indexPattern: schema.string(), }) ), + groupBy: schema.maybe(schema.string()), + filterQuery: schema.maybe(schema.string()), }), }, defaultActionGroupId: FIRED_ACTIONS.id, actionGroups: [FIRED_ACTIONS], async executor({ services, params }) { - const { criteria } = params as { criteria: MetricExpressionParams[] }; - const alertInstance = services.alertInstanceFactory(alertUUID); + const { criteria, groupBy, filterQuery } = params as { + criteria: MetricExpressionParams[]; + groupBy: string | undefined; + filterQuery: string | undefined; + }; const alertResults = await Promise.all( - criteria.map(({ threshold, comparator }) => + criteria.map(criterion => (async () => { - const currentValue = await getMetric(services, params as MetricExpressionParams); - if (typeof currentValue === 'undefined') + const currentValues = await getMetric(services, criterion, groupBy, filterQuery); + if (typeof currentValues === 'undefined') throw new Error('Could not get current value of metric'); - + const { threshold, comparator } = criterion; const comparisonFunction = comparatorMap[comparator]; - return { shouldFire: comparisonFunction(currentValue, threshold), currentValue }; + + return mapValues(currentValues, value => ({ + shouldFire: comparisonFunction(value, threshold), + currentValue: value, + })); })() ) ); - const shouldAlertFire = alertResults.every(({ shouldFire }) => shouldFire); + const groups = Object.keys(alertResults[0]); + for (const group of groups) { + const alertInstance = services.alertInstanceFactory(`${alertUUID}-${group}`); + + const shouldAlertFire = alertResults.every(result => result[group].shouldFire); - if (shouldAlertFire) { - alertInstance.scheduleActions(FIRED_ACTIONS.id, { - value: alertResults.map(({ currentValue }) => currentValue), + if (shouldAlertFire) { + alertInstance.scheduleActions(FIRED_ACTIONS.id, { + group, + value: alertResults.map(result => result[group].currentValue), + }); + } + + // Future use: ability to fetch display current alert state + alertInstance.replaceState({ + alertState: shouldAlertFire ? AlertStates.ALERT : AlertStates.OK, }); } - - // Future use: ability to fetch display current alert state - alertInstance.replaceState({ - alertState: shouldAlertFire ? AlertStates.ALERT : AlertStates.OK, - }); }, }); } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index 9bb0d8963ac66..1c3d0cea3dc84 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -31,4 +31,5 @@ export interface MetricExpressionParams { indexPattern: string; threshold: number[]; comparator: Comparator; + filterQuery: string; } diff --git a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts index ef453be76d8d7..8e5f8e6716f3c 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts @@ -77,6 +77,11 @@ const handleAfterKey = createAfterKeyHandler( input => input?.aggregations?.nodes?.after_key ); +const callClusterFactory = (framework: KibanaFramework, requestContext: RequestHandlerContext) => ( + opts: any +) => + framework.callWithRequest<{}, InfraSnapshotAggregationResponse>(requestContext, 'search', opts); + const requestGroupedNodes = async ( requestContext: RequestHandlerContext, options: InfraSnapshotRequestOptions, @@ -119,7 +124,7 @@ const requestGroupedNodes = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeGroupByBucket - >(framework, requestContext, query, bucketSelector, handleAfterKey); + >(callClusterFactory(framework, requestContext), query, bucketSelector, handleAfterKey); }; const requestNodeMetrics = async ( @@ -170,7 +175,7 @@ const requestNodeMetrics = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeMetricsBucket - >(framework, requestContext, query, bucketSelector, handleAfterKey); + >(callClusterFactory(framework, requestContext), query, bucketSelector, handleAfterKey); }; // buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[] diff --git a/x-pack/plugins/infra/server/utils/get_all_composite_data.ts b/x-pack/plugins/infra/server/utils/get_all_composite_data.ts index c7ff1b077f685..093dd266ea915 100644 --- a/x-pack/plugins/infra/server/utils/get_all_composite_data.ts +++ b/x-pack/plugins/infra/server/utils/get_all_composite_data.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext } from 'src/core/server'; -import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; import { InfraDatabaseSearchResponse } from '../lib/adapters/framework'; export const getAllCompositeData = async < @@ -13,18 +11,13 @@ export const getAllCompositeData = async < Bucket = {}, Options extends object = {} >( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + callCluster: (options: Options) => Promise>, options: Options, bucketSelector: (response: InfraDatabaseSearchResponse<{}, Aggregation>) => Bucket[], onAfterKey: (options: Options, response: InfraDatabaseSearchResponse<{}, Aggregation>) => Options, previousBuckets: Bucket[] = [] ): Promise => { - const response = await framework.callWithRequest<{}, Aggregation>( - requestContext, - 'search', - options - ); + const response = await callCluster(options); // Nothing available, return the previous buckets. if (response.hits.total.value === 0) { @@ -46,8 +39,7 @@ export const getAllCompositeData = async < // There is possibly more data, concat previous and current buckets and call ourselves recursively. const newOptions = onAfterKey(options, response); return getAllCompositeData( - framework, - requestContext, + callCluster, newOptions, bucketSelector, onAfterKey, diff --git a/x-pack/plugins/transform/common/utils/object_utils.test.ts b/x-pack/plugins/transform/common/utils/object_utils.test.ts new file mode 100644 index 0000000000000..7ac68b41b625c --- /dev/null +++ b/x-pack/plugins/transform/common/utils/object_utils.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getNestedProperty } from './object_utils'; + +describe('object_utils', () => { + test('getNestedProperty()', () => { + const testObj = { + the: { + nested: { + value: 'the-nested-value', + }, + }, + }; + + const falseyObj = { + the: { + nested: { + value: false, + }, + other_nested: { + value: 0, + }, + }, + }; + + const test1 = getNestedProperty(testObj, 'the'); + expect(typeof test1).toBe('object'); + expect(Object.keys(test1)).toStrictEqual(['nested']); + + const test2 = getNestedProperty(testObj, 'the$'); + expect(typeof test2).toBe('undefined'); + + const test3 = getNestedProperty(testObj, 'the$', 'the-default-value'); + expect(typeof test3).toBe('string'); + expect(test3).toBe('the-default-value'); + + const test4 = getNestedProperty(testObj, 'the.neSted'); + expect(typeof test4).toBe('undefined'); + + const test5 = getNestedProperty(testObj, 'the.nested'); + expect(typeof test5).toBe('object'); + expect(Object.keys(test5)).toStrictEqual(['value']); + + const test6 = getNestedProperty(testObj, 'the.nested.vaLue'); + expect(typeof test6).toBe('undefined'); + + const test7 = getNestedProperty(testObj, 'the.nested.value'); + expect(typeof test7).toBe('string'); + expect(test7).toBe('the-nested-value'); + + const test8 = getNestedProperty(testObj, 'the.nested.value.doesntExist'); + expect(typeof test8).toBe('undefined'); + + const test9 = getNestedProperty(testObj, 'the.nested.value.doesntExist', 'the-default-value'); + expect(typeof test9).toBe('string'); + expect(test9).toBe('the-default-value'); + + const test10 = getNestedProperty(falseyObj, 'the.nested.value'); + expect(typeof test10).toBe('boolean'); + expect(test10).toBe(false); + + const test11 = getNestedProperty(falseyObj, 'the.other_nested.value'); + expect(typeof test11).toBe('number'); + expect(test11).toBe(0); + }); +}); diff --git a/x-pack/plugins/transform/common/utils/object_utils.ts b/x-pack/plugins/transform/common/utils/object_utils.ts index 589803b33e11c..dfdcd0959260d 100644 --- a/x-pack/plugins/transform/common/utils/object_utils.ts +++ b/x-pack/plugins/transform/common/utils/object_utils.ts @@ -11,5 +11,9 @@ export const getNestedProperty = ( accessor: string, defaultValue?: any ) => { - return accessor.split('.').reduce((o, i) => o?.[i], obj) || defaultValue; + const value = accessor.split('.').reduce((o, i) => o?.[i], obj); + + if (value === undefined) return defaultValue; + + return value; }; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts new file mode 100644 index 0000000000000..172256ddb5cee --- /dev/null +++ b/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiDataGridSorting } from '@elastic/eui'; + +import { + getPreviewRequestBody, + PivotAggsConfig, + PivotGroupByConfig, + PIVOT_SUPPORTED_AGGS, + PIVOT_SUPPORTED_GROUP_BY_AGGS, + SimpleQuery, +} from '../../common'; + +import { multiColumnSortFactory, getPivotPreviewDevConsoleStatement } from './common'; + +describe('Transform: Define Pivot Common', () => { + test('multiColumnSortFactory()', () => { + const data = [ + { s: 'a', n: 1 }, + { s: 'a', n: 2 }, + { s: 'b', n: 3 }, + { s: 'b', n: 4 }, + ]; + + const sortingColumns1: EuiDataGridSorting['columns'] = [{ id: 's', direction: 'desc' }]; + const multiColumnSort1 = multiColumnSortFactory(sortingColumns1); + data.sort(multiColumnSort1); + + expect(data).toStrictEqual([ + { s: 'b', n: 3 }, + { s: 'b', n: 4 }, + { s: 'a', n: 1 }, + { s: 'a', n: 2 }, + ]); + + const sortingColumns2: EuiDataGridSorting['columns'] = [ + { id: 's', direction: 'asc' }, + { id: 'n', direction: 'desc' }, + ]; + const multiColumnSort2 = multiColumnSortFactory(sortingColumns2); + data.sort(multiColumnSort2); + + expect(data).toStrictEqual([ + { s: 'a', n: 2 }, + { s: 'a', n: 1 }, + { s: 'b', n: 4 }, + { s: 'b', n: 3 }, + ]); + + const sortingColumns3: EuiDataGridSorting['columns'] = [ + { id: 'n', direction: 'desc' }, + { id: 's', direction: 'desc' }, + ]; + const multiColumnSort3 = multiColumnSortFactory(sortingColumns3); + data.sort(multiColumnSort3); + + expect(data).toStrictEqual([ + { s: 'b', n: 4 }, + { s: 'b', n: 3 }, + { s: 'a', n: 2 }, + { s: 'a', n: 1 }, + ]); + }); + + test('getPivotPreviewDevConsoleStatement()', () => { + const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, + }; + const groupBy: PivotGroupByConfig = { + agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, + field: 'the-group-by-field', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', + }; + const agg: PivotAggsConfig = { + agg: PIVOT_SUPPORTED_AGGS.AVG, + field: 'the-agg-field', + aggName: 'the-agg-agg-name', + dropDownName: 'the-agg-drop-down-name', + }; + const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]); + const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); + + expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview +{ + "source": { + "index": [ + "the-index-pattern-title" + ] + }, + "pivot": { + "group_by": { + "the-group-by-agg-name": { + "terms": { + "field": "the-group-by-field" + } + } + }, + "aggregations": { + "the-agg-agg-name": { + "avg": { + "field": "the-agg-field" + } + } + } + } +} +`); + }); +}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts new file mode 100644 index 0000000000000..498c3a3ac60af --- /dev/null +++ b/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiDataGridSorting } from '@elastic/eui'; + +import { getNestedProperty } from '../../../../common/utils/object_utils'; + +import { PreviewRequestBody } from '../../common'; + +/** + * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. + * `sortFn()` is recursive to support sorting on multiple columns. + * + * @param sortingColumns - The EUI data grid sorting configuration + * @returns The sorting function which can be used with an array's sort() function. + */ +export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => { + const isString = (arg: any): arg is string => { + return typeof arg === 'string'; + }; + + const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => { + const sort = sortingColumns[sortingColumnIndex]; + const aValue = getNestedProperty(a, sort.id, null); + const bValue = getNestedProperty(b, sort.id, null); + + if (typeof aValue === 'number' && typeof bValue === 'number') { + if (aValue < bValue) { + return sort.direction === 'asc' ? -1 : 1; + } + if (aValue > bValue) { + return sort.direction === 'asc' ? 1 : -1; + } + } + + if (isString(aValue) && isString(bValue)) { + if (aValue.localeCompare(bValue) === -1) { + return sort.direction === 'asc' ? -1 : 1; + } + if (aValue.localeCompare(bValue) === 1) { + return sort.direction === 'asc' ? 1 : -1; + } + } + + if (sortingColumnIndex + 1 < sortingColumns.length) { + return sortFn(a, b, sortingColumnIndex + 1); + } + + return 0; + }; + + return sortFn; +}; + +export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { + return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; +}; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts new file mode 100644 index 0000000000000..049e73d6309fc --- /dev/null +++ b/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PivotPreview } from './pivot_preview'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx similarity index 82% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx rename to x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx index f39885f520995..b37cdbb132bab 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx +++ b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx @@ -8,20 +8,19 @@ import React from 'react'; import { render, wait } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { Providers } from '../../../../app_dependencies.mock'; +import { Providers } from '../../app_dependencies.mock'; import { getPivotQuery, PivotAggsConfig, PivotGroupByConfig, PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, -} from '../../../../common'; -import { SearchItems } from '../../../../hooks/use_search_items'; +} from '../../common'; import { PivotPreview } from './pivot_preview'; jest.mock('ui/new_platform'); -jest.mock('../../../../../shared_imports'); +jest.mock('../../../shared_imports'); describe('Transform: ', () => { // Using the async/await wait()/done() pattern to avoid act() errors. @@ -42,10 +41,7 @@ describe('Transform: ', () => { const props = { aggs: { 'the-agg-name': agg }, groupBy: { 'the-group-by-name': groupBy }, - indexPattern: { - title: 'the-index-pattern-title', - fields: [] as any[], - } as SearchItems['indexPattern'], + indexPatternTitle: 'the-index-pattern-title', query: getPivotQuery('the-query'), }; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx similarity index 75% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx rename to x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx index 9b32bbbae839e..51ca9f38a3d10 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx +++ b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment-timezone'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -21,8 +22,11 @@ import { EuiTitle, } from '@elastic/eui'; -import { dictionaryToArray } from '../../../../../../common/types/common'; -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; +import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; + +import { dictionaryToArray } from '../../../../common/types/common'; +import { formatHumanReadableDateTimeSeconds } from '../../../../common/utils/date_utils'; +import { getNestedProperty } from '../../../../common/utils/object_utils'; import { euiDataGridStyle, @@ -33,8 +37,8 @@ import { PivotGroupByConfig, PivotGroupByConfigDict, PivotQuery, -} from '../../../../common'; -import { SearchItems } from '../../../../hooks/use_search_items'; +} from '../../common'; +import { SearchItems } from '../../hooks/use_search_items'; import { getPivotPreviewDevConsoleStatement, multiColumnSortFactory } from './common'; import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; @@ -102,21 +106,22 @@ const ErrorMessage: FC = ({ message }) => ( interface PivotPreviewProps { aggs: PivotAggsConfigDict; groupBy: PivotGroupByConfigDict; - indexPattern: SearchItems['indexPattern']; + indexPatternTitle: SearchItems['indexPattern']['title']; query: PivotQuery; + showHeader?: boolean; } const defaultPagination = { pageIndex: 0, pageSize: 5 }; export const PivotPreview: FC = React.memo( - ({ aggs, groupBy, indexPattern, query }) => { + ({ aggs, groupBy, indexPatternTitle, query, showHeader = true }) => { const { previewData: data, previewMappings, errorMessage, previewRequest, status, - } = usePivotPreviewData(indexPattern, query, aggs, groupBy); + } = usePivotPreviewData(indexPatternTitle, query, aggs, groupBy); const groupByArr = dictionaryToArray(groupBy); // Filters mapping properties of type `object`, which get returned for nested field parents. @@ -142,7 +147,42 @@ export const PivotPreview: FC = React.memo( }, [data.length]); // EuiDataGrid State - const dataGridColumns = columnKeys.map(id => ({ id })); + const dataGridColumns = columnKeys.map(id => { + const field = previewMappings.properties[id]; + + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + + switch (field?.type) { + case ES_FIELD_TYPES.GEO_POINT: + case ES_FIELD_TYPES.GEO_SHAPE: + schema = 'json'; + break; + case ES_FIELD_TYPES.BOOLEAN: + schema = 'boolean'; + break; + case ES_FIELD_TYPES.DATE: + schema = 'datetime'; + break; + case ES_FIELD_TYPES.BYTE: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.SHORT: + schema = 'numeric'; + break; + // keep schema undefined for text based columns + case ES_FIELD_TYPES.KEYWORD: + case ES_FIELD_TYPES.TEXT: + break; + } + + return { id, schema }; + }); const onChangeItemsPerPage = useCallback( pageSize => { @@ -191,13 +231,17 @@ export const PivotPreview: FC = React.memo( return JSON.stringify(cellValue); } - if (cellValue === undefined) { + if (cellValue === undefined || cellValue === null) { return null; } + if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.DATE) { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + return cellValue; }; - }, [pageData, pagination.pageIndex, pagination.pageSize]); + }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); if (status === PIVOT_PREVIEW_STATUS.ERROR) { return ( @@ -256,13 +300,17 @@ export const PivotPreview: FC = React.memo( return (
- -
- {status === PIVOT_PREVIEW_STATUS.LOADING && } - {status !== PIVOT_PREVIEW_STATUS.LOADING && ( - - )} -
+ {showHeader && ( + <> + +
+ {status === PIVOT_PREVIEW_STATUS.LOADING && } + {status !== PIVOT_PREVIEW_STATUS.LOADING && ( + + )} +
+ + )} {dataGridColumns.length > 0 && data.length > 0 && ( void; interface TestHookProps { @@ -46,12 +44,7 @@ let pivotPreviewObj: UsePivotPreviewDataReturnType; describe('usePivotPreviewData', () => { test('indexPattern not defined', () => { testHook(() => { - pivotPreviewObj = usePivotPreviewData( - ({ id: 'the-id', title: 'the-title', fields: [] } as unknown) as IndexPattern, - query, - {}, - {} - ); + pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {}); }); expect(pivotPreviewObj.errorMessage).toBe(''); @@ -61,12 +54,7 @@ describe('usePivotPreviewData', () => { test('indexPattern set triggers loading', () => { testHook(() => { - pivotPreviewObj = usePivotPreviewData( - ({ id: 'the-id', title: 'the-title', fields: [] } as unknown) as IndexPattern, - query, - {}, - {} - ); + pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {}); }); expect(pivotPreviewObj.errorMessage).toBe(''); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts similarity index 86% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts rename to x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts index 215435027d5b8..c3ccddbfc2906 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts +++ b/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts @@ -6,11 +6,11 @@ import { useEffect, useState } from 'react'; -import { dictionaryToArray } from '../../../../../../common/types/common'; -import { useApi } from '../../../../hooks/use_api'; +import { dictionaryToArray } from '../../../../common/types/common'; +import { useApi } from '../../hooks/use_api'; -import { Dictionary } from '../../../../../../common/types/common'; -import { IndexPattern, ES_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/public'; +import { Dictionary } from '../../../../common/types/common'; +import { IndexPattern, ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; import { getPreviewRequestBody, @@ -18,7 +18,7 @@ import { PivotAggsConfigDict, PivotGroupByConfigDict, PivotQuery, -} from '../../../../common'; +} from '../../common'; export enum PIVOT_PREVIEW_STATUS { UNUSED, @@ -51,7 +51,7 @@ export interface GetTransformsResponse { } export const usePivotPreviewData = ( - indexPattern: IndexPattern, + indexPatternTitle: IndexPattern['title'], query: PivotQuery, aggs: PivotAggsConfigDict, groupBy: PivotGroupByConfigDict @@ -65,7 +65,7 @@ export const usePivotPreviewData = ( const aggsArr = dictionaryToArray(aggs); const groupByArr = dictionaryToArray(groupBy); - const previewRequest = getPreviewRequestBody(indexPattern.title, query, groupByArr, aggsArr); + const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); const getPreviewData = async () => { if (aggsArr.length === 0 || groupByArr.length === 0) { @@ -94,7 +94,7 @@ export const usePivotPreviewData = ( // custom comparison /* eslint-disable react-hooks/exhaustive-deps */ }, [ - indexPattern.title, + indexPatternTitle, JSON.stringify(aggsArr), JSON.stringify(groupByArr), JSON.stringify(query), diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx index 76ed12ff772f5..2a467ba4a5772 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import moment from 'moment-timezone'; +import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -17,9 +18,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiProgress, + EuiSpacer, EuiTitle, } from '@elastic/eui'; +import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; import { getNestedProperty } from '../../../../../../common/utils/object_utils'; import { @@ -29,6 +32,7 @@ import { PivotQuery, } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; +import { useToastNotifications } from '../../../../app_dependencies'; import { getSourceIndexDevConsoleStatement } from './common'; import { SOURCE_INDEX_STATUS, useSourceIndexData } from './use_source_index_data'; @@ -52,9 +56,8 @@ interface Props { query: PivotQuery; } -const defaultPagination = { pageIndex: 0, pageSize: 5 }; - export const SourceIndexPreview: React.FC = React.memo(({ indexPattern, query }) => { + const toastNotifications = useToastNotifications(); const allFields = indexPattern.fields.map(f => f.name); const indexPatternFields: string[] = allFields.filter(f => { if (indexPattern.metaFields.includes(f)) { @@ -73,38 +76,67 @@ export const SourceIndexPreview: React.FC = React.memo(({ indexPattern, q // Column visibility const [visibleColumns, setVisibleColumns] = useState(indexPatternFields); - const [pagination, setPagination] = useState(defaultPagination); - - useEffect(() => { - setPagination(defaultPagination); - }, [query]); - - const { errorMessage, status, rowCount, tableItems: data } = useSourceIndexData( - indexPattern, - query, - pagination - ); + const { + errorMessage, + pagination, + setPagination, + setSortingColumns, + rowCount, + sortingColumns, + status, + tableItems: data, + } = useSourceIndexData(indexPattern, query); // EuiDataGrid State - const dataGridColumns = indexPatternFields.map(id => { - const field = indexPattern.fields.getByName(id); - - let schema = 'string'; - - switch (field?.type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'number': - schema = 'numeric'; - break; - } + const dataGridColumns = [ + ...indexPatternFields.map(id => { + const field = indexPattern.fields.getByName(id); + + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + + switch (field?.type) { + case 'date': + schema = 'datetime'; + break; + case 'geo_point': + schema = 'json'; + break; + case 'number': + schema = 'numeric'; + break; + } - return { id, schema }; - }); + return { id, schema }; + }), + ]; + + const onSort = useCallback( + (sc: Array<{ id: string; direction: 'asc' | 'desc' }>) => { + // Check if an unsupported column type for sorting was selected. + const invalidSortingColumnns = sc.reduce((arr, current) => { + const columnType = dataGridColumns.find(dgc => dgc.id === current.id); + if (columnType?.schema === 'json') { + arr.push(current.id); + } + return arr; + }, []); + if (invalidSortingColumnns.length === 0) { + setSortingColumns(sc); + } else { + invalidSortingColumnns.forEach(columnId => { + toastNotifications.addDanger( + i18n.translate('xpack.transform.sourceIndexPreview.invalidSortingColumnError', { + defaultMessage: `The column '{columnId}' cannot be used for sorting.`, + values: { columnId }, + }) + ); + }); + } + }, + [dataGridColumns, setSortingColumns, toastNotifications] + ); const onChangeItemsPerPage = useCallback( pageSize => { @@ -120,10 +152,6 @@ export const SourceIndexPreview: React.FC = React.memo(({ indexPattern, q setPagination, ]); - // ** Sorting config - const [sortingColumns, setSortingColumns] = useState([]); - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - const renderCellValue = useMemo(() => { return ({ rowIndex, @@ -144,32 +172,18 @@ export const SourceIndexPreview: React.FC = React.memo(({ indexPattern, q return JSON.stringify(cellValue); } - if (cellValue === undefined) { + if (cellValue === undefined || cellValue === null) { return null; } + const field = indexPattern.fields.getByName(columnId); + if (field?.type === 'date') { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + return cellValue; }; - }, [data, pagination.pageIndex, pagination.pageSize]); - - if (status === SOURCE_INDEX_STATUS.ERROR) { - return ( -
- - - - {errorMessage} - - -
- ); - } + }, [data, indexPattern.fields, pagination.pageIndex, pagination.pageSize]); if (status === SOURCE_INDEX_STATUS.LOADED && data.length === 0) { return ( @@ -200,7 +214,11 @@ export const SourceIndexPreview: React.FC = React.memo(({ indexPattern, q }); return ( -
+
@@ -222,24 +240,38 @@ export const SourceIndexPreview: React.FC = React.memo(({ indexPattern, q )}
- {dataGridColumns.length > 0 && data.length > 0 && ( - + {status === SOURCE_INDEX_STATUS.ERROR && ( +
+ + + {errorMessage} + + + +
)} +
); }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx index 9992f153f3b86..5a1d8a8db5b42 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx @@ -26,10 +26,7 @@ const query: SimpleQuery = { describe('useSourceIndexData', () => { test('indexPattern set triggers loading', async done => { const { result, waitForNextUpdate } = renderHook(() => - useSourceIndexData({ id: 'the-id', title: 'the-title', fields: [] }, query, { - pageIndex: 0, - pageSize: 10, - }) + useSourceIndexData({ id: 'the-id', title: 'the-title', fields: [] }, query) ); const sourceIndexObj: UseSourceIndexDataReturnType = result.current; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts index ae5bd9040baca..5301a3c168a51 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import { useEffect, useState, Dispatch, SetStateAction } from 'react'; import { SearchResponse } from 'elasticsearch'; +import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; + import { IIndexPattern } from 'src/plugins/data/public'; -import { useApi } from '../../../../hooks/use_api'; +import { Dictionary } from '../../../../../../common/types/common'; import { isDefaultQuery, matchAllQuery, EsDocSource, PivotQuery } from '../../../../common'; +import { useApi } from '../../../../hooks/use_api'; export enum SOURCE_INDEX_STATUS { UNUSED, @@ -21,19 +24,25 @@ export enum SOURCE_INDEX_STATUS { ERROR, } +type EsSorting = Dictionary<{ + order: 'asc' | 'desc'; +}>; + interface ErrorResponse { - error: { - body: string; - msg: string; - path: string; - query: any; - response: string; + request: Dictionary; + response: Dictionary; + body: { statusCode: number; + error: string; + message: string; }; + name: string; + req: Dictionary; + res: Dictionary; } const isErrorResponse = (arg: any): arg is ErrorResponse => { - return arg.error !== undefined; + return arg?.body?.error !== undefined && arg?.body?.message !== undefined; }; // The types specified in `@types/elasticsearch` are out of date and still have `total: number`. @@ -46,42 +55,60 @@ interface SearchResponse7 extends SearchResponse { }; } -type SourceIndexSearchResponse = ErrorResponse | SearchResponse7; +type SourceIndexSearchResponse = SearchResponse7; + +type SourceIndexPagination = Pick; +const defaultPagination: SourceIndexPagination = { pageIndex: 0, pageSize: 5 }; export interface UseSourceIndexDataReturnType { errorMessage: string; - status: SOURCE_INDEX_STATUS; + pagination: SourceIndexPagination; + setPagination: Dispatch>; + setSortingColumns: Dispatch>; rowCount: number; + sortingColumns: EuiDataGridSorting['columns']; + status: SOURCE_INDEX_STATUS; tableItems: EsDocSource[]; } export const useSourceIndexData = ( indexPattern: IIndexPattern, - query: PivotQuery, - pagination: { pageIndex: number; pageSize: number } + query: PivotQuery ): UseSourceIndexDataReturnType => { const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(SOURCE_INDEX_STATUS.UNUSED); + const [pagination, setPagination] = useState(defaultPagination); + const [sortingColumns, setSortingColumns] = useState([]); const [rowCount, setRowCount] = useState(0); const [tableItems, setTableItems] = useState([]); const api = useApi(); + useEffect(() => { + setPagination(defaultPagination); + }, [query]); + const getSourceIndexData = async function() { setErrorMessage(''); setStatus(SOURCE_INDEX_STATUS.LOADING); - try { - const resp: SourceIndexSearchResponse = await api.esSearch({ - index: indexPattern.title, + const sort: EsSorting = sortingColumns.reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const esSearchRequest = { + index: indexPattern.title, + body: { + // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. + query: isDefaultQuery(query) ? matchAllQuery : query, from: pagination.pageIndex * pagination.pageSize, size: pagination.pageSize, - // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. - body: { query: isDefaultQuery(query) ? matchAllQuery : query }, - }); + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }; - if (isErrorResponse(resp)) { - throw resp.error; - } + try { + const resp: SourceIndexSearchResponse = await api.esSearch(esSearchRequest); const docs = resp.hits.hits.map(d => d._source); @@ -89,12 +116,11 @@ export const useSourceIndexData = ( setTableItems(docs); setStatus(SOURCE_INDEX_STATUS.LOADED); } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); + if (isErrorResponse(e)) { + setErrorMessage(`${e.body.error}: ${e.body.message}`); } else { setErrorMessage(JSON.stringify(e, null, 2)); } - setTableItems([]); setStatus(SOURCE_INDEX_STATUS.ERROR); } }; @@ -103,6 +129,15 @@ export const useSourceIndexData = ( getSourceIndexData(); // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPattern.title, JSON.stringify(query), JSON.stringify(pagination)]); - return { errorMessage, status, rowCount, tableItems }; + }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + return { + errorMessage, + pagination, + setPagination, + setSortingColumns, + rowCount, + sortingColumns, + status, + tableItems, + }; }; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts index c9a52304578ee..5db6a233c9134 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts @@ -4,73 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiDataGridSorting } from '@elastic/eui'; - -import { - getPreviewRequestBody, - PivotAggsConfig, - PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, - PIVOT_SUPPORTED_GROUP_BY_AGGS, - SimpleQuery, -} from '../../../../common'; - -import { - multiColumnSortFactory, - getPivotPreviewDevConsoleStatement, - getPivotDropdownOptions, -} from './common'; +import { getPivotDropdownOptions } from './common'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; describe('Transform: Define Pivot Common', () => { - test('customSortFactory()', () => { - const data = [ - { s: 'a', n: 1 }, - { s: 'a', n: 2 }, - { s: 'b', n: 3 }, - { s: 'b', n: 4 }, - ]; - - const sortingColumns1: EuiDataGridSorting['columns'] = [{ id: 's', direction: 'desc' }]; - const multiColumnSort1 = multiColumnSortFactory(sortingColumns1); - data.sort(multiColumnSort1); - - expect(data).toStrictEqual([ - { s: 'b', n: 3 }, - { s: 'b', n: 4 }, - { s: 'a', n: 1 }, - { s: 'a', n: 2 }, - ]); - - const sortingColumns2: EuiDataGridSorting['columns'] = [ - { id: 's', direction: 'asc' }, - { id: 'n', direction: 'desc' }, - ]; - const multiColumnSort2 = multiColumnSortFactory(sortingColumns2); - data.sort(multiColumnSort2); - - expect(data).toStrictEqual([ - { s: 'a', n: 2 }, - { s: 'a', n: 1 }, - { s: 'b', n: 4 }, - { s: 'b', n: 3 }, - ]); - - const sortingColumns3: EuiDataGridSorting['columns'] = [ - { id: 'n', direction: 'desc' }, - { id: 's', direction: 'desc' }, - ]; - const multiColumnSort3 = multiColumnSortFactory(sortingColumns3); - data.sort(multiColumnSort3); - - expect(data).toStrictEqual([ - { s: 'b', n: 4 }, - { s: 'b', n: 3 }, - { s: 'a', n: 2 }, - { s: 'a', n: 1 }, - ]); - }); - test('getPivotDropdownOptions()', () => { // The field name includes the characters []> as well as a leading and ending space charcter // which cannot be used for aggregation names. The test results verifies that the characters @@ -155,53 +92,4 @@ describe('Transform: Define Pivot Common', () => { }, }); }); - - test('getPivotPreviewDevConsoleStatement()', () => { - const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, - }; - const groupBy: PivotGroupByConfig = { - agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, - field: 'the-group-by-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const agg: PivotAggsConfig = { - agg: PIVOT_SUPPORTED_AGGS.AVG, - field: 'the-agg-field', - aggName: 'the-agg-agg-name', - dropDownName: 'the-agg-drop-down-name', - }; - const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]); - const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); - - expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview -{ - "source": { - "index": [ - "the-index-pattern-title" - ] - }, - "pivot": { - "group_by": { - "the-group-by-agg-name": { - "terms": { - "field": "the-group-by-field" - } - } - }, - "aggregations": { - "the-agg-agg-name": { - "avg": { - "field": "the-agg-field" - } - } - } - } -} -`); - }); }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts index 0779cb1339af6..a9413afb6243e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { EuiComboBoxOptionOption, EuiDataGridSorting } from '@elastic/eui'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; import { IndexPattern, KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/public'; -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; - import { - PreviewRequestBody, DropDownLabel, DropDownOption, EsFieldName, @@ -27,51 +24,6 @@ export interface Field { type: KBN_FIELD_TYPES; } -/** - * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. - * `sortFn()` is recursive to support sorting on multiple columns. - * - * @param sortingColumns - The EUI data grid sorting configuration - * @returns The sorting function which can be used with an array's sort() function. - */ -export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => { - const isString = (arg: any): arg is string => { - return typeof arg === 'string'; - }; - - const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => { - const sort = sortingColumns[sortingColumnIndex]; - const aValue = getNestedProperty(a, sort.id, null); - const bValue = getNestedProperty(b, sort.id, null); - - if (typeof aValue === 'number' && typeof bValue === 'number') { - if (aValue < bValue) { - return sort.direction === 'asc' ? -1 : 1; - } - if (aValue > bValue) { - return sort.direction === 'asc' ? 1 : -1; - } - } - - if (isString(aValue) && isString(bValue)) { - if (aValue.localeCompare(bValue) === -1) { - return sort.direction === 'asc' ? -1 : 1; - } - if (aValue.localeCompare(bValue) === 1) { - return sort.direction === 'asc' ? 1 : -1; - } - } - - if (sortingColumnIndex + 1 < sortingColumns.length) { - return sortFn(a, b, sortingColumnIndex + 1); - } - - return 0; - }; - - return sortFn; -}; - function getDefaultGroupByConfig( aggName: string, dropDownName: string, @@ -166,7 +118,3 @@ export function getPivotDropdownOptions(indexPattern: IndexPattern) { aggOptionsData, }; } - -export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { - return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; -}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 254d867165ae6..5b6283fc4777d 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -26,6 +26,7 @@ import { EuiSwitch, } from '@elastic/eui'; +import { PivotPreview } from '../../../../components/pivot_preview'; import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { SavedSearchQuery, SearchItems } from '../../../../hooks/use_search_items'; import { useXJsonMode, xJsonMode } from '../../../../hooks/use_x_json_mode'; @@ -36,7 +37,6 @@ import { DropDown } from '../aggregation_dropdown'; import { AggListForm } from '../aggregation_list'; import { GroupByListForm } from '../group_by_list'; import { SourceIndexPreview } from '../source_index_preview'; -import { PivotPreview } from './pivot_preview'; import { KqlFilterBar } from '../../../../../shared_imports'; import { SwitchModal } from './switch_modal'; @@ -899,7 +899,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index f8fb9db9bd686..00948109c811d 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -18,12 +18,12 @@ import { } from '@elastic/eui'; import { getPivotQuery } from '../../../../common'; +import { PivotPreview } from '../../../../components/pivot_preview'; import { SearchItems } from '../../../../hooks/use_search_items'; import { AggListSummary } from '../aggregation_list'; import { GroupByListSummary } from '../group_by_list'; -import { PivotPreview } from './pivot_preview'; import { StepDefineExposedState } from './step_define_form'; const defaultSearch = '*'; @@ -134,7 +134,7 @@ export const StepDefineSummary: FC = ({ diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index 0e9b531e1feaf..eaaedc2eb77ce 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -4,218 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import moment from 'moment-timezone'; -import { Direction } from '@elastic/eui'; -import { SortDirection, SORT_DIRECTION, FieldDataColumnType } from '../../../../../shared_imports'; +import React, { FC } from 'react'; -import { useApi } from '../../../../hooks/use_api'; +import { SearchItems } from '../../../../hooks/use_search_items'; -import { - getFlattenedFields, - useRefreshTransformList, - EsDoc, - PreviewRequestBody, - TransformPivotConfig, -} from '../../../../common'; -import { ES_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/public'; -import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; -import { transformTableFactory } from './transform_table'; +import { getPivotQuery, TransformPivotConfig } from '../../../../common'; -const TransformTable = transformTableFactory(); +import { + applyTransformConfigToDefineState, + getDefaultStepDefineState, +} from '../../../create_transform/components/step_define/'; +import { PivotPreview } from '../../../../components/pivot_preview'; interface Props { transformConfig: TransformPivotConfig; } -export function sortColumns(groupByArr: string[]) { - return (a: string, b: string) => { - // make sure groupBy fields are always most left columns - if (groupByArr.some(aggName => aggName === a) && groupByArr.some(aggName => aggName === b)) { - return a.localeCompare(b); - } - if (groupByArr.some(aggName => aggName === a)) { - return -1; - } - if (groupByArr.some(aggName => aggName === b)) { - return 1; - } - return a.localeCompare(b); - }; -} - -function getDataFromTransform( - transformConfig: TransformPivotConfig -): { previewRequest: PreviewRequestBody; groupByArr: string[] | [] } { - const index = transformConfig.source.index; - const query = transformConfig.source.query; - const pivot = transformConfig.pivot; - const groupByArr = []; - - const previewRequest: PreviewRequestBody = { - source: { - index, - query, - }, - pivot, - }; - // hasOwnProperty check to ensure only properties on object itself, and not its prototypes - if (pivot.group_by !== undefined) { - for (const key in pivot.group_by) { - if (pivot.group_by.hasOwnProperty(key)) { - groupByArr.push(key); - } - } - } - - return { groupByArr, previewRequest }; -} - export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { - const [previewData, setPreviewData] = useState([]); - const [columns, setColumns] = useState> | []>([]); - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(10); - const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); - const [sortField, setSortField] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [errorMessage, setErrorMessage] = useState(''); - const api = useApi(); - - const getPreviewFactory = () => { - let concurrentLoads = 0; - - return async function getPreview() { - try { - concurrentLoads++; - - if (concurrentLoads > 1) { - return; - } - - const { previewRequest, groupByArr } = getDataFromTransform(transformConfig); - setIsLoading(true); - const resp: any = await api.getTransformsPreview(previewRequest); - setIsLoading(false); - - if (resp.preview.length > 0) { - const columnKeys = getFlattenedFields(resp.preview[0]); - columnKeys.sort(sortColumns(groupByArr)); - - const tableColumns: Array> = columnKeys.map(k => { - const column: FieldDataColumnType = { - field: k, - name: k, - sortable: true, - truncateText: true, - }; - - if (typeof resp.mappings.properties[k] !== 'undefined') { - const esFieldType = resp.mappings.properties[k].type; - switch (esFieldType) { - case ES_FIELD_TYPES.BOOLEAN: - column.dataType = 'boolean'; - break; - case ES_FIELD_TYPES.DATE: - column.align = 'right'; - column.render = (d: any) => - formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); - break; - case ES_FIELD_TYPES.BYTE: - case ES_FIELD_TYPES.DOUBLE: - case ES_FIELD_TYPES.FLOAT: - case ES_FIELD_TYPES.HALF_FLOAT: - case ES_FIELD_TYPES.INTEGER: - case ES_FIELD_TYPES.LONG: - case ES_FIELD_TYPES.SCALED_FLOAT: - case ES_FIELD_TYPES.SHORT: - column.dataType = 'number'; - break; - case ES_FIELD_TYPES.KEYWORD: - case ES_FIELD_TYPES.TEXT: - column.dataType = 'string'; - break; - } - } - - return column; - }); - - setPreviewData(resp.preview); - setColumns(tableColumns); - setSortField(sortField); - setSortDirection(sortDirection); - } - concurrentLoads--; - - if (concurrentLoads > 0) { - concurrentLoads = 0; - getPreview(); - } - } catch (error) { - setIsLoading(false); - setErrorMessage( - i18n.translate('xpack.transform.transformList.stepDetails.previewPane.errorMessage', { - defaultMessage: 'Preview could not be loaded', - }) - ); - } - }; - }; - - useRefreshTransformList({ onRefresh: getPreviewFactory() }); - - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: previewData.length, - pageSizeOptions: [10, 20], - hidePerPageOptions: false, - }; - - const sorting = { - sort: { - field: sortField as string, - direction: sortDirection, - }, - }; - - const onTableChange = ({ - page = { index: 0, size: 10 }, - sort = { field: columns[0].field, direction: SORT_DIRECTION.ASC }, - }: { - page?: { index: number; size: number }; - sort?: { field: keyof typeof previewData[number]; direction: SortDirection | Direction }; - }) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); - - const { field, direction } = sort; - setSortField(field); - setSortDirection(direction); - }; + const previewConfig = applyTransformConfigToDefineState( + getDefaultStepDefineState({} as SearchItems), + transformConfig + ); - const transformTableLoading = previewData.length === 0 && isLoading === true; - const dataTestSubj = `transformPreviewTabContent${!transformTableLoading ? ' loaded' : ''}`; + const indexPatternTitle = Array.isArray(transformConfig.source.index) + ? transformConfig.source.index.join(',') + : transformConfig.source.index; return ( -
- ({ - 'data-test-subj': 'transformPreviewTabContentRow', - })} - sorting={sorting} - error={errorMessage} - /> -
+ ); }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5635bb19b7e83..b64caea05dbe4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12500,7 +12500,6 @@ "xpack.transform.transformList.startModalTitle": "{transformId} を開始", "xpack.transform.transformList.startTransformErrorMessage": "変換 {transformId} の開始中にエラーが発生しました", "xpack.transform.transformList.startTransformSuccessMessage": "変換 {transformId} の開始リクエストが受け付けられました。", - "xpack.transform.transformList.stepDetails.previewPane.errorMessage": "プレビューを読み込めませんでした", "xpack.transform.transformList.stopActionName": "停止", "xpack.transform.transformList.stoppedTransformBulkToolTip": "1 つまたは複数の変換が既に開始済みです。", "xpack.transform.transformList.stoppedTransformToolTip": "{transformId} は既に停止済みです。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0523021046167..872ec3b71ec10 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12500,7 +12500,6 @@ "xpack.transform.transformList.startModalTitle": "启动 {transformId}", "xpack.transform.transformList.startTransformErrorMessage": "启动转换 {transformId} 时发生错误", "xpack.transform.transformList.startTransformSuccessMessage": "启动转换 {transformId} 的请求已确认。", - "xpack.transform.transformList.stepDetails.previewPane.errorMessage": "无法加载预览", "xpack.transform.transformList.stopActionName": "停止", "xpack.transform.transformList.stoppedTransformBulkToolTip": "一个或多个选定数据帧转换已停止。", "xpack.transform.transformList.stoppedTransformToolTip": "{transformId} 已停止。", diff --git a/x-pack/test/functional/services/transform_ui/transform_table.ts b/x-pack/test/functional/services/transform_ui/transform_table.ts index ebd7fe527b45f..858524eb2e1fd 100644 --- a/x-pack/test/functional/services/transform_ui/transform_table.ts +++ b/x-pack/test/functional/services/transform_ui/transform_table.ts @@ -61,17 +61,17 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { return rows; } - async parseEuiInMemoryTable(tableSubj: string) { + async parseEuiDataGrid(tableSubj: string) { const table = await testSubjects.find(`~${tableSubj}`); const $ = await table.parseDomContent(); const rows = []; // For each row, get the content of each cell and // add its values as an array to each row. - for (const tr of $.findTestSubjects(`~${tableSubj}Row`).toArray()) { + for (const tr of $.findTestSubjects(`~dataGridRow`).toArray()) { rows.push( $(tr) - .find('.euiTableCellContent') + .find('.euiDataGridRowCell__truncate') .toArray() .map(cell => $(cell) @@ -84,14 +84,14 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { return rows; } - async assertEuiInMemoryTableColumnValues( + async assertEuiDataGridColumnValues( tableSubj: string, column: number, expectedColumnValues: string[] ) { await retry.tryForTime(2000, async () => { // get a 2D array of rows and cell values - const rows = await this.parseEuiInMemoryTable(tableSubj); + const rows = await this.parseEuiDataGrid(tableSubj); // reduce the rows data to an array of unique values in the specified column const uniqueColumnValues = rows @@ -148,17 +148,17 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('transformPreviewTab'); await testSubjects.click('transformPreviewTab'); - await testSubjects.existOrFail('~transformPreviewTabContent'); + await testSubjects.existOrFail('~transformPivotPreview'); } public async waitForTransformsExpandedRowPreviewTabToLoad() { - await testSubjects.existOrFail('~transformPreviewTabContent', { timeout: 60 * 1000 }); - await testSubjects.existOrFail('transformPreviewTabContent loaded', { timeout: 30 * 1000 }); + await testSubjects.existOrFail('~transformPivotPreview', { timeout: 60 * 1000 }); + await testSubjects.existOrFail('transformPivotPreview loaded', { timeout: 30 * 1000 }); } async assertTransformsExpandedRowPreviewColumnValues(column: number, values: string[]) { await this.waitForTransformsExpandedRowPreviewTabToLoad(); - await this.assertEuiInMemoryTableColumnValues('transformPreviewTabContent', column, values); + await this.assertEuiDataGridColumnValues('transformPivotPreview', column, values); } })(); }