diff --git a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.ts b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.ts index 6ebec4b118c7..8a4d16ae3bfb 100644 --- a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.ts +++ b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.ts @@ -17,7 +17,10 @@ import { EXPRESSION_HEATMAP_LEGEND_NAME, } from '../constants'; -const convertToVisDimension = (columns: DatatableColumn[], accessor: string) => { +const convertToVisDimension = ( + columns: DatatableColumn[], + accessor: string +): ExpressionValueVisDimension | undefined => { const column = columns.find((c) => c.id === accessor); if (!column) return; return { @@ -27,7 +30,7 @@ const convertToVisDimension = (columns: DatatableColumn[], accessor: string) => params: { ...column.meta.params?.params }, }, type: 'vis_dimension', - } as ExpressionValueVisDimension; + }; }; const prepareHeatmapLogTable = ( @@ -70,12 +73,14 @@ export const heatmapFunction = (): HeatmapExpressionFunctionDefinition => ({ help: i18n.translate('expressionHeatmap.function.legendConfig.help', { defaultMessage: 'Configure the chart legend.', }), + default: `{${EXPRESSION_HEATMAP_LEGEND_NAME}}`, }, gridConfig: { types: [EXPRESSION_HEATMAP_GRID_NAME], help: i18n.translate('expressionHeatmap.function.gridConfig.help', { defaultMessage: 'Configure the heatmap layout.', }), + default: `{${EXPRESSION_HEATMAP_GRID_NAME}}`, }, showTooltip: { types: ['boolean'], @@ -118,6 +123,7 @@ export const heatmapFunction = (): HeatmapExpressionFunctionDefinition => ({ help: i18n.translate('expressionHeatmap.function.args.valueAccessorHelpText', { defaultMessage: 'The id of the value column or the corresponding dimension', }), + required: true, }, // not supported yet, small multiples accessor splitRowAccessor: { diff --git a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_grid.ts b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_grid.ts index 17513555d394..7ade1793f1f6 100644 --- a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_grid.ts +++ b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_grid.ts @@ -20,7 +20,7 @@ export const heatmapGridConfig: ExpressionFunctionDefinition< name: EXPRESSION_HEATMAP_GRID_NAME, aliases: [], type: EXPRESSION_HEATMAP_GRID_NAME, - help: `Configure the heatmap layout `, + help: `Configure the heatmap layout`, inputTypes: ['null'], args: { // grid @@ -38,20 +38,6 @@ export const heatmapGridConfig: ExpressionFunctionDefinition< }), required: false, }, - cellHeight: { - types: ['number'], - help: i18n.translate('expressionHeatmap.function.args.grid.cellHeight.help', { - defaultMessage: 'Specifies the grid cell height', - }), - required: false, - }, - cellWidth: { - types: ['number'], - help: i18n.translate('expressionHeatmap.function.args.grid.cellWidth.help', { - defaultMessage: 'Specifies the grid cell width', - }), - required: false, - }, // cells isCellLabelVisible: { types: ['boolean'], @@ -66,20 +52,6 @@ export const heatmapGridConfig: ExpressionFunctionDefinition< defaultMessage: 'Specifies whether or not the Y-axis labels are visible.', }), }, - yAxisLabelWidth: { - types: ['number'], - help: i18n.translate('expressionHeatmap.function.args.grid.yAxisLabelWidth.help', { - defaultMessage: 'Specifies the width of the Y-axis labels.', - }), - required: false, - }, - yAxisLabelColor: { - types: ['string'], - help: i18n.translate('expressionHeatmap.function.args.grid.yAxisLabelColor.help', { - defaultMessage: 'Specifies the color of the Y-axis labels.', - }), - required: false, - }, // X-axis isXAxisLabelVisible: { types: ['boolean'], diff --git a/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts index a983da669c56..b99f7c13bc20 100644 --- a/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts @@ -48,14 +48,10 @@ export interface HeatmapGridConfig { // grid strokeWidth?: number; strokeColor?: string; - cellHeight?: number; - cellWidth?: number; // cells isCellLabelVisible: boolean; // Y-axis isYAxisLabelVisible: boolean; - yAxisLabelWidth?: number; - yAxisLabelColor?: string; // X-axis isXAxisLabelVisible: boolean; } diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx index 9f99c5f6d44b..f1fbbccc3c20 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx +++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx @@ -258,7 +258,7 @@ export const HeatmapComponent: FC = memo( const percentageNumber = (Math.abs(value - min) / (max - min)) * 100; value = parseInt(percentageNumber.toString(), 10) / 100; } - return metricFormatter.convert(value); + return `${metricFormatter.convert(value) ?? ''}`; }; const { colors, ranges } = computeColorRanges( @@ -415,7 +415,8 @@ export const HeatmapComponent: FC = memo( name: yAxisColumn?.name ?? '', ...(yAxisColumn ? { - formatter: (v: number | string) => formatFactory(yAxisColumn.meta.params).convert(v), + formatter: (v: number | string) => + `${formatFactory(yAxisColumn.meta.params).convert(v) ?? ''}`, } : {}), }, @@ -424,7 +425,7 @@ export const HeatmapComponent: FC = memo( // eui color subdued textColor: chartTheme.axes?.tickLabel?.fill ?? `#6a717d`, padding: xAxisColumn?.name ? 8 : 0, - formatter: (v: number | string) => xValuesFormatter.convert(v), + formatter: (v: number | string) => `${xValuesFormatter.convert(v) ?? ''}`, name: xAxisColumn?.name ?? '', }, brushMask: { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/heatmap/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/heatmap/index.ts new file mode 100644 index 000000000000..ebb2b1c1edd1 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/heatmap/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ElementFactory } from '../../../types'; + +export const heatmap: ElementFactory = () => ({ + name: 'heatmap', + displayName: 'Heatmap', + type: 'chart', + help: 'Heatmap visualization', + icon: 'heatmap', + expression: `filters +| demodata +| head 10 +| heatmap xAccessor={visdimension "age"} yAccessor={visdimension "project"} valueAccessor={visdimension "cost"} +| render`, +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts index b73957b50019..f4383bb3e325 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts @@ -33,6 +33,7 @@ import { verticalProgressBar } from './vertical_progress_bar'; import { verticalProgressPill } from './vertical_progress_pill'; import { tagCloud } from './tag_cloud'; import { metricVis } from './metric_vis'; +import { heatmap } from './heatmap'; import { SetupInitializer } from '../plugin'; import { ElementFactory } from '../../types'; @@ -63,6 +64,7 @@ const elementSpecs = [ verticalProgressBar, verticalProgressPill, tagCloud, + heatmap, ]; const initializeElementFactories = [metricElementInitializer]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/color_picker/color_picker.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/color_picker/color_picker.tsx new file mode 100644 index 000000000000..70a9d0dd2ed9 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/color_picker/color_picker.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiColorPicker, + EuiFlexGroup, + EuiFlexItem, + EuiSetColorMethod, + useColorPickerState, +} from '@elastic/eui'; +import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; +import { withDebounceArg } from '../../../../public/components/with_debounce_arg'; +import { ArgumentStrings } from '../../../../i18n'; + +const { Color: strings } = ArgumentStrings; + +interface Props { + onValueChange: (value: string) => void; + argValue: string; +} + +const ColorPicker: FC = ({ onValueChange, argValue }) => { + const [color, setColor, errors] = useColorPickerState(argValue); + + const pickColor: EuiSetColorMethod = (value, meta) => { + setColor(value, meta); + onValueChange(value); + }; + + return ( + + + + + + ); +}; + +ColorPicker.propTypes = { + argValue: PropTypes.any.isRequired, + onValueChange: PropTypes.func.isRequired, +}; + +export const colorPicker = () => ({ + name: 'color_picker', + displayName: strings.getDisplayName(), + help: strings.getHelp(), + simpleTemplate: templateFromReactComponent(withDebounceArg(ColorPicker)), + default: '"#000"', +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/color_picker/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/color_picker/index.ts new file mode 100644 index 000000000000..e3ad6cf7972b --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/color_picker/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { colorPicker } from './color_picker'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js index f300957c3952..987fd11c5fae 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js @@ -39,7 +39,7 @@ const getMathValue = (argValue, columns) => { // TODO: Garbage, we could make a much nicer math form that can handle way more. const DatacolumnArgInput = ({ onValueChange, - columns, + resolved: { columns }, argValue, renderError, argId, @@ -123,7 +123,9 @@ const DatacolumnArgInput = ({ }; DatacolumnArgInput.propTypes = { - columns: PropTypes.array.isRequired, + resolved: PropTypes.shape({ + columns: PropTypes.array.isRequired, + }).isRequired, onValueChange: PropTypes.func.isRequired, typeInstance: PropTypes.object.isRequired, renderError: PropTypes.func.isRequired, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts index c6a220062227..40e98d18a706 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts @@ -32,6 +32,7 @@ import { textarea } from './textarea'; // @ts-expect-error untyped local import { toggle } from './toggle'; import { visdimension } from './vis_dimension'; +import { colorPicker } from './color_picker'; import { SetupInitializer } from '../../plugin'; @@ -51,6 +52,7 @@ export const args = [ textarea, toggle, visdimension, + colorPicker, ]; export const initializers = [dateFormatInitializer, numberFormatInitializer]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number.js index cf360c5d648a..2dfe1b1b6797 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number.js @@ -9,6 +9,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { EuiFieldNumber, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { withDebounceArg } from '../../../public/components/with_debounce_arg'; import { ArgumentStrings } from '../../../i18n'; const { Number: strings } = ArgumentStrings; @@ -28,8 +29,11 @@ const NumberArgInput = ({ argId, argValue, typeInstance, onValueChange }) => { const onChange = useCallback( (ev) => { - const onChangeFn = confirm ? setValue : onValueChange; - onChangeFn(ev.target.value); + const { value } = ev.target; + setValue(value); + if (!confirm) { + onValueChange(value); + } }, [confirm, onValueChange] ); @@ -62,6 +66,6 @@ export const number = () => ({ name: 'number', displayName: strings.getDisplayName(), help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(NumberArgInput), + simpleTemplate: templateFromReactComponent(withDebounceArg(NumberArgInput)), default: '0', }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/percentage.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/percentage.js index aa9d510d32ad..230d6d4a3899 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/percentage.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/percentage.js @@ -5,18 +5,27 @@ * 2.0. */ -import React from 'react'; +import React, { useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import { EuiRange } from '@elastic/eui'; +import { withDebounceArg } from '../../../public/components/with_debounce_arg'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; import { ArgumentStrings } from '../../../i18n'; const { Percentage: strings } = ArgumentStrings; const PercentageArgInput = ({ onValueChange, argValue }) => { - const handleChange = (ev) => { - return onValueChange(ev.target.value / 100); - }; + const [value, setValue] = useState(argValue); + + const handleChange = useCallback( + (ev) => { + const { value } = ev.target; + const numberVal = Number(value) / 100; + setValue(numberVal); + onValueChange(numberVal); + }, + [onValueChange] + ); return ( { max={100} showLabels showInput - value={argValue * 100} + value={value * 100} onChange={handleChange} /> ); @@ -41,5 +50,5 @@ export const percentage = () => ({ name: 'percentage', displayName: strings.getDisplayName(), help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(PercentageArgInput), + simpleTemplate: templateFromReactComponent(withDebounceArg(PercentageArgInput, 50)), }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/range.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/range.js index e178191764ae..a8ea45d672aa 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/range.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/range.js @@ -5,19 +5,28 @@ * 2.0. */ -import React from 'react'; +import React, { useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import { EuiRange } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { withDebounceArg } from '../../../public/components/with_debounce_arg'; import { ArgumentStrings } from '../../../i18n'; const { Range: strings } = ArgumentStrings; const RangeArgInput = ({ typeInstance, onValueChange, argValue }) => { const { min, max, step } = typeInstance.options; - const handleChange = (ev) => { - return onValueChange(Number(ev.target.value)); - }; + const [value, setValue] = useState(argValue); + + const handleChange = useCallback( + (ev) => { + const { value } = ev.target; + const numberVal = Number(value); + setValue(numberVal); + onValueChange(numberVal); + }, + [onValueChange] + ); return ( { step={step} showLabels showInput - value={argValue} + value={value} onChange={handleChange} /> ); @@ -50,5 +59,5 @@ export const range = () => ({ name: 'range', displayName: strings.getDisplayName(), help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(RangeArgInput), + simpleTemplate: templateFromReactComponent(withDebounceArg(RangeArgInput, 50)), }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/string.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/string.js index e5b9ab049a3b..cba96fd42e9e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/string.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/string.js @@ -9,6 +9,8 @@ import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexItem, EuiFlexGroup, EuiFieldText, EuiButton } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { withDebounceArg } from '../../../public/components/with_debounce_arg'; + import { ArgumentStrings } from '../../../i18n'; const { String: strings } = ArgumentStrings; @@ -23,8 +25,11 @@ const StringArgInput = ({ argValue, typeInstance, onValueChange, argId }) => { const onChange = useCallback( (ev) => { - const onChangeFn = confirm ? setValue : onValueChange; - onChangeFn(ev.target.value); + const { value } = ev.target; + setValue(value); + if (!confirm) { + onValueChange(value); + } }, [confirm, onValueChange] ); @@ -56,5 +61,5 @@ export const string = () => ({ name: 'string', displayName: strings.getDisplayName(), help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(StringArgInput), + simpleTemplate: templateFromReactComponent(withDebounceArg(StringArgInput)), }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/textarea.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/textarea.js index a39b151a11f0..8cc301aa5ed9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/textarea.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/textarea.js @@ -9,18 +9,22 @@ import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { EuiFormRow, EuiTextArea, EuiSpacer, EuiButton } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { withDebounceArg } from '../../../public/components/with_debounce_arg'; import { ArgumentStrings } from '../../../i18n'; const { Textarea: strings } = ArgumentStrings; const TextAreaArgInput = ({ argValue, typeInstance, onValueChange, renderError, argId }) => { const confirm = typeInstance?.options?.confirm; - const [value, setValue] = useState(); + const [value, setValue] = useState(argValue); const onChange = useCallback( (ev) => { - const onChangeFn = confirm ? setValue : onValueChange; - onChangeFn(ev.target.value); + const { value } = ev.target; + setValue(value); + if (!confirm) { + onValueChange(value); + } }, [confirm, onValueChange] ); @@ -68,5 +72,5 @@ export const textarea = () => ({ name: 'textarea', displayName: strings.getDisplayName(), help: strings.getHelp(), - template: templateFromReactComponent(TextAreaArgInput), + template: templateFromReactComponent(withDebounceArg(TextAreaArgInput)), }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/vis_dimension.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/vis_dimension.tsx index 312457b658ad..94831be2e003 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/vis_dimension.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/vis_dimension.tsx @@ -10,27 +10,25 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; import { DatatableColumn, ExpressionAstExpression } from 'src/plugins/expressions'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; import { ArgumentStrings } from '../../../i18n'; +import { ResolvedArgProps, ResolvedColumns } from '../../../public/expression_types/arg'; const { VisDimension: strings } = ArgumentStrings; -interface VisDimensionArgInputProps { +type VisDimensionArgInputProps = { onValueChange: (value: ExpressionAstExpression) => void; argValue: ExpressionAstExpression; - argId?: string; - columns: DatatableColumn[]; typeInstance: { options?: { confirm?: string; }; }; -} +} & ResolvedArgProps; const VisDimensionArgInput: React.FC = ({ argValue, typeInstance, onValueChange, - argId, - columns, + resolved: { columns }, }) => { const [value, setValue] = useState(argValue); const confirm = typeInstance?.options?.confirm; @@ -75,7 +73,7 @@ const VisDimensionArgInput: React.FC = ({ return ( - + {confirm && ( diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/heatmap_grid.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/heatmap_grid.ts new file mode 100644 index 000000000000..ed8c86b4b0f3 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/heatmap_grid.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get } from 'lodash'; +import { getState, getValue } from '../../../public/lib/resolved_arg'; +import { ModelStrings } from '../../../i18n'; +import { ResolvedColumns } from '../../../public/expression_types/arg'; + +const { HeatmapGrid: strings } = ModelStrings; + +export const heatmapGrid = () => ({ + name: 'heatmap_grid', + displayName: strings.getDisplayName(), + args: [ + { + name: 'strokeWidth', + displayName: strings.getStrokeWidthDisplayName(), + help: strings.getStrokeWidthHelp(), + argType: 'number', + }, + { + name: 'strokeColor', + displayName: strings.getStrokeColorDisplayName(), + help: strings.getStrokeColorDisplayName(), + argType: 'color_picker', + }, + { + name: 'isCellLabelVisible', + displayName: strings.getIsCellLabelVisibleDisplayName(), + help: strings.getIsCellLabelVisibleHelp(), + argType: 'toggle', + }, + { + name: 'isYAxisLabelVisible', + displayName: strings.getIsYAxisLabelVisibleDisplayName(), + help: strings.getIsYAxisLabelVisibleHelp(), + argType: 'toggle', + }, + { + name: 'isXAxisLabelVisible', + displayName: strings.getIsXAxisLabelVisibleDisplayName(), + help: strings.getIsXAxisLabelVisibleHelp(), + argType: 'toggle', + }, + ], + resolve({ context }: any): ResolvedColumns { + if (getState(context) !== 'ready') { + return { columns: [] }; + } + return { columns: get(getValue(context), 'columns', []) }; + }, +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/heatmap_legend.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/heatmap_legend.ts new file mode 100644 index 000000000000..44f81435b492 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/heatmap_legend.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get } from 'lodash'; +import { getState, getValue } from '../../../public/lib/resolved_arg'; +import { ModelStrings } from '../../../i18n'; +import { ResolvedColumns } from '../../../public/expression_types/arg'; + +const { HeatmapLegend: strings } = ModelStrings; + +export const heatmapLegend = () => ({ + name: 'heatmap_legend', + displayName: strings.getDisplayName(), + args: [ + { + name: 'isVisible', + displayName: strings.getIsVisibleDisplayName(), + help: strings.getIsVisibleHelp(), + argType: 'toggle', + default: true, + }, + { + name: 'position', + displayName: strings.getPositionDisplayName(), + help: strings.getPositionHelp(), + argType: 'select', + default: 'right', + options: { + choices: [ + { value: 'top', name: strings.getPositionTopOption() }, + { value: 'right', name: strings.getPositionRightOption() }, + { value: 'bottom', name: strings.getPositionBottomOption() }, + { value: 'left', name: strings.getPositionLeftOption() }, + ], + }, + }, + { + name: 'maxLines', + displayName: strings.getMaxLinesDisplayName(), + help: strings.getMaxLinesHelp(), + argType: 'number', + default: 10, + }, + { + name: 'shouldTruncate', + displayName: strings.getShouldTruncateDisplayName(), + help: strings.getShouldTruncateHelp(), + argType: 'toggle', + }, + ], + resolve({ context }: any): ResolvedColumns { + if (getState(context) !== 'ready') { + return { columns: [] }; + } + return { columns: get(getValue(context), 'columns', []) }; + }, +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/index.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/index.js index 8fadc9e2e6c8..82a0fbfbb3a4 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/index.js @@ -9,5 +9,7 @@ import { pointseries } from './point_series'; import { math } from './math'; import { tagcloud } from './tagcloud'; import { metricVis } from './metric_vis'; +import { heatmapLegend } from './heatmap_legend'; +import { heatmapGrid } from './heatmap_grid'; -export const modelSpecs = [pointseries, math, tagcloud, metricVis]; +export const modelSpecs = [pointseries, math, tagcloud, metricVis, heatmapLegend, heatmapGrid]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/metric_vis.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/metric_vis.ts index 9796c4553978..403522399e36 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/metric_vis.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/metric_vis.ts @@ -6,6 +6,7 @@ */ import { get } from 'lodash'; +import { ResolvedColumns } from '../../../public/expression_types/arg'; import { ViewStrings } from '../../../i18n'; import { getState, getValue } from '../../../public/lib/resolved_arg'; @@ -70,7 +71,7 @@ export const metricVis = () => ({ argType: 'toggle', }, ], - resolve({ context }: any) { + resolve({ context }: any): ResolvedColumns { if (getState(context) !== 'ready') { return { columns: [] }; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/tagcloud.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/tagcloud.ts index 11cad3461c02..d210d5dee1e6 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/tagcloud.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/tagcloud.ts @@ -6,6 +6,7 @@ */ import { get } from 'lodash'; +import { ResolvedColumns } from '../../../public/expression_types/arg'; import { ViewStrings } from '../../../i18n'; import { getState, getValue } from '../../../public/lib/resolved_arg'; @@ -82,7 +83,7 @@ export const tagcloud = () => ({ default: true, }, ], - resolve({ context }: any) { + resolve({ context }: any): ResolvedColumns { if (getState(context) !== 'ready') { return { columns: [] }; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/heatmap.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/heatmap.ts new file mode 100644 index 000000000000..dba1165377b7 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/heatmap.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get } from 'lodash'; +import { ResolvedColumns } from '../../../public/expression_types/arg'; + +import { ViewStrings } from '../../../i18n'; +import { getState, getValue } from '../../../public/lib/resolved_arg'; + +const { Heatmap: strings } = ViewStrings; + +export const heatmap = () => ({ + name: 'heatmap', + displayName: strings.getDisplayName(), + args: [ + { + name: 'xAccessor', + displayName: strings.getXAccessorDisplayName(), + help: strings.getXAccessorHelp(), + argType: 'vis_dimension', + default: `{visdimension}`, + }, + { + name: 'yAccessor', + displayName: strings.getYAccessorDisplayName(), + help: strings.getYAccessorHelp(), + argType: 'vis_dimension', + default: `{visdimension}`, + }, + { + name: 'valueAccessor', + displayName: strings.getValueAccessorDisplayName(), + help: strings.getValueAccessorHelp(), + argType: 'vis_dimension', + default: `{visdimension}`, + }, + { + name: 'splitRowAccessor', + displayName: strings.getSplitRowAccessorDisplayName(), + help: strings.getSplitRowAccessorHelp(), + argType: 'vis_dimension', + default: `{visdimension}`, + }, + { + name: 'splitColumnAccessor', + displayName: strings.getSplitColumnAccessorDisplayName(), + help: strings.getSplitColumnAccessorHelp(), + argType: 'vis_dimension', + default: `{visdimension}`, + }, + { + name: 'showTooltip', + displayName: strings.getShowTooltipDisplayName(), + help: strings.getShowTooltipHelp(), + argType: 'toggle', + default: true, + }, + { + name: 'highlightInHover', + displayName: strings.getHighlightInHoverDisplayName(), + help: strings.getHighlightInHoverHelp(), + argType: 'toggle', + }, + { + name: 'lastRangeIsRightOpen', + displayName: strings.getLastRangeIsRightOpenDisplayName(), + help: strings.getLastRangeIsRightOpenHelp(), + argType: 'toggle', + default: true, + }, + { + name: 'palette', + argType: 'stops_palette', + }, + { + name: 'legend', + displayName: strings.getLegendDisplayName(), + help: strings.getLegendHelp(), + type: 'model', + argType: 'heatmap_legend', + }, + { + name: 'gridConfig', + displayName: strings.getGridConfigDisplayName(), + help: strings.getGridConfigHelp(), + type: 'model', + argType: 'heatmap_grid', + }, + ], + resolve({ context }: any): ResolvedColumns { + if (getState(context) !== 'ready') { + return { columns: [] }; + } + return { columns: get(getValue(context), 'columns', []) }; + }, +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/index.ts index 74ad90964823..faa277224839 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/index.ts @@ -32,6 +32,8 @@ import { shape } from './shape'; import { table } from './table'; // @ts-expect-error untyped local import { timefilterControl } from './timefilterControl'; +import { heatmap } from './heatmap'; + import { SetupInitializer } from '../../plugin'; export const viewSpecs = [ @@ -48,6 +50,7 @@ export const viewSpecs = [ shape, table, timefilterControl, + heatmap, ]; export const viewInitializers = [metricInitializer]; diff --git a/x-pack/plugins/canvas/i18n/elements/element_strings.ts b/x-pack/plugins/canvas/i18n/elements/element_strings.ts index c97dd1b434d5..a1497707a14a 100644 --- a/x-pack/plugins/canvas/i18n/elements/element_strings.ts +++ b/x-pack/plugins/canvas/i18n/elements/element_strings.ts @@ -238,4 +238,12 @@ export const getElementStrings = (): ElementStringDict => ({ defaultMessage: 'Metric visualization', }), }, + heatmap: { + displayName: i18n.translate('xpack.canvas.elements.heatmapDisplayName', { + defaultMessage: 'Heatmap', + }), + help: i18n.translate('xpack.canvas.elements.heatmapHelpText', { + defaultMessage: 'Heatmap visualization', + }), + }, }); diff --git a/x-pack/plugins/canvas/i18n/ui.ts b/x-pack/plugins/canvas/i18n/ui.ts index 4856de96885e..8029688d8a1c 100644 --- a/x-pack/plugins/canvas/i18n/ui.ts +++ b/x-pack/plugins/canvas/i18n/ui.ts @@ -244,6 +244,16 @@ export const ArgumentStrings = { defaultMessage: 'Custom', }), }, + Color: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.colorTitle', { + defaultMessage: 'Color', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.colorLabel', { + defaultMessage: 'Color picker', + }), + }, Percentage: { getDisplayName: () => i18n.translate('xpack.canvas.uis.arguments.percentageTitle', { @@ -591,6 +601,106 @@ export const ModelStrings = { defaultMessage: 'Data along the vertical axis. Usually a number', }), }, + HeatmapLegend: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.title', { + defaultMessage: "Configure the heatmap chart's legend", + }), + getIsVisibleDisplayName: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.args.isVisibleTitle', { + defaultMessage: 'Show legend', + }), + getIsVisibleHelp: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.args.isVisibleLabel', { + defaultMessage: 'Specifies whether or not the legend is visible', + }), + getPositionDisplayName: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.args.positionTitle', { + defaultMessage: 'Legend Position', + }), + getPositionHelp: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.args.positionLabel', { + defaultMessage: 'Specifies the legend position.', + }), + getPositionTopOption: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.args.positionTopLabel', { + defaultMessage: 'Top', + }), + getPositionBottomOption: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.args.positionBottomLabel', { + defaultMessage: 'Bottom', + }), + getPositionLeftOption: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.args.positionLeftLabel', { + defaultMessage: 'Left', + }), + getPositionRightOption: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.args.positionRightLabel', { + defaultMessage: 'Right', + }), + getMaxLinesDisplayName: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.args.maxLinesTitle', { + defaultMessage: 'Legend maximum lines', + }), + getMaxLinesHelp: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.args.maxLinesLabel', { + defaultMessage: 'Specifies the number of lines per legend item.', + }), + getShouldTruncateDisplayName: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.args.shouldTruncateTitle', { + defaultMessage: 'Truncate label', + }), + getShouldTruncateHelp: () => + i18n.translate('xpack.canvas.uis.models.heatmap_legend.args.shouldTruncateLabel', { + defaultMessage: 'Specifies whether or not the legend items should be truncated', + }), + }, + HeatmapGrid: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.models.heatmap_grid.title', { + defaultMessage: 'Configure the heatmap layout', + }), + getStrokeWidthDisplayName: () => + i18n.translate('xpack.canvas.uis.models.heatmap_grid.args.strokeWidthTitle', { + defaultMessage: 'Stroke width', + }), + getStrokeWidthHelp: () => + i18n.translate('xpack.canvas.uis.models.heatmap_grid.args.strokeWidthLabel', { + defaultMessage: 'Specifies the grid stroke width', + }), + getStrokeColorDisplayName: () => + i18n.translate('xpack.canvas.uis.models.heatmap_grid.args.strokeColorTitle', { + defaultMessage: 'Stroke color', + }), + getStrokeColorHelp: () => + i18n.translate('xpack.canvas.uis.models.heatmap_grid.args.strokeColorLabel', { + defaultMessage: 'Specifies the grid stroke color', + }), + getIsCellLabelVisibleDisplayName: () => + i18n.translate('xpack.canvas.uis.models.heatmap_grid.args.isCellLabelVisibleTitle', { + defaultMessage: 'Show cell label', + }), + getIsCellLabelVisibleHelp: () => + i18n.translate('xpack.canvas.uis.models.heatmap_grid.args.isCellLabelVisibleLabel', { + defaultMessage: 'Specifies whether or not the cell label is visible', + }), + getIsYAxisLabelVisibleDisplayName: () => + i18n.translate('xpack.canvas.uis.models.heatmap_grid.args.isYAxisLabelVisibleTile', { + defaultMessage: 'Show Y-axis labels', + }), + getIsYAxisLabelVisibleHelp: () => + i18n.translate('xpack.canvas.uis.models.heatmap_grid.args.isYAxisLabelVisibleLabel', { + defaultMessage: 'Specifies whether or not the Y-axis labels are visible', + }), + getIsXAxisLabelVisibleDisplayName: () => + i18n.translate('xpack.canvas.uis.models.heatmap_grid.args.isXAxisLabelVisibleTile', { + defaultMessage: 'Show X-axis labels', + }), + getIsXAxisLabelVisibleHelp: () => + i18n.translate('xpack.canvas.uis.models.heatmap_grid.args.isXAxisLabelVisibleLabel', { + defaultMessage: 'Specifies whether or not the X-axis labels are visible', + }), + }, }; export const TransformStrings = { @@ -1349,4 +1459,91 @@ export const ViewStrings = { defaultMessage: 'Background', }), }, + Heatmap: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.views.heatmapTitle', { + defaultMessage: 'Heatmap Visualization', + }), + getXAccessorDisplayName: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.xAccessorDisplayName', { + defaultMessage: 'X-axis', + }), + getXAccessorHelp: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.xAccessorHelp', { + defaultMessage: 'The name of the x axis column or the corresponding dimension', + }), + getYAccessorDisplayName: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.yAccessorDisplayName', { + defaultMessage: 'Y-axis', + }), + getYAccessorHelp: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.yAccessorHelp', { + defaultMessage: 'The name of the y axis column or the corresponding dimension', + }), + getValueAccessorDisplayName: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.valueAccessorDisplayName', { + defaultMessage: 'Value', + }), + getValueAccessorHelp: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.valueAccessorHelp', { + defaultMessage: 'The name of the value column or the corresponding dimension', + }), + getLegendHelp: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.legendHelp', { + defaultMessage: "Configure the heatmap chart's legend", + }), + getLegendDisplayName: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.legendDisplayName', { + defaultMessage: 'Heatmap legend', + }), + getGridConfigHelp: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.gridConfigHelp', { + defaultMessage: 'Configure the heatmap layout', + }), + getGridConfigDisplayName: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.gridConfigDisplayName', { + defaultMessage: 'Heatmap layout configuration', + }), + getSplitRowAccessorDisplayName: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.splitRowAccessorDisplayName', { + defaultMessage: 'Split row', + }), + getSplitRowAccessorHelp: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.plitRowAccessorHelp', { + defaultMessage: 'The id of the split row or the corresponding dimension', + }), + getSplitColumnAccessorDisplayName: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.splitColumnAccessorDisplayName', { + defaultMessage: 'Split column', + }), + getSplitColumnAccessorHelp: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.splitColumnAccessorHelp', { + defaultMessage: 'The id of the split column or the corresponding dimension', + }), + getShowTooltipDisplayName: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.showTooltipDisplayName', { + defaultMessage: 'Show tooltip', + }), + getShowTooltipHelp: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.showTooltipHelp', { + defaultMessage: 'Show tooltip on hover', + }), + getHighlightInHoverDisplayName: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.highlightInHoverDisplayName', { + defaultMessage: 'Hightlight on hover', + }), + getHighlightInHoverHelp: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.highlightInHoverHelp', { + defaultMessage: + 'When this is enabled, it highlights the ranges of the same color on legend hover', + }), + getLastRangeIsRightOpenDisplayName: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.lastRangeIsRightOpenDisplayName', { + defaultMessage: 'Last range is right open', + }), + getLastRangeIsRightOpenHelp: () => + i18n.translate('xpack.canvas.uis.views.heatmap.args.lastRangeIsRightOpenHelp', { + defaultMessage: 'If is set to true, the last range value will be right open', + }), + }, }; diff --git a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx index 0368cd3d9fac..9f05ac278405 100644 --- a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx +++ b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx @@ -11,7 +11,6 @@ import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Popover } from '../popover'; import { ArgAdd } from '../arg_add'; -import type { Arg } from '../../expression_types/arg'; const strings = { getAddAriaLabel: () => @@ -20,8 +19,10 @@ const strings = { }), }; -interface ArgOptions { - arg: Arg; +export interface ArgOptions { + name?: string; + displayName?: string; + help?: string; onValueAdd: () => void; } @@ -49,9 +50,9 @@ export const ArgAddPopover: FC = ({ options }) => { {({ closePopover }) => options.map((opt) => ( { opt.onValueAdd(); closePopover(); diff --git a/x-pack/plugins/canvas/public/components/arg_add_popover/index.ts b/x-pack/plugins/canvas/public/components/arg_add_popover/index.ts index b5744b3ab27f..1eae5058e7d3 100644 --- a/x-pack/plugins/canvas/public/components/arg_add_popover/index.ts +++ b/x-pack/plugins/canvas/public/components/arg_add_popover/index.ts @@ -6,3 +6,4 @@ */ export { ArgAddPopover } from './arg_add_popover'; +export type { ArgOptions } from './arg_add_popover'; diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx b/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx index af58e624d1b3..952c4fa66e9d 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx +++ b/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx @@ -9,10 +9,11 @@ import React, { useState, useEffect, useCallback, useRef, memo, ReactPortal } fr import deepEqual from 'react-fast-compare'; import usePrevious from 'react-use/lib/usePrevious'; import useEffectOnce from 'react-use/lib/useEffectOnce'; +import { ExpressionAstExpression, ExpressionValue } from 'src/plugins/expressions'; import { ExpressionFormHandlers } from '../../../common/lib/expression_form_handlers'; import { UpdatePropsRef } from '../../../types/arguments'; -interface ArgTemplateFormProps { +export interface ArgTemplateFormProps { template?: ( domNode: HTMLElement, config: ArgTemplateFormProps['argumentProps'], @@ -24,10 +25,13 @@ interface ArgTemplateFormProps { label?: string; setLabel: (label: string) => void; expand?: boolean; + argValue: any; setExpand?: (expand: boolean) => void; - onValueRemove?: (argName: string, argIndex: string) => void; + onValueRemove?: () => void; + onValueChange: (value: any) => void; resetErrorState: () => void; renderError: () => void; + argResolver: (ast: ExpressionAstExpression) => Promise; }; handlers?: { [key: string]: (...args: any[]) => any }; error?: unknown; diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js index fbb96abc53b6..8e2176b845bf 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js @@ -39,6 +39,7 @@ const strings = { defaultMessage: 'Save', }), }; + export class DatasourceComponent extends PureComponent { static propTypes = { args: PropTypes.object.isRequired, diff --git a/x-pack/plugins/canvas/public/components/datasource/index.js b/x-pack/plugins/canvas/public/components/datasource/index.js index 06c920937f34..d0e3a2200477 100644 --- a/x-pack/plugins/canvas/public/components/datasource/index.js +++ b/x-pack/plugins/canvas/public/components/datasource/index.js @@ -12,7 +12,7 @@ import { get } from 'lodash'; import { datasourceRegistry } from '../../expression_types'; import { getServerFunctions } from '../../state/selectors/app'; import { getSelectedElement, getSelectedPage } from '../../state/selectors/workpad'; -import { setArgumentAtIndex, setAstAtIndex, flushContext } from '../../state/actions/elements'; +import { setAstAtIndex, flushContext } from '../../state/actions/elements'; import { Datasource as Component } from './datasource'; const DatasourceComponent = (props) => { @@ -52,7 +52,6 @@ const mapStateToProps = (state) => ({ }); const mapDispatchToProps = (dispatch) => ({ - dispatchArgumentAtIndex: (props) => (arg) => dispatch(setArgumentAtIndex({ ...props, arg })), dispatchAstAtIndex: ({ index, element, pageId }) => (ast) => { @@ -63,7 +62,7 @@ const mapDispatchToProps = (dispatch) => ({ const mergeProps = (stateProps, dispatchProps, ownProps) => { const { element, pageId, functionDefinitions } = stateProps; - const { dispatchArgumentAtIndex, dispatchAstAtIndex } = dispatchProps; + const { dispatchAstAtIndex } = dispatchProps; const getDataTableFunctionsByName = (name) => functionDefinitions.find((fn) => fn.name === name && fn.type === 'datatable'); @@ -106,11 +105,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { element, index: datasourceAst && datasourceAst.expressionIndex, }), - setDatasourceArgs: dispatchArgumentAtIndex({ - pageId, - element, - index: datasourceAst && datasourceAst.expressionIndex, - }), }; }; diff --git a/x-pack/plugins/canvas/public/components/function_form/function_form_component.tsx b/x-pack/plugins/canvas/public/components/function_form/function_form_component.tsx index 8e625db94b49..868693905abe 100644 --- a/x-pack/plugins/canvas/public/components/function_form/function_form_component.tsx +++ b/x-pack/plugins/canvas/public/components/function_form/function_form_component.tsx @@ -13,13 +13,15 @@ type FunctionFormComponentProps = RenderArgData; export const FunctionFormComponent: FunctionComponent = (props) => { const passedProps = { name: props.name, + removable: props.removable, argResolver: props.argResolver, args: props.args, + id: props.id, + nestedFunctionsArgs: props.nestedFunctionsArgs, argType: props.argType, argTypeDef: props.argTypeDef, filterGroups: props.filterGroups, context: props.context, - expressionIndex: props.expressionIndex, expressionType: props.expressionType, nextArgType: props.nextArgType, nextExpressionType: props.nextExpressionType, @@ -27,6 +29,7 @@ export const FunctionFormComponent: FunctionComponent void; } -export const FunctionFormContextPending: React.FunctionComponent< - FunctionFormContextPendingProps -> = (props) => { +export const FunctionFormContextPending: FC = (props) => { const { contextExpression, expressionType, context, updateContext } = props; const prevContextExpression = usePrevious(contextExpression); const fetchContext = useCallback( diff --git a/x-pack/plugins/canvas/public/components/function_form/index.tsx b/x-pack/plugins/canvas/public/components/function_form/index.tsx index 2494396d3712..1194943f385e 100644 --- a/x-pack/plugins/canvas/public/components/function_form/index.tsx +++ b/x-pack/plugins/canvas/public/components/function_form/index.tsx @@ -19,8 +19,8 @@ import { getId } from '../../lib/get_id'; import { createAsset } from '../../state/actions/assets'; import { fetchContext, - setArgumentAtIndex, - addArgumentValueAtIndex, + setArgument as setArgumentValue, + addArgumentValue, deleteArgumentAtIndex, // @ts-expect-error untyped local } from '../../state/actions/elements'; @@ -34,24 +34,29 @@ import { getAssets } from '../../state/selectors/assets'; // @ts-expect-error unconverted lib import { findExistingAsset } from '../../lib/find_existing_asset'; import { FunctionForm as Component } from './function_form'; -import { ArgType, ArgTypeDef } from '../../expression_types/types'; +import { Args, ArgType, ArgTypeDef } from '../../expression_types/types'; import { State, ExpressionContext, CanvasElement, AssetType } from '../../../types'; interface FunctionFormProps { name: string; argResolver: (ast: ExpressionAstExpression) => Promise; - args: Record> | null; + args: Args; + nestedFunctionsArgs: Args; argType: ArgType; argTypeDef: ArgTypeDef; expressionIndex: number; nextArgType?: ArgType; + path: string; + parentPath: string; + removable?: boolean; } export const FunctionForm: React.FunctionComponent = (props) => { - const { expressionIndex, argType, nextArgType } = props; + const { expressionIndex, ...restProps } = props; + const { nextArgType, path, parentPath, argType } = restProps; const dispatch = useDispatch(); const context = useSelector( - (state) => getContextForIndex(state, expressionIndex), + (state) => getContextForIndex(state, parentPath, expressionIndex), deepEqual ); const element = useSelector( @@ -67,55 +72,33 @@ export const FunctionForm: React.FunctionComponent = (props) const addArgument = useCallback( (argName: string, argValue: string | Ast | null) => () => { - dispatch( - addArgumentValueAtIndex({ - index: expressionIndex, - element, - pageId, - argName, - value: argValue, - }) - ); + dispatch(addArgumentValue({ element, pageId, argName, value: argValue, path })); }, - [dispatch, element, expressionIndex, pageId] + [dispatch, element, pageId, path] ); - const updateContext = useCallback( - () => dispatch(fetchContext(expressionIndex, element)), - [dispatch, element, expressionIndex] - ); + const updateContext = useCallback(() => { + return dispatch(fetchContext(expressionIndex, element, false, parentPath)); + }, [dispatch, element, expressionIndex, parentPath]); const setArgument = useCallback( (argName: string, valueIndex: number) => (value: string | Ast | null) => { - dispatch( - setArgumentAtIndex({ - index: expressionIndex, - element, - pageId, - argName, - value, - valueIndex, - }) - ); + dispatch(setArgumentValue({ element, pageId, argName, value, valueIndex, path })); }, - [dispatch, element, expressionIndex, pageId] + [dispatch, element, pageId, path] ); const deleteArgument = useCallback( (argName: string, argIndex: number) => () => { - dispatch( - deleteArgumentAtIndex({ - index: expressionIndex, - element, - pageId, - argName, - argIndex, - }) - ); + dispatch(deleteArgumentAtIndex({ element, pageId, argName, argIndex, path })); }, - [dispatch, element, expressionIndex, pageId] + [dispatch, element, pageId, path] ); + const deleteParentArgument = useCallback(() => { + dispatch(deleteArgumentAtIndex({ element, pageId, path: parentPath })); + }, [dispatch, element, pageId, parentPath]); + const onAssetAddDispatch = useCallback( (type: AssetType['type'], content: AssetType['value']) => { // make the ID here and pass it into the action @@ -138,9 +121,11 @@ export const FunctionForm: React.FunctionComponent = (props) }, [assets, onAssetAddDispatch] ); + return ( = (props) updateContext={updateContext} onValueChange={setArgument} onValueRemove={deleteArgument} + onContainerRemove={deleteParentArgument} onAssetAdd={onAssetAdd} /> ); diff --git a/x-pack/plugins/canvas/public/components/function_form_list/index.js b/x-pack/plugins/canvas/public/components/function_form_list/index.js index 94de0a8838af..6048ac360386 100644 --- a/x-pack/plugins/canvas/public/components/function_form_list/index.js +++ b/x-pack/plugins/canvas/public/components/function_form_list/index.js @@ -9,62 +9,185 @@ import { compose, withProps } from 'recompose'; import { get } from 'lodash'; import { toExpression } from '@kbn/interpreter'; import { interpretAst } from '../../lib/run_interpreter'; -import { modelRegistry, viewRegistry, transformRegistry } from '../../expression_types'; +import { getArgTypeDef } from '../../lib/args'; import { FunctionFormList as Component } from './function_form_list'; function normalizeContext(chain) { if (!Array.isArray(chain) || !chain.length) { return null; } - return { - type: 'expression', - chain, - }; + return { type: 'expression', chain }; } function getExpression(ast) { return ast != null && ast.type === 'expression' ? toExpression(ast) : ast; } -function getArgTypeDef(fn) { - return modelRegistry.get(fn) || viewRegistry.get(fn) || transformRegistry.get(fn); +const isPureArgumentType = (arg) => !arg.type || arg.type === 'argument'; + +const reduceArgsByCondition = (argsObject, isMatchingCondition) => + Object.keys(argsObject).reduce((acc, argName) => { + if (isMatchingCondition(argName)) { + return { ...acc, [argName]: argsObject[argName] }; + } + return acc; + }, {}); + +const createComponentsWithContext = () => ({ mapped: [], context: [] }); + +const getPureArgs = (argTypeDef, args) => { + const pureArgumentsView = argTypeDef.args.filter((arg) => isPureArgumentType(arg)); + const pureArgumentsNames = pureArgumentsView.map((arg) => arg.name); + const pureArgs = reduceArgsByCondition(args, (argName) => pureArgumentsNames.includes(argName)); + return { args: pureArgs, argumentsView: pureArgumentsView }; +}; + +const getComplexArgs = (argTypeDef, args) => { + const complexArgumentsView = argTypeDef.args.filter((arg) => !isPureArgumentType(arg)); + const complexArgumentsNames = complexArgumentsView.map((arg) => arg.name); + const complexArgs = reduceArgsByCondition(args, (argName) => + complexArgumentsNames.includes(argName) + ); + return { args: complexArgs, argumentsView: complexArgumentsView }; +}; + +const mergeComponentsAndContexts = ( + { context = [], mapped = [] }, + { context: nextContext = [], mapped: nextMapped = [] } +) => ({ + mapped: [...mapped, ...nextMapped], + context: [...context, ...nextContext], +}); + +const buildPath = (prevPath = '', argName, index, removable = false) => { + const newPath = index === undefined ? argName : `${argName}.${index}`; + return { path: prevPath.length ? `${prevPath}.${newPath}` : newPath, removable }; +}; + +const componentFactory = ({ + args, + argsWithExprFunctions, + argType, + argTypeDef, + argumentsView, + argUiConfig, + prevContext, + expressionIndex, + nextArg, + path, + parentPath, + removable, +}) => ({ + args, + nestedFunctionsArgs: argsWithExprFunctions, + argType: argType.function, + argTypeDef: Object.assign(argTypeDef, { + args: argumentsView, + name: argUiConfig?.name ?? argTypeDef.name, + displayName: argUiConfig?.displayName ?? argTypeDef.displayName, + help: argUiConfig?.help ?? argTypeDef.name, + }), + argResolver: (argAst) => interpretAst(argAst, prevContext), + contextExpression: getExpression(prevContext), + expressionIndex, // preserve the index in the AST + nextArgType: nextArg && nextArg.function, + path, + parentPath, + removable, +}); + +/** + * Converts expression functions at the arguments for the expression, to the array of UI component configurations. + * @param {Ast['chain'][number]['arguments']} complexArgs - expression's arguments, which are expression functions. + * @param {object[]} complexArgumentsViews - argument UI views/models/tranforms. + * @param {string} argumentPath - path at the AST to the current expression. + * @returns flatten array of the arguments UI configurations. + */ +const transformNestedFunctionsToUIConfig = (complexArgs, complexArgumentsViews, argumentPath) => + Object.keys(complexArgs).reduce((current, argName) => { + const next = complexArgs[argName] + .map(({ chain }, index) => + transformFunctionsToUIConfig( + chain, + buildPath(argumentPath, argName, index, true), + complexArgumentsViews?.find((argView) => argView.name === argName) + ) + ) + .reduce( + (current, next) => mergeComponentsAndContexts(current, next), + createComponentsWithContext() + ); + return mergeComponentsAndContexts(current, next); + }, createComponentsWithContext()); + +/** + * Converts chain of expressions to the array of UI component configurations. + * Recursively loops through the AST, detects expression functions inside + * the expression chain of the top and nested levels, finds view/model/transform definition + * for the found expression functions, splits arguments of the expression for two categories: simple and expression functions. + * After, recursively loops through the nested expression functions, creates UI component configurations and flatten them to the array. + * + * @param {Ast['chain']} functionsChain - chain of expression functions. + * @param {{ path: string, removable: boolean }} functionMeta - saves the path to the current expressions chain at the original AST + * and saves the information about that it can be removed (is an argument of the other expression). + * @param {object} argUiConfig - Argument UI configuration of the element, which contains current expressions chain. It can be view, model, transform or argument. + * @returns UI component configurations of expressions, found at AST. + */ +function transformFunctionsToUIConfig(functionsChain, { path, removable }, argUiConfig) { + const parentPath = path; + const argumentsPath = path ? `${path}.chain` : `chain`; + return functionsChain.reduce((current, argType, i) => { + const argumentPath = `${argumentsPath}.${i}.arguments`; + const argTypeDef = getArgTypeDef(argType.function); + current.context = current.context.concat(argType); + + // filter out argTypes that shouldn't be in the sidebar + if (!argTypeDef) { + return current; + } + + const { argumentsView, args } = getPureArgs(argTypeDef, argType.arguments); + const { argumentsView: exprFunctionsViews, args: argsWithExprFunctions } = getComplexArgs( + argTypeDef, + argType.arguments + ); + + // wrap each part of the chain in ArgType, passing in the previous context + const component = componentFactory({ + args, + argsWithExprFunctions, + argType, + argTypeDef, + argumentsView, + argUiConfig, + prevContext: normalizeContext(current.context), + expressionIndex: i, // preserve the index in the AST + nextArg: functionsChain[i + 1] || null, + path: argumentPath, + parentPath, + removable, + }); + + const components = transformNestedFunctionsToUIConfig( + argsWithExprFunctions, + exprFunctionsViews, + argumentPath + ); + + return mergeComponentsAndContexts(current, { + ...components, + mapped: [component, ...components.mapped], + }); + }, createComponentsWithContext()); } const functionFormItems = withProps((props) => { const selectedElement = props.element; - const FunctionFormChain = get(selectedElement, 'ast.chain', []); - + const functionsChain = get(selectedElement, 'ast.chain', []); // map argTypes from AST, attaching nextArgType if one exists - const FunctionFormListItems = FunctionFormChain.reduce( - (acc, argType, i) => { - const argTypeDef = getArgTypeDef(argType.function); - const prevContext = normalizeContext(acc.context); - const nextArg = FunctionFormChain[i + 1] || null; - - // filter out argTypes that shouldn't be in the sidebar - if (argTypeDef) { - // wrap each part of the chain in ArgType, passing in the previous context - const component = { - args: argType.arguments, - argType: argType.function, - argTypeDef: argTypeDef, - argResolver: (argAst) => interpretAst(argAst, prevContext), - contextExpression: getExpression(prevContext), - expressionIndex: i, // preserve the index in the AST - nextArgType: nextArg && nextArg.function, - }; - - acc.mapped.push(component); - } - - acc.context = acc.context.concat(argType); - return acc; - }, - { mapped: [], context: [] } - ); - + const functionsListItems = transformFunctionsToUIConfig(functionsChain, buildPath('', 'ast')); return { - functionFormItems: FunctionFormListItems.mapped, + functionFormItems: functionsListItems.mapped, }; }); diff --git a/x-pack/plugins/canvas/public/components/with_debounce_arg/index.ts b/x-pack/plugins/canvas/public/components/with_debounce_arg/index.ts new file mode 100644 index 000000000000..53e604a3795a --- /dev/null +++ b/x-pack/plugins/canvas/public/components/with_debounce_arg/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { withDebounceArg } from './with_debounce_arg'; diff --git a/x-pack/plugins/canvas/public/components/with_debounce_arg/with_debounce_arg.tsx b/x-pack/plugins/canvas/public/components/with_debounce_arg/with_debounce_arg.tsx new file mode 100644 index 000000000000..61e953a4a432 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/with_debounce_arg/with_debounce_arg.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useState, useEffect } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import deepEqual from 'react-fast-compare'; +import { ArgTemplateFormProps } from '../arg_form/arg_template_form'; + +type Props = ArgTemplateFormProps['argumentProps']; + +export const withDebounceArg = + (Arg: FC, debouncePeriod: number = 150): FC => + ({ argValue, onValueChange, ...restProps }) => { + const [localArgValue, setArgValue] = useState(argValue); + + const [, cancel] = useDebounce( + () => { + if (localArgValue === argValue || deepEqual(localArgValue, argValue)) { + return; + } + + onValueChange(localArgValue); + }, + debouncePeriod, + [localArgValue] + ); + + useEffect(() => { + return () => { + cancel(); + }; + }, [cancel]); + + return ; + }; diff --git a/x-pack/plugins/canvas/public/expression_types/arg.ts b/x-pack/plugins/canvas/public/expression_types/arg.ts index 9c46f4d1e2b9..4ebf47099583 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg.ts +++ b/x-pack/plugins/canvas/public/expression_types/arg.ts @@ -11,13 +11,14 @@ import { Ast } from '@kbn/interpreter'; // @ts-expect-error unconverted components import { ArgForm } from '../components/arg_form'; import { argTypeRegistry } from './arg_type_registry'; -import type { ArgType, ArgTypeDef, ExpressionType } from './types'; +import type { Args, ArgType, ArgTypeDef, ArgValue, ExpressionType } from './types'; import { AssetType, CanvasElement, ExpressionAstExpression, ExpressionValue, ExpressionContext, + DatatableColumn, } from '../../types'; import { BaseFormProps } from './base_form'; @@ -26,6 +27,7 @@ interface ArtOwnProps { multi?: boolean; required?: boolean; types?: string[]; + type?: 'model' | 'argument'; default?: string | null; resolve?: (...args: any[]) => any; options?: { @@ -38,10 +40,25 @@ interface ArtOwnProps { shapes?: string[]; }; } -export type ArgProps = ArtOwnProps & BaseFormProps; +export type ArgUiConfig = ArtOwnProps & BaseFormProps; + +export interface ResolvedColumns { + columns: DatatableColumn[]; +} +export interface ResolvedLabels { + labels: string[]; +} + +export interface ResolvedDataurl { + dataurl: string; +} + +export interface ResolvedArgProps { + resolved: T; +} export interface DataArg { - argValue?: string | Ast | null; + argValue?: ArgValue | null; skipRender?: boolean; label?: string; valueIndex: number; @@ -50,16 +67,15 @@ export interface DataArg { contextExpression?: string; name: string; argResolver: (ast: ExpressionAstExpression) => Promise; - args: Record> | null; + args: Args; argType: ArgType; argTypeDef?: ArgTypeDef; filterGroups: string[]; context?: ExpressionContext; - expressionIndex: number; expressionType: ExpressionType; nextArgType?: ArgType; nextExpressionType?: ExpressionType; - onValueAdd: (argName: string, argValue: string | Ast | null) => () => void; + onValueAdd: (argName: string, argValue: ArgValue | null) => () => void; onAssetAdd: (type: AssetType['type'], content: AssetType['value']) => string; onValueChange: (value: Ast | string) => void; onValueRemove: () => void; @@ -81,7 +97,7 @@ export class Arg { displayName?: string; help?: string; - constructor(props: ArgProps) { + constructor(props: ArgUiConfig) { const argType = argTypeRegistry.get(props.argType); if (!argType) { throw new Error(`Invalid arg type: ${props.argType}`); @@ -117,26 +133,32 @@ export class Arg { } // TODO: Document what these otherProps are. Maybe make them named arguments? - render(data: DataArg) { - const { onValueChange, onValueRemove, argValue, key, label, ...otherProps } = data; + render(data: DataArg & ResolvedArgProps) { + const { onValueChange, onValueRemove, key, label, ...otherProps } = data; + const resolvedProps = this.resolve?.(otherProps); + const { argValue, onAssetAdd, resolved, filterGroups, argResolver } = otherProps; + const argId = key; // This is everything the arg_type template needs to render const templateProps = { - ...otherProps, - ...this.resolve?.(otherProps), - onValueChange, argValue, + argId, + onAssetAdd, + onValueChange, typeInstance: this, + resolved: { ...resolved, ...resolvedProps }, + argResolver, + filterGroups, }; const formProps = { key, argTypeInstance: this, - valueMissing: this.required && argValue == null, + valueMissing: this.required && data.argValue == null, label, onValueChange, onValueRemove, templateProps, - argId: key, + argId, options: this.options, }; diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/extended_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/extended_template.stories.tsx index 2b2155833949..10b6f1b17f4a 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/extended_template.stories.tsx +++ b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/extended_template.stories.tsx @@ -49,7 +49,7 @@ class Interactive extends React.Component<{}, { argValue: ExpressionAstExpressio action('onValueChange')(argValue); this.setState({ argValue }); }} - labels={array('Series Labels', ['label1', 'label2'])} + resolved={{ labels: array('Series Labels', ['label1', 'label2']) }} typeInstance={{ name: radios('Type Instance', { default: 'defaultStyle', custom: 'custom' }, 'custom'), options: { @@ -74,7 +74,7 @@ storiesOf('arguments/SeriesStyle/components', module) .add('extended: defaults', () => ( { action('onValueChange')(argValue); @@ -64,6 +65,7 @@ storiesOf('arguments/SeriesStyle/components', module) argValue={defaultExpression} onValueChange={action('onValueChange')} workpad={getDefaultWorkpad()} + resolved={{ labels: [] }} typeInstance={{ name: 'defaultStyle', }} @@ -72,7 +74,7 @@ storiesOf('arguments/SeriesStyle/components', module) .add('simple: defaults', () => ( ( void; typeInstance?: { name: string; @@ -34,10 +34,15 @@ export interface Props { include: string[]; }; }; -} +} & ResolvedArgProps; export const ExtendedTemplate: FunctionComponent = (props) => { - const { typeInstance, onValueChange, labels, argValue } = props; + const { + typeInstance, + onValueChange, + resolved: { labels }, + argValue, + } = props; const chain = get(argValue, 'chain.0', {}); const chainArgs = get(chain, 'arguments', {}); const selectedSeries = get(chainArgs, 'label.0', ''); @@ -141,5 +146,7 @@ ExtendedTemplate.propTypes = { onValueChange: PropTypes.func.isRequired, argValue: PropTypes.any.isRequired, typeInstance: PropTypes.object, - labels: PropTypes.array.isRequired, + resolved: PropTypes.shape({ + labels: PropTypes.array.isRequired, + }).isRequired, }; diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx index 8dcfbfe8aba4..d54e6fdb6004 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx +++ b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx @@ -10,6 +10,7 @@ import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiButtonIcon, EuiText } from '@elastic/eui'; import immutable from 'object-path-immutable'; import { get } from 'lodash'; +import { ResolvedArgProps, ResolvedLabels } from '../../arg'; import { ColorPickerPopover } from '../../../components/color_picker_popover'; import { TooltipIcon, IconType } from '../../../components/tooltip_icon'; import { ExpressionAstExpression, CanvasWorkpad } from '../../../../types'; @@ -23,18 +24,23 @@ interface Arguments { } type Argument = keyof Arguments; -interface Props { +type Props = { argValue: ExpressionAstExpression; - labels?: string[]; onValueChange: (argValue: ExpressionAstExpression) => void; typeInstance: { name: string; }; workpad: CanvasWorkpad; -} +} & ResolvedArgProps; export const SimpleTemplate: FunctionComponent = (props) => { - const { typeInstance, argValue, onValueChange, labels, workpad } = props; + const { + typeInstance, + argValue, + onValueChange, + resolved: { labels }, + workpad, + } = props; const { name } = typeInstance; const chain = get(argValue, 'chain.0', {}); const chainArgs = get(chain, 'arguments', {}); @@ -107,7 +113,9 @@ SimpleTemplate.displayName = 'SeriesStyleArgSimpleInput'; SimpleTemplate.propTypes = { argValue: PropTypes.any.isRequired, - labels: PropTypes.array, + resolved: PropTypes.shape({ + labels: PropTypes.array.isRequired, + }).isRequired, onValueChange: PropTypes.func.isRequired, workpad: PropTypes.shape({ colors: PropTypes.array.isRequired, diff --git a/x-pack/plugins/canvas/public/expression_types/function_form.tsx b/x-pack/plugins/canvas/public/expression_types/function_form.tsx index b2fbfa387b49..f66cfd301c7d 100644 --- a/x-pack/plugins/canvas/public/expression_types/function_form.tsx +++ b/x-pack/plugins/canvas/public/expression_types/function_form.tsx @@ -6,65 +6,71 @@ */ import React, { ReactElement } from 'react'; -import { EuiCallOut } from '@elastic/eui'; +import { EuiButtonIcon, EuiCallOut, EuiFlexGroup, EuiFormRow, EuiToolTip } from '@elastic/eui'; import { isPlainObject, uniq, last, compact } from 'lodash'; import { Ast, fromExpression } from '@kbn/interpreter'; -import { ArgAddPopover } from '../components/arg_add_popover'; +import { ArgAddPopover, ArgOptions } from '../components/arg_add_popover'; // @ts-expect-error unconverted components import { SidebarSection } from '../components/sidebar/sidebar_section'; // @ts-expect-error unconverted components import { SidebarSectionTitle } from '../components/sidebar/sidebar_section_title'; import { BaseForm, BaseFormProps } from './base_form'; -import { Arg, ArgProps } from './arg'; -import { ArgType, ArgTypeDef, ExpressionType } from './types'; +import { Arg, ArgUiConfig, ResolvedArgProps } from './arg'; +import { ArgDisplayType, Args, ArgType, ArgTypeDef, ArgValue, ExpressionType } from './types'; +import { Model, Transform, View } from '../expression_types'; import { AssetType, CanvasElement, - DatatableColumn, ExpressionAstExpression, ExpressionContext, ExpressionValue, } from '../../types'; +import { buildDefaultArgExpr, getArgTypeDef } from '../lib/args'; -export interface DataArg { +export interface ArgWithValues { arg: Arg | undefined; - argValues?: Array; - skipRender?: boolean; - label?: 'string'; + argValues?: Array; } export type RenderArgData = BaseFormProps & { argType: ArgType; + removable?: boolean; + type?: ArgDisplayType; argTypeDef?: ArgTypeDef; - args: Record> | null; + args: Args; + id: string; + nestedFunctionsArgs: Args; argResolver: (ast: ExpressionAstExpression) => Promise; context?: ExpressionContext; contextExpression?: string; - expressionIndex: number; expressionType: ExpressionType; filterGroups: string[]; nextArgType?: ArgType; nextExpressionType?: ExpressionType; - onValueAdd: (argName: string, argValue: string | Ast | null) => () => void; + onValueAdd: (argName: string, argValue: ArgValue | null) => () => void; onValueChange: (argName: string, argIndex: number) => (value: string | Ast) => void; onValueRemove: (argName: string, argIndex: number) => () => void; + onContainerRemove: () => void; onAssetAdd: (type: AssetType['type'], content: AssetType['value']) => string; updateContext: (element?: CanvasElement) => void; typeInstance?: ExpressionType; - columns?: DatatableColumn[]; }; export type RenderArgProps = { typeInstance: FunctionForm; -} & RenderArgData; +} & RenderArgData & + ResolvedArgProps; export type FunctionFormProps = { - args?: ArgProps[]; + args?: ArgUiConfig[]; resolve?: (...args: any[]) => any; } & BaseFormProps; export class FunctionForm extends BaseForm { - args: ArgProps[]; + /** + * UI arguments config + */ + args: ArgUiConfig[]; resolve: (...args: any[]) => any; constructor(props: FunctionFormProps) { @@ -74,23 +80,21 @@ export class FunctionForm extends BaseForm { this.resolve = props.resolve || (() => ({})); } - renderArg(props: RenderArgProps, dataArg: DataArg) { - const { onValueRemove, onValueChange, ...passedProps } = props; - const { arg, argValues, skipRender, label } = dataArg; - const { argType, expressionIndex } = passedProps; - + renderArg(argWithValues: ArgWithValues, props: RenderArgProps) { + const { onValueRemove, onValueChange, onContainerRemove, id, ...passedProps } = props; + const { arg, argValues } = argWithValues; // TODO: show some information to the user than an argument was skipped - if (!arg || skipRender) { + if (!arg) { return null; } + const renderArgWithProps = ( argValue: string | Ast | null, valueIndex: number ): ReactElement | null => arg.render({ - key: `${argType}-${expressionIndex}-${arg.name}-${valueIndex}`, + key: `${id}.${arg.name}.${valueIndex}`, ...passedProps, - label, valueIndex, onValueChange: onValueChange(arg.name, valueIndex), onValueRemove: onValueRemove(arg.name, valueIndex), @@ -107,75 +111,166 @@ export class FunctionForm extends BaseForm { return argValues && argValues.map(renderArgWithProps); } - // TODO: Argument adding isn't very good, we should improve this UI - getAddableArg(props: RenderArgProps, dataArg: DataArg) { - const { onValueAdd } = props; - const { arg, argValues, skipRender } = dataArg; + getArgDescription({ name, displayName, help }: Arg | ArgTypeDef, argUiConfig: ArgUiConfig) { + return { + name: argUiConfig.name ?? name ?? '', + displayName: argUiConfig.displayName ?? displayName, + help: argUiConfig.help ?? help, + }; + } + + getAddableArgComplex( + argUiConfig: ArgUiConfig, + argValues: Array, + onValueAdd: RenderArgProps['onValueAdd'] + ) { + if (argValues && !argUiConfig.multi) { + return null; + } + + const argExpression = buildDefaultArgExpr(argUiConfig); + + const arg = getArgTypeDef(argUiConfig.argType); + if (!arg || argExpression === undefined) { + return null; + } + + const value = argExpression === null ? null : fromExpression(argExpression, 'argument'); + + return { + ...this.getArgDescription(arg, argUiConfig), + onValueAdd: onValueAdd(argUiConfig.name, value), + }; + } + + getAddableArgSimple( + argUiConfig: ArgUiConfig, + argValues: Array, + onValueAdd: RenderArgProps['onValueAdd'] + ) { + const arg = new Arg(argUiConfig); // skip arguments that aren't defined in the expression type schema - if (!arg || arg.required || skipRender) { + if (!arg || arg.required) { return null; } + if (argValues && !arg.multi) { return null; } - const value = arg.default == null ? null : fromExpression(arg.default, 'argument'); - return { arg, onValueAdd: onValueAdd(arg.name, value) }; + const value = + arg.default === null || arg.default === undefined + ? null + : fromExpression(arg.default, 'argument'); + return { ...this.getArgDescription(arg, argUiConfig), onValueAdd: onValueAdd(arg.name, value) }; } - resolveArg(...args: unknown[]) { - // basically a no-op placeholder - return {}; + getAddableArgs( + simpleFunctionArgs: RenderArgData['args'] = {}, + nestedFunctionsArgs: RenderArgData['nestedFunctionsArgs'] = {}, + onValueAdd: RenderArgData['onValueAdd'] + ) { + const simpleArgs = simpleFunctionArgs === null ? {} : simpleFunctionArgs; + const complexArgs = nestedFunctionsArgs === null ? {} : nestedFunctionsArgs; + const addableArgs = this.args.reduce((addable, arg) => { + if (!arg.type || arg.type === 'argument') { + const addableArg = this.getAddableArgSimple(arg, simpleArgs[arg.name], onValueAdd); + return addableArg ? [...addable, addableArg] : addable; + } + + const addableArg = this.getAddableArgComplex(arg, complexArgs[arg.name], onValueAdd); + return addableArg ? [...addable, addableArg] : addable; + }, []); + + return addableArgs; } - render(data: RenderArgData) { - if (!data) { - data = { - args: null, - argTypeDef: undefined, - } as RenderArgData; + getArgsWithValues(args: RenderArgData['args'], argTypeDef: RenderArgData['argTypeDef']) { + let argInstances: Arg[] = []; + if (this.isExpressionFunctionForm(argTypeDef)) { + const argNames = argTypeDef.args.map(({ name }) => name); + argInstances = this.args + .filter((arg) => argNames.includes(arg.name)) + .map((argSpec) => new Arg(argSpec)); + } else { + argInstances = this.args.map((argSpec) => new Arg(argSpec)); } - const { args, argTypeDef } = data; - // Don't instaniate these until render time, to give the registries a chance to populate. - const argInstances = this.args.map((argSpec) => new Arg(argSpec)); if (args === null || !isPlainObject(args)) { throw new Error(`Form "${this.name}" expects "args" object`); } - // get a mapping of arg values from the expression and from the renderable's schema - const argNames = uniq(argInstances.map((arg) => arg.name).concat(Object.keys(args))); - const dataArgs = argNames.map((argName) => { + const argNames = uniq(this.args.map((arg) => arg.name).concat(Object.keys(args))); + + return argNames.map((argName) => { const arg = argInstances.find((argument) => argument.name === argName); // if arg is not multi, only preserve the last value found // otherwise, leave the value alone (including if the arg is not defined) const isMulti = arg && arg.multi; const argValues = args[argName] && !isMulti ? [last(args[argName]) ?? null] : args[argName]; + return { arg, argValues }; }); + } - // props are passed to resolve and the returned object is mixed into the template props - const props = { ...data, ...this.resolve(data), typeInstance: this }; + resolveArg(...args: unknown[]) { + // basically a no-op placeholder + return {}; + } + + private isExpressionFunctionForm( + argTypeDef?: ArgTypeDef + ): argTypeDef is View | Model | Transform { + return ( + !!argTypeDef && + (argTypeDef instanceof View || argTypeDef instanceof Model || argTypeDef instanceof Transform) + ); + } + + render(data: RenderArgData = { args: null, argTypeDef: undefined } as RenderArgData) { + const { args, argTypeDef, nestedFunctionsArgs = {}, removable } = data; + const argsWithValues = this.getArgsWithValues(args, argTypeDef); try { + // props are passed to resolve and the returned object is mixed into the template props + const props: RenderArgProps = { ...data, resolved: this.resolve(data), typeInstance: this }; + // allow a hook to override the data args - const resolvedDataArgs = dataArgs.map((d) => ({ ...d, ...this.resolveArg(d, props) })); + const resolvedArgsWithValues = argsWithValues.map((argWithValues) => ({ + ...argWithValues, + ...this.resolveArg(argWithValues, props), + })); const argumentForms = compact( - resolvedDataArgs.map((dataArg) => this.renderArg(props, dataArg)) - ); - const addableArgs = compact( - resolvedDataArgs.map((dataArg) => this.getAddableArg(props, dataArg)) + resolvedArgsWithValues.map((argWithValues) => this.renderArg(argWithValues, props)) ); + const addableArgs = this.getAddableArgs(args, nestedFunctionsArgs, props.onValueAdd); if (!addableArgs.length && !argumentForms.length) { return null; } - return ( - {addableArgs.length === 0 ? null : } + + + {removable && ( + + { + props.onContainerRemove(); + }} + iconType="cross" + iconSize="s" + aria-label={'Remove'} + className="canvasArg__remove" + /> + + )} + {addableArgs.length === 0 ? null : } + + {argumentForms} diff --git a/x-pack/plugins/canvas/public/expression_types/types.ts b/x-pack/plugins/canvas/public/expression_types/types.ts index 704dae83c8a5..493d63c0871a 100644 --- a/x-pack/plugins/canvas/public/expression_types/types.ts +++ b/x-pack/plugins/canvas/public/expression_types/types.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { Ast } from '@kbn/interpreter'; import type { Transform } from './transform'; import type { View } from './view'; import type { Datasource } from './datasource'; import type { Model } from './model'; export type ArgType = string; +export type ArgDisplayType = 'model' | 'argument'; export type ArgTypeDef = View | Model | Transform | Datasource; @@ -20,3 +22,6 @@ export type { Arg } from './arg'; export type ExpressionType = View | Model | Transform; export type { RenderArgData } from './function_form'; + +export type ArgValue = string | Ast; +export type Args = Record> | null; diff --git a/x-pack/plugins/canvas/public/lib/args.ts b/x-pack/plugins/canvas/public/lib/args.ts new file mode 100644 index 000000000000..01067ca00ce0 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/args.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fromExpression, toExpression } from '@kbn/interpreter'; +import { + modelRegistry, + viewRegistry, + transformRegistry, + Model, + View, + Transform, +} from '../expression_types'; +import { ArgUiConfig } from '../expression_types/arg'; + +type ArgType = Model | View | Transform; + +export function getArgTypeDef(fn: string): ArgType { + return modelRegistry.get(fn) || viewRegistry.get(fn) || transformRegistry.get(fn); +} + +const buildArg = (arg: ArgUiConfig, expr: string) => `${arg.name}=${formatExpr(expr)}`; + +const filterValidArguments = (args: Array) => + args.filter((arg) => arg !== undefined); + +const formatExpr = (expr: string) => { + if (isWithBrackets(expr)) { + const exprWithoutBrackets = removeFigureBrackets(expr); + return toExpression(fromExpression(exprWithoutBrackets)); + } + return expr; +}; + +const removeFigureBrackets = (expr: string) => { + if (isWithBrackets(expr)) { + return expr.substring(1, expr.length - 1); + } + return expr; +}; + +const isWithBrackets = (expr: string) => expr[0] === '{' && expr[expr.length - 1] === '}'; + +export function buildDefaultArgExpr(argUiConfig: ArgUiConfig): string | undefined { + const argConfig = getArgTypeDef(argUiConfig.argType); + if (argUiConfig.default) { + return buildArg(argUiConfig, argUiConfig.default); + } + if (!argConfig) { + return undefined; + } + + const defaultArgs = argConfig.args.map((arg) => { + const argConf = getArgTypeDef(arg.argType); + + if (arg.default && argConf && Array.isArray(argConf.args)) { + return buildArg(arg, arg.default); + } + return buildDefaultArgExpr(arg); + }); + + const validArgs = filterValidArguments(defaultArgs); + const defExpr = validArgs.length + ? `{${argUiConfig.argType} ${validArgs.join(' ')}}` + : `{${argUiConfig.argType}}`; + + return defExpr; +} diff --git a/x-pack/plugins/canvas/public/state/actions/elements.js b/x-pack/plugins/canvas/public/state/actions/elements.js index 4b14e17a2d10..bcc02c3cbc2c 100644 --- a/x-pack/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/plugins/canvas/public/state/actions/elements.js @@ -7,7 +7,7 @@ import { createAction } from 'redux-actions'; import immutable from 'object-path-immutable'; -import { get, pick, cloneDeep, without } from 'lodash'; +import { get, pick, cloneDeep, without, last } from 'lodash'; import { toExpression, safeElementFromExpression } from '@kbn/interpreter'; import { createThunk } from '../../lib/create_thunk'; import { @@ -30,8 +30,8 @@ const { actionsElements: strings } = ErrorStrings; const { set, del } = immutable; -export function getSiblingContext(state, elementId, checkIndex) { - const prevContextPath = [elementId, 'expressionContext', checkIndex]; +export function getSiblingContext(state, elementId, checkIndex, path = ['ast.chain']) { + const prevContextPath = [elementId, 'expressionContext', ...path, checkIndex]; const prevContextValue = getResolvedArgsValue(state, prevContextPath); // if a value is found, return it, along with the index it was found at @@ -49,7 +49,7 @@ export function getSiblingContext(state, elementId, checkIndex) { } // walk back up to find the closest cached context available - return getSiblingContext(state, elementId, prevContextIndex); + return getSiblingContext(state, elementId, prevContextIndex, path); } function getBareElement(el, includeId = false) { @@ -71,8 +71,9 @@ export const flushContextAfterIndex = createAction('flushContextAfterIndex'); export const fetchContext = createThunk( 'fetchContext', - ({ dispatch, getState }, index, element, fullRefresh = false) => { - const chain = get(element, 'ast.chain'); + ({ dispatch, getState }, index, element, fullRefresh = false, path) => { + const pathToTarget = [...path.split('.'), 'chain']; + const chain = get(element, pathToTarget); const invalidIndex = chain ? index >= chain.length : true; if (!element || !chain || invalidIndex) { @@ -81,22 +82,18 @@ export const fetchContext = createThunk( // cache context as the previous index const contextIndex = index - 1; - const contextPath = [element.id, 'expressionContext', contextIndex]; + const contextPath = [element.id, 'expressionContext', path, contextIndex]; // set context state to loading - dispatch( - args.setLoading({ - path: contextPath, - }) - ); + dispatch(args.setLoading({ path: contextPath })); // function to walk back up to find the closest context available - const getContext = () => getSiblingContext(getState(), element.id, contextIndex - 1); + const getContext = () => getSiblingContext(getState(), element.id, contextIndex - 1, [path]); const { index: prevContextIndex, context: prevContextValue } = fullRefresh !== true ? getContext() : {}; // modify the ast chain passed to the interpreter - const astChain = element.ast.chain.filter((exp, i) => { + const astChain = chain.filter((exp, i) => { if (prevContextValue != null) { return i > prevContextIndex && i < index; } @@ -104,22 +101,10 @@ export const fetchContext = createThunk( }); const variables = getWorkpadVariablesAsObject(getState()); - + const elementWithNewAst = set(element, pathToTarget, astChain); // get context data from a partial AST - return interpretAst( - { - ...element.ast, - chain: astChain, - }, - variables, - prevContextValue - ).then((value) => { - dispatch( - args.setValue({ - path: contextPath, - value, - }) - ); + return interpretAst(elementWithNewAst.ast, variables, prevContextValue).then((value) => { + dispatch(args.setValue({ path: contextPath, value })); }); } ); @@ -359,57 +344,71 @@ export const setAstAtIndex = createThunk( } ); -// index here is the top-level argument in the expression. for example in the expression -// demodata().pointseries().plot(), demodata is 0, pointseries is 1, and plot is 2 -// argIndex is the index in multi-value arguments, and is optional. excluding it will cause -// the entire argument from be set to the passed value -export const setArgumentAtIndex = createThunk('setArgumentAtIndex', ({ dispatch }, args) => { - const { index, argName, value, valueIndex, element, pageId } = args; - let selector = `ast.chain.${index}.arguments.${argName}`; +/** + * Updating the value of the given argument of the element's expression. + * @param {string} args.path - the path to the argument at the AST. Example: "ast.chain.0.arguments.some_arg.chain.1.arguments". + * @param {string} args.argName - the argument name at the AST. + * @param {number} args.valueIndex - the index of the value in the array of argument's values. + * @param {any} args.value - the value to be set to the AST. + * @param {any} args.element - the element, which contains the expression. + * @param {any} args.pageId - the workpad's page, where element is located. + */ +export const setArgument = createThunk('setArgument', ({ dispatch }, args) => { + const { argName, value, valueIndex, element, pageId, path } = args; + let selector = `${path}.${argName}`; if (valueIndex != null) { selector += '.' + valueIndex; } const newElement = set(element, selector, value); - const newAst = get(newElement, ['ast', 'chain', index]); - dispatch(setAstAtIndex(index, newAst, element, pageId)); + const pathTerms = path.split('.'); + const argumentChainPath = pathTerms.slice(0, 3); + const argumnentChainIndex = last(argumentChainPath); + const newAst = get(newElement, argumentChainPath); + dispatch(setAstAtIndex(argumnentChainIndex, newAst, element, pageId)); }); -// index here is the top-level argument in the expression. for example in the expression -// demodata().pointseries().plot(), demodata is 0, pointseries is 1, and plot is 2 -export const addArgumentValueAtIndex = createThunk( - 'addArgumentValueAtIndex', - ({ dispatch }, args) => { - const { index, argName, value, element } = args; - - const values = get(element, ['ast', 'chain', index, 'arguments', argName], []); - const newValue = values.concat(value); - - dispatch( - setArgumentAtIndex({ - ...args, - value: newValue, - }) - ); - } -); +/** + * Adding the value to the given argument of the element's expression. + * @param {string} args.path - the path to the argument at the AST. Example: "ast.chain.0.arguments.some_arg.chain.1.arguments". + * @param {string} args.argName - the argument name at the given path of the AST. + * @param {any} args.value - the value to be added to the array of argument's values at the AST. + * @param {any} args.element - the element, which contains the expression. + * @param {any} args.pageId - the workpad's page, where element is located. + */ +export const addArgumentValue = createThunk('addArgumentValue', ({ dispatch }, args) => { + const { argName, value, element, path } = args; + const values = get(element, [...path.split('.'), argName], []); + const newValue = values.concat(value); + dispatch( + setArgument({ + ...args, + value: newValue, + }) + ); +}); -// index here is the top-level argument in the expression. for example in the expression -// demodata().pointseries().plot(), demodata is 0, pointseries is 1, and plot is 2 -// argIndex is the index in multi-value arguments, and is optional. excluding it will remove -// the entire argument from the expresion export const deleteArgumentAtIndex = createThunk('deleteArgumentAtIndex', ({ dispatch }, args) => { - const { index, element, pageId, argName, argIndex } = args; - const curVal = get(element, ['ast', 'chain', index, 'arguments', argName]); - - const newElement = + const { element, pageId, argName, argIndex, path } = args; + const pathTerms = path.split('.'); + const argumentChainPath = pathTerms.slice(0, 3); + const argumnentChainIndex = last(argumentChainPath); + const curVal = get(element, [...pathTerms, argName]); + let newElement = argIndex != null && curVal.length > 1 ? // if more than one val, remove the specified val - del(element, `ast.chain.${index}.arguments.${argName}.${argIndex}`) + del(element, `${path}.${argName}.${argIndex}`) : // otherwise, remove the entire key - del(element, `ast.chain.${index}.arguments.${argName}`); + del(element, argName ? `${path}.${argName}` : path); + + const parentPath = pathTerms.slice(0, pathTerms.length - 1); + const updatedArgument = get(newElement, parentPath); + + if (Array.isArray(updatedArgument) && !updatedArgument.length) { + newElement = del(element, parentPath); + } - dispatch(setAstAtIndex(index, get(newElement, ['ast', 'chain', index]), element, pageId)); + dispatch(setAstAtIndex(argumnentChainIndex, get(newElement, argumentChainPath), element, pageId)); }); /* diff --git a/x-pack/plugins/canvas/public/state/actions/elements.test.js b/x-pack/plugins/canvas/public/state/actions/elements.test.js index f21567fd0677..491f9716b078 100644 --- a/x-pack/plugins/canvas/public/state/actions/elements.test.js +++ b/x-pack/plugins/canvas/public/state/actions/elements.test.js @@ -67,41 +67,48 @@ describe('getSiblingContext', () => { }, }; - it('should find context when a previous context value is found', () => { - // pointseries map - expect(getSiblingContext(state, 'element-foo', 2)).toEqual({ - index: 2, - context: { - type: 'pointseries', - columns: { - x: { type: 'string', role: 'dimension', expression: 'cost' }, - y: { type: 'string', role: 'dimension', expression: 'project' }, - color: { type: 'string', role: 'dimension', expression: 'project' }, + const stateWithDefaultPath = { + transient: { + resolvedArgs: { + 'element-foo': { + expressionContext: { + 'ast.chain': state.transient.resolvedArgs['element-foo'].expressionContext, + }, }, - rows: [ - { x: '200', y: 'tigers', color: 'tigers' }, - { x: '500', y: 'pandas', color: 'pandas' }, - ], }, - }); + }, + }; + + const expectedElement = { + index: 2, + context: { + type: 'pointseries', + columns: { + x: { type: 'string', role: 'dimension', expression: 'cost' }, + y: { type: 'string', role: 'dimension', expression: 'project' }, + color: { type: 'string', role: 'dimension', expression: 'project' }, + }, + rows: [ + { x: '200', y: 'tigers', color: 'tigers' }, + { x: '500', y: 'pandas', color: 'pandas' }, + ], + }, + }; + it('should find context when a previous context value is found', () => { + // pointseries map + expect(getSiblingContext(state, 'element-foo', 2, [])).toEqual(expectedElement); + expect(getSiblingContext(stateWithDefaultPath, 'element-foo', 2)).toEqual(expectedElement); + expect(getSiblingContext(stateWithDefaultPath, 'element-foo', 2, ['ast.chain'])).toEqual( + expectedElement + ); }); it('should find context when a previous context value is not found', () => { // pointseries map - expect(getSiblingContext(state, 'element-foo', 1000)).toEqual({ - index: 2, - context: { - type: 'pointseries', - columns: { - x: { type: 'string', role: 'dimension', expression: 'cost' }, - y: { type: 'string', role: 'dimension', expression: 'project' }, - color: { type: 'string', role: 'dimension', expression: 'project' }, - }, - rows: [ - { x: '200', y: 'tigers', color: 'tigers' }, - { x: '500', y: 'pandas', color: 'pandas' }, - ], - }, - }); + expect(getSiblingContext(state, 'element-foo', 1000, [])).toEqual(expectedElement); + expect(getSiblingContext(stateWithDefaultPath, 'element-foo', 1000)).toEqual(expectedElement); + expect(getSiblingContext(stateWithDefaultPath, 'element-foo', 1000, ['ast.chain'])).toEqual( + expectedElement + ); }); }); diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/plugins/canvas/public/state/selectors/workpad.ts index a2c001d58ced..ac94ccc562e8 100644 --- a/x-pack/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/plugins/canvas/public/state/selectors/workpad.ts @@ -456,7 +456,7 @@ export function getResolvedArgs(state: State, elementId: string, path: any): any return args; } -export function getSelectedResolvedArgs(state: State, path: any): any { +export function getSelectedResolvedArgs(state: State, path: Array): any { const elementId = getSelectedElementId(state); if (elementId) { @@ -464,8 +464,12 @@ export function getSelectedResolvedArgs(state: State, path: any): any { } } -export function getContextForIndex(state: State, index: number): ExpressionContext { - return getSelectedResolvedArgs(state, ['expressionContext', index - 1]); +export function getContextForIndex( + state: State, + parentPath: string, + index: number +): ExpressionContext { + return getSelectedResolvedArgs(state, ['expressionContext', parentPath, index - 1]); } export function getRefreshInterval(state: State): number { diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts b/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts index 6e61bb684fa9..9a463efae6a2 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts @@ -432,14 +432,10 @@ describe('heatmap', () => { // grid strokeWidth: [], strokeColor: [], - cellHeight: [], - cellWidth: [], // cells isCellLabelVisible: [false], // Y-axis isYAxisLabelVisible: [true], - yAxisLabelWidth: [], - yAxisLabelColor: [], // X-axis isXAxisLabelVisible: [true], }, diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx index 0ba31b7f1523..108e9b3ffb95 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx @@ -356,18 +356,10 @@ export const getHeatmapVisualization = ({ strokeColor: state.gridConfig.strokeColor ? [state.gridConfig.strokeColor] : [], - cellHeight: state.gridConfig.cellHeight ? [state.gridConfig.cellHeight] : [], - cellWidth: state.gridConfig.cellWidth ? [state.gridConfig.cellWidth] : [], // cells isCellLabelVisible: [state.gridConfig.isCellLabelVisible], // Y-axis isYAxisLabelVisible: [state.gridConfig.isYAxisLabelVisible], - yAxisLabelWidth: state.gridConfig.yAxisLabelWidth - ? [state.gridConfig.yAxisLabelWidth] - : [], - yAxisLabelColor: state.gridConfig.yAxisLabelColor - ? [state.gridConfig.yAxisLabelColor] - : [], // X-axis isXAxisLabelVisible: state.gridConfig.isXAxisLabelVisible ? [state.gridConfig.isXAxisLabelVisible] diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 26667333750b..7ba2e66a8140 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1956,15 +1956,11 @@ "expressionMetricVis.function.metric.help": "メトリックディメンションの構成です。", "expressionMetricVis.function.percentageMode.help": "百分率モードでメトリックを表示します。colorRange を設定する必要があります。", "expressionMetricVis.function.showLabels.help": "メトリック値の下にラベルを表示します。", - "expressionHeatmap.function.args.grid.cellHeight.help": "指定网格单元格高度", - "expressionHeatmap.function.args.grid.cellWidth.help": "指定网格单元格宽度", "expressionHeatmap.function.args.grid.isCellLabelVisible.help": "指定单元格标签是否可见。", "expressionHeatmap.function.args.grid.isXAxisLabelVisible.help": "指定 X 轴标签是否可见。", "expressionHeatmap.function.args.grid.isYAxisLabelVisible.help": "指定 Y 轴标签是否可见。", "expressionHeatmap.function.args.grid.strokeColor.help": "指定网格笔画颜色", "expressionHeatmap.function.args.grid.strokeWidth.help": "指定网格笔画宽度", - "expressionHeatmap.function.args.grid.yAxisLabelColor.help": "指定 Y 轴标签的颜色。", - "expressionHeatmap.function.args.grid.yAxisLabelWidth.help": "指定 Y 轴标签的宽度。", "expressionHeatmap.function.args.legend.isVisible.help": "指定图例是否可见。", "expressionHeatmap.function.args.legend.maxLines.help": "指定每个图例项的行数。", "expressionHeatmap.function.args.legend.position.help": "指定图例位置。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6ce32cd32f3c..8872694177f5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1964,15 +1964,11 @@ "expressionMetric.functions.metricHelpText": "在标签上显示数字。", "expressionMetric.renderer.metric.displayName": "指标", "expressionMetric.renderer.metric.helpDescription": "在标签上呈现数字", - "expressionHeatmap.function.args.grid.cellHeight.help": "指定网格单元格高度", - "expressionHeatmap.function.args.grid.cellWidth.help": "指定网格单元格宽度", "expressionHeatmap.function.args.grid.isCellLabelVisible.help": "指定单元格标签是否可见。", "expressionHeatmap.function.args.grid.isXAxisLabelVisible.help": "指定 X 轴标签是否可见。", "expressionHeatmap.function.args.grid.isYAxisLabelVisible.help": "指定 Y 轴标签是否可见。", "expressionHeatmap.function.args.grid.strokeColor.help": "指定网格笔画颜色", "expressionHeatmap.function.args.grid.strokeWidth.help": "指定网格笔画宽度", - "expressionHeatmap.function.args.grid.yAxisLabelColor.help": "指定 Y 轴标签的颜色。", - "expressionHeatmap.function.args.grid.yAxisLabelWidth.help": "指定 Y 轴标签的宽度。", "expressionHeatmap.function.args.legend.isVisible.help": "指定图例是否可见。", "expressionHeatmap.function.args.legend.maxLines.help": "指定每个图例项的行数。", "expressionHeatmap.function.args.legend.position.help": "指定图例位置。",