mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Canvas] HeatMap. (#120239)
* Added defaults to expressions. * Fixed error on number passed as a name. * One more fix. * Added Heatmap element to Canvas. * Added support of nested expressions. * Added support of the adding models/views as arguments of the expressions. * Added support of the name from parent configuration. * Added support of removing nested models. * Added heatmap legend description) * Replaced help and displayName of legend. * Added heatmap_grid. * Fixed label. * Added context of nested expressions support. * Fixed bugs with updating of elements. * Added color picker. * Make color compressed * Added usable inputs with good user experience. * Reduced number of props, passing to the arg.. * Percentage and range args with debounce/ * Removed not used args from heatmap_grid * fixed arg name. * Fixed storybooks. * Fixed one more story. * Fixed unused args from lens. * Added comments to the recursive function. * Added docs to the transformNestedFunctionsToUIConfig * Removed not used translations. * Fixed tests. * Added rest of arguments. * Fixed args defaults generating. * Fixed tests of lens. * Changed '@kbn/interpreter/common' to '@kbn/interpreter'. * Changed names of setArgumentAtIndex and addArgumentValueAtIndex and changed comments. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
0493f00d7f
commit
dd9d8461d2
51 changed files with 1269 additions and 387 deletions
|
@ -17,7 +17,10 @@ import {
|
||||||
EXPRESSION_HEATMAP_LEGEND_NAME,
|
EXPRESSION_HEATMAP_LEGEND_NAME,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
|
||||||
const convertToVisDimension = (columns: DatatableColumn[], accessor: string) => {
|
const convertToVisDimension = (
|
||||||
|
columns: DatatableColumn[],
|
||||||
|
accessor: string
|
||||||
|
): ExpressionValueVisDimension | undefined => {
|
||||||
const column = columns.find((c) => c.id === accessor);
|
const column = columns.find((c) => c.id === accessor);
|
||||||
if (!column) return;
|
if (!column) return;
|
||||||
return {
|
return {
|
||||||
|
@ -27,7 +30,7 @@ const convertToVisDimension = (columns: DatatableColumn[], accessor: string) =>
|
||||||
params: { ...column.meta.params?.params },
|
params: { ...column.meta.params?.params },
|
||||||
},
|
},
|
||||||
type: 'vis_dimension',
|
type: 'vis_dimension',
|
||||||
} as ExpressionValueVisDimension;
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const prepareHeatmapLogTable = (
|
const prepareHeatmapLogTable = (
|
||||||
|
@ -70,12 +73,14 @@ export const heatmapFunction = (): HeatmapExpressionFunctionDefinition => ({
|
||||||
help: i18n.translate('expressionHeatmap.function.legendConfig.help', {
|
help: i18n.translate('expressionHeatmap.function.legendConfig.help', {
|
||||||
defaultMessage: 'Configure the chart legend.',
|
defaultMessage: 'Configure the chart legend.',
|
||||||
}),
|
}),
|
||||||
|
default: `{${EXPRESSION_HEATMAP_LEGEND_NAME}}`,
|
||||||
},
|
},
|
||||||
gridConfig: {
|
gridConfig: {
|
||||||
types: [EXPRESSION_HEATMAP_GRID_NAME],
|
types: [EXPRESSION_HEATMAP_GRID_NAME],
|
||||||
help: i18n.translate('expressionHeatmap.function.gridConfig.help', {
|
help: i18n.translate('expressionHeatmap.function.gridConfig.help', {
|
||||||
defaultMessage: 'Configure the heatmap layout.',
|
defaultMessage: 'Configure the heatmap layout.',
|
||||||
}),
|
}),
|
||||||
|
default: `{${EXPRESSION_HEATMAP_GRID_NAME}}`,
|
||||||
},
|
},
|
||||||
showTooltip: {
|
showTooltip: {
|
||||||
types: ['boolean'],
|
types: ['boolean'],
|
||||||
|
@ -118,6 +123,7 @@ export const heatmapFunction = (): HeatmapExpressionFunctionDefinition => ({
|
||||||
help: i18n.translate('expressionHeatmap.function.args.valueAccessorHelpText', {
|
help: i18n.translate('expressionHeatmap.function.args.valueAccessorHelpText', {
|
||||||
defaultMessage: 'The id of the value column or the corresponding dimension',
|
defaultMessage: 'The id of the value column or the corresponding dimension',
|
||||||
}),
|
}),
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
// not supported yet, small multiples accessor
|
// not supported yet, small multiples accessor
|
||||||
splitRowAccessor: {
|
splitRowAccessor: {
|
||||||
|
|
|
@ -20,7 +20,7 @@ export const heatmapGridConfig: ExpressionFunctionDefinition<
|
||||||
name: EXPRESSION_HEATMAP_GRID_NAME,
|
name: EXPRESSION_HEATMAP_GRID_NAME,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
type: EXPRESSION_HEATMAP_GRID_NAME,
|
type: EXPRESSION_HEATMAP_GRID_NAME,
|
||||||
help: `Configure the heatmap layout `,
|
help: `Configure the heatmap layout`,
|
||||||
inputTypes: ['null'],
|
inputTypes: ['null'],
|
||||||
args: {
|
args: {
|
||||||
// grid
|
// grid
|
||||||
|
@ -38,20 +38,6 @@ export const heatmapGridConfig: ExpressionFunctionDefinition<
|
||||||
}),
|
}),
|
||||||
required: false,
|
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
|
// cells
|
||||||
isCellLabelVisible: {
|
isCellLabelVisible: {
|
||||||
types: ['boolean'],
|
types: ['boolean'],
|
||||||
|
@ -66,20 +52,6 @@ export const heatmapGridConfig: ExpressionFunctionDefinition<
|
||||||
defaultMessage: 'Specifies whether or not the Y-axis labels are visible.',
|
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
|
// X-axis
|
||||||
isXAxisLabelVisible: {
|
isXAxisLabelVisible: {
|
||||||
types: ['boolean'],
|
types: ['boolean'],
|
||||||
|
|
|
@ -48,14 +48,10 @@ export interface HeatmapGridConfig {
|
||||||
// grid
|
// grid
|
||||||
strokeWidth?: number;
|
strokeWidth?: number;
|
||||||
strokeColor?: string;
|
strokeColor?: string;
|
||||||
cellHeight?: number;
|
|
||||||
cellWidth?: number;
|
|
||||||
// cells
|
// cells
|
||||||
isCellLabelVisible: boolean;
|
isCellLabelVisible: boolean;
|
||||||
// Y-axis
|
// Y-axis
|
||||||
isYAxisLabelVisible: boolean;
|
isYAxisLabelVisible: boolean;
|
||||||
yAxisLabelWidth?: number;
|
|
||||||
yAxisLabelColor?: string;
|
|
||||||
// X-axis
|
// X-axis
|
||||||
isXAxisLabelVisible: boolean;
|
isXAxisLabelVisible: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -258,7 +258,7 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
|
||||||
const percentageNumber = (Math.abs(value - min) / (max - min)) * 100;
|
const percentageNumber = (Math.abs(value - min) / (max - min)) * 100;
|
||||||
value = parseInt(percentageNumber.toString(), 10) / 100;
|
value = parseInt(percentageNumber.toString(), 10) / 100;
|
||||||
}
|
}
|
||||||
return metricFormatter.convert(value);
|
return `${metricFormatter.convert(value) ?? ''}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { colors, ranges } = computeColorRanges(
|
const { colors, ranges } = computeColorRanges(
|
||||||
|
@ -415,7 +415,8 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
|
||||||
name: yAxisColumn?.name ?? '',
|
name: yAxisColumn?.name ?? '',
|
||||||
...(yAxisColumn
|
...(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<HeatmapRenderProps> = memo(
|
||||||
// eui color subdued
|
// eui color subdued
|
||||||
textColor: chartTheme.axes?.tickLabel?.fill ?? `#6a717d`,
|
textColor: chartTheme.axes?.tickLabel?.fill ?? `#6a717d`,
|
||||||
padding: xAxisColumn?.name ? 8 : 0,
|
padding: xAxisColumn?.name ? 8 : 0,
|
||||||
formatter: (v: number | string) => xValuesFormatter.convert(v),
|
formatter: (v: number | string) => `${xValuesFormatter.convert(v) ?? ''}`,
|
||||||
name: xAxisColumn?.name ?? '',
|
name: xAxisColumn?.name ?? '',
|
||||||
},
|
},
|
||||||
brushMask: {
|
brushMask: {
|
||||||
|
|
|
@ -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`,
|
||||||
|
});
|
|
@ -33,6 +33,7 @@ import { verticalProgressBar } from './vertical_progress_bar';
|
||||||
import { verticalProgressPill } from './vertical_progress_pill';
|
import { verticalProgressPill } from './vertical_progress_pill';
|
||||||
import { tagCloud } from './tag_cloud';
|
import { tagCloud } from './tag_cloud';
|
||||||
import { metricVis } from './metric_vis';
|
import { metricVis } from './metric_vis';
|
||||||
|
import { heatmap } from './heatmap';
|
||||||
|
|
||||||
import { SetupInitializer } from '../plugin';
|
import { SetupInitializer } from '../plugin';
|
||||||
import { ElementFactory } from '../../types';
|
import { ElementFactory } from '../../types';
|
||||||
|
@ -63,6 +64,7 @@ const elementSpecs = [
|
||||||
verticalProgressBar,
|
verticalProgressBar,
|
||||||
verticalProgressPill,
|
verticalProgressPill,
|
||||||
tagCloud,
|
tagCloud,
|
||||||
|
heatmap,
|
||||||
];
|
];
|
||||||
|
|
||||||
const initializeElementFactories = [metricElementInitializer];
|
const initializeElementFactories = [metricElementInitializer];
|
||||||
|
|
|
@ -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<Props> = ({ onValueChange, argValue }) => {
|
||||||
|
const [color, setColor, errors] = useColorPickerState(argValue);
|
||||||
|
|
||||||
|
const pickColor: EuiSetColorMethod = (value, meta) => {
|
||||||
|
setColor(value, meta);
|
||||||
|
onValueChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFlexGroup gutterSize="s">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiColorPicker compressed onChange={pickColor} color={color} isInvalid={!!errors} />
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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"',
|
||||||
|
});
|
|
@ -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';
|
|
@ -39,7 +39,7 @@ const getMathValue = (argValue, columns) => {
|
||||||
// TODO: Garbage, we could make a much nicer math form that can handle way more.
|
// TODO: Garbage, we could make a much nicer math form that can handle way more.
|
||||||
const DatacolumnArgInput = ({
|
const DatacolumnArgInput = ({
|
||||||
onValueChange,
|
onValueChange,
|
||||||
columns,
|
resolved: { columns },
|
||||||
argValue,
|
argValue,
|
||||||
renderError,
|
renderError,
|
||||||
argId,
|
argId,
|
||||||
|
@ -123,7 +123,9 @@ const DatacolumnArgInput = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
DatacolumnArgInput.propTypes = {
|
DatacolumnArgInput.propTypes = {
|
||||||
columns: PropTypes.array.isRequired,
|
resolved: PropTypes.shape({
|
||||||
|
columns: PropTypes.array.isRequired,
|
||||||
|
}).isRequired,
|
||||||
onValueChange: PropTypes.func.isRequired,
|
onValueChange: PropTypes.func.isRequired,
|
||||||
typeInstance: PropTypes.object.isRequired,
|
typeInstance: PropTypes.object.isRequired,
|
||||||
renderError: PropTypes.func.isRequired,
|
renderError: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { textarea } from './textarea';
|
||||||
// @ts-expect-error untyped local
|
// @ts-expect-error untyped local
|
||||||
import { toggle } from './toggle';
|
import { toggle } from './toggle';
|
||||||
import { visdimension } from './vis_dimension';
|
import { visdimension } from './vis_dimension';
|
||||||
|
import { colorPicker } from './color_picker';
|
||||||
|
|
||||||
import { SetupInitializer } from '../../plugin';
|
import { SetupInitializer } from '../../plugin';
|
||||||
|
|
||||||
|
@ -51,6 +52,7 @@ export const args = [
|
||||||
textarea,
|
textarea,
|
||||||
toggle,
|
toggle,
|
||||||
visdimension,
|
visdimension,
|
||||||
|
colorPicker,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const initializers = [dateFormatInitializer, numberFormatInitializer];
|
export const initializers = [dateFormatInitializer, numberFormatInitializer];
|
||||||
|
|
|
@ -9,6 +9,7 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { EuiFieldNumber, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
import { EuiFieldNumber, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||||
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
||||||
|
import { withDebounceArg } from '../../../public/components/with_debounce_arg';
|
||||||
import { ArgumentStrings } from '../../../i18n';
|
import { ArgumentStrings } from '../../../i18n';
|
||||||
|
|
||||||
const { Number: strings } = ArgumentStrings;
|
const { Number: strings } = ArgumentStrings;
|
||||||
|
@ -28,8 +29,11 @@ const NumberArgInput = ({ argId, argValue, typeInstance, onValueChange }) => {
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(ev) => {
|
(ev) => {
|
||||||
const onChangeFn = confirm ? setValue : onValueChange;
|
const { value } = ev.target;
|
||||||
onChangeFn(ev.target.value);
|
setValue(value);
|
||||||
|
if (!confirm) {
|
||||||
|
onValueChange(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[confirm, onValueChange]
|
[confirm, onValueChange]
|
||||||
);
|
);
|
||||||
|
@ -62,6 +66,6 @@ export const number = () => ({
|
||||||
name: 'number',
|
name: 'number',
|
||||||
displayName: strings.getDisplayName(),
|
displayName: strings.getDisplayName(),
|
||||||
help: strings.getHelp(),
|
help: strings.getHelp(),
|
||||||
simpleTemplate: templateFromReactComponent(NumberArgInput),
|
simpleTemplate: templateFromReactComponent(withDebounceArg(NumberArgInput)),
|
||||||
default: '0',
|
default: '0',
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,18 +5,27 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { EuiRange } from '@elastic/eui';
|
import { EuiRange } from '@elastic/eui';
|
||||||
|
import { withDebounceArg } from '../../../public/components/with_debounce_arg';
|
||||||
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
||||||
import { ArgumentStrings } from '../../../i18n';
|
import { ArgumentStrings } from '../../../i18n';
|
||||||
|
|
||||||
const { Percentage: strings } = ArgumentStrings;
|
const { Percentage: strings } = ArgumentStrings;
|
||||||
|
|
||||||
const PercentageArgInput = ({ onValueChange, argValue }) => {
|
const PercentageArgInput = ({ onValueChange, argValue }) => {
|
||||||
const handleChange = (ev) => {
|
const [value, setValue] = useState(argValue);
|
||||||
return onValueChange(ev.target.value / 100);
|
|
||||||
};
|
const handleChange = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
const { value } = ev.target;
|
||||||
|
const numberVal = Number(value) / 100;
|
||||||
|
setValue(numberVal);
|
||||||
|
onValueChange(numberVal);
|
||||||
|
},
|
||||||
|
[onValueChange]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiRange
|
<EuiRange
|
||||||
|
@ -25,7 +34,7 @@ const PercentageArgInput = ({ onValueChange, argValue }) => {
|
||||||
max={100}
|
max={100}
|
||||||
showLabels
|
showLabels
|
||||||
showInput
|
showInput
|
||||||
value={argValue * 100}
|
value={value * 100}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -41,5 +50,5 @@ export const percentage = () => ({
|
||||||
name: 'percentage',
|
name: 'percentage',
|
||||||
displayName: strings.getDisplayName(),
|
displayName: strings.getDisplayName(),
|
||||||
help: strings.getHelp(),
|
help: strings.getHelp(),
|
||||||
simpleTemplate: templateFromReactComponent(PercentageArgInput),
|
simpleTemplate: templateFromReactComponent(withDebounceArg(PercentageArgInput, 50)),
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,19 +5,28 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { EuiRange } from '@elastic/eui';
|
import { EuiRange } from '@elastic/eui';
|
||||||
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
||||||
|
import { withDebounceArg } from '../../../public/components/with_debounce_arg';
|
||||||
import { ArgumentStrings } from '../../../i18n';
|
import { ArgumentStrings } from '../../../i18n';
|
||||||
|
|
||||||
const { Range: strings } = ArgumentStrings;
|
const { Range: strings } = ArgumentStrings;
|
||||||
|
|
||||||
const RangeArgInput = ({ typeInstance, onValueChange, argValue }) => {
|
const RangeArgInput = ({ typeInstance, onValueChange, argValue }) => {
|
||||||
const { min, max, step } = typeInstance.options;
|
const { min, max, step } = typeInstance.options;
|
||||||
const handleChange = (ev) => {
|
const [value, setValue] = useState(argValue);
|
||||||
return onValueChange(Number(ev.target.value));
|
|
||||||
};
|
const handleChange = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
const { value } = ev.target;
|
||||||
|
const numberVal = Number(value);
|
||||||
|
setValue(numberVal);
|
||||||
|
onValueChange(numberVal);
|
||||||
|
},
|
||||||
|
[onValueChange]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiRange
|
<EuiRange
|
||||||
|
@ -27,7 +36,7 @@ const RangeArgInput = ({ typeInstance, onValueChange, argValue }) => {
|
||||||
step={step}
|
step={step}
|
||||||
showLabels
|
showLabels
|
||||||
showInput
|
showInput
|
||||||
value={argValue}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -50,5 +59,5 @@ export const range = () => ({
|
||||||
name: 'range',
|
name: 'range',
|
||||||
displayName: strings.getDisplayName(),
|
displayName: strings.getDisplayName(),
|
||||||
help: strings.getHelp(),
|
help: strings.getHelp(),
|
||||||
simpleTemplate: templateFromReactComponent(RangeArgInput),
|
simpleTemplate: templateFromReactComponent(withDebounceArg(RangeArgInput, 50)),
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,8 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { EuiFlexItem, EuiFlexGroup, EuiFieldText, EuiButton } from '@elastic/eui';
|
import { EuiFlexItem, EuiFlexGroup, EuiFieldText, EuiButton } from '@elastic/eui';
|
||||||
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
||||||
|
import { withDebounceArg } from '../../../public/components/with_debounce_arg';
|
||||||
|
|
||||||
import { ArgumentStrings } from '../../../i18n';
|
import { ArgumentStrings } from '../../../i18n';
|
||||||
|
|
||||||
const { String: strings } = ArgumentStrings;
|
const { String: strings } = ArgumentStrings;
|
||||||
|
@ -23,8 +25,11 @@ const StringArgInput = ({ argValue, typeInstance, onValueChange, argId }) => {
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(ev) => {
|
(ev) => {
|
||||||
const onChangeFn = confirm ? setValue : onValueChange;
|
const { value } = ev.target;
|
||||||
onChangeFn(ev.target.value);
|
setValue(value);
|
||||||
|
if (!confirm) {
|
||||||
|
onValueChange(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[confirm, onValueChange]
|
[confirm, onValueChange]
|
||||||
);
|
);
|
||||||
|
@ -56,5 +61,5 @@ export const string = () => ({
|
||||||
name: 'string',
|
name: 'string',
|
||||||
displayName: strings.getDisplayName(),
|
displayName: strings.getDisplayName(),
|
||||||
help: strings.getHelp(),
|
help: strings.getHelp(),
|
||||||
simpleTemplate: templateFromReactComponent(StringArgInput),
|
simpleTemplate: templateFromReactComponent(withDebounceArg(StringArgInput)),
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,18 +9,22 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { EuiFormRow, EuiTextArea, EuiSpacer, EuiButton } from '@elastic/eui';
|
import { EuiFormRow, EuiTextArea, EuiSpacer, EuiButton } from '@elastic/eui';
|
||||||
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
||||||
|
import { withDebounceArg } from '../../../public/components/with_debounce_arg';
|
||||||
import { ArgumentStrings } from '../../../i18n';
|
import { ArgumentStrings } from '../../../i18n';
|
||||||
|
|
||||||
const { Textarea: strings } = ArgumentStrings;
|
const { Textarea: strings } = ArgumentStrings;
|
||||||
|
|
||||||
const TextAreaArgInput = ({ argValue, typeInstance, onValueChange, renderError, argId }) => {
|
const TextAreaArgInput = ({ argValue, typeInstance, onValueChange, renderError, argId }) => {
|
||||||
const confirm = typeInstance?.options?.confirm;
|
const confirm = typeInstance?.options?.confirm;
|
||||||
const [value, setValue] = useState();
|
const [value, setValue] = useState(argValue);
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(ev) => {
|
(ev) => {
|
||||||
const onChangeFn = confirm ? setValue : onValueChange;
|
const { value } = ev.target;
|
||||||
onChangeFn(ev.target.value);
|
setValue(value);
|
||||||
|
if (!confirm) {
|
||||||
|
onValueChange(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[confirm, onValueChange]
|
[confirm, onValueChange]
|
||||||
);
|
);
|
||||||
|
@ -68,5 +72,5 @@ export const textarea = () => ({
|
||||||
name: 'textarea',
|
name: 'textarea',
|
||||||
displayName: strings.getDisplayName(),
|
displayName: strings.getDisplayName(),
|
||||||
help: strings.getHelp(),
|
help: strings.getHelp(),
|
||||||
template: templateFromReactComponent(TextAreaArgInput),
|
template: templateFromReactComponent(withDebounceArg(TextAreaArgInput)),
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,27 +10,25 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui';
|
||||||
import { DatatableColumn, ExpressionAstExpression } from 'src/plugins/expressions';
|
import { DatatableColumn, ExpressionAstExpression } from 'src/plugins/expressions';
|
||||||
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
||||||
import { ArgumentStrings } from '../../../i18n';
|
import { ArgumentStrings } from '../../../i18n';
|
||||||
|
import { ResolvedArgProps, ResolvedColumns } from '../../../public/expression_types/arg';
|
||||||
|
|
||||||
const { VisDimension: strings } = ArgumentStrings;
|
const { VisDimension: strings } = ArgumentStrings;
|
||||||
|
|
||||||
interface VisDimensionArgInputProps {
|
type VisDimensionArgInputProps = {
|
||||||
onValueChange: (value: ExpressionAstExpression) => void;
|
onValueChange: (value: ExpressionAstExpression) => void;
|
||||||
argValue: ExpressionAstExpression;
|
argValue: ExpressionAstExpression;
|
||||||
argId?: string;
|
|
||||||
columns: DatatableColumn[];
|
|
||||||
typeInstance: {
|
typeInstance: {
|
||||||
options?: {
|
options?: {
|
||||||
confirm?: string;
|
confirm?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
} & ResolvedArgProps<ResolvedColumns>;
|
||||||
|
|
||||||
const VisDimensionArgInput: React.FC<VisDimensionArgInputProps> = ({
|
const VisDimensionArgInput: React.FC<VisDimensionArgInputProps> = ({
|
||||||
argValue,
|
argValue,
|
||||||
typeInstance,
|
typeInstance,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
argId,
|
resolved: { columns },
|
||||||
columns,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [value, setValue] = useState(argValue);
|
const [value, setValue] = useState(argValue);
|
||||||
const confirm = typeInstance?.options?.confirm;
|
const confirm = typeInstance?.options?.confirm;
|
||||||
|
@ -75,7 +73,7 @@ const VisDimensionArgInput: React.FC<VisDimensionArgInputProps> = ({
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup gutterSize="s" direction="column">
|
<EuiFlexGroup gutterSize="s" direction="column">
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiSelect options={options} value={column} onChange={onChange} />
|
<EuiSelect compressed options={options} value={column} onChange={onChange} />
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
{confirm && (
|
{confirm && (
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
|
|
|
@ -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', []) };
|
||||||
|
},
|
||||||
|
});
|
|
@ -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', []) };
|
||||||
|
},
|
||||||
|
});
|
|
@ -9,5 +9,7 @@ import { pointseries } from './point_series';
|
||||||
import { math } from './math';
|
import { math } from './math';
|
||||||
import { tagcloud } from './tagcloud';
|
import { tagcloud } from './tagcloud';
|
||||||
import { metricVis } from './metric_vis';
|
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];
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
|
import { ResolvedColumns } from '../../../public/expression_types/arg';
|
||||||
|
|
||||||
import { ViewStrings } from '../../../i18n';
|
import { ViewStrings } from '../../../i18n';
|
||||||
import { getState, getValue } from '../../../public/lib/resolved_arg';
|
import { getState, getValue } from '../../../public/lib/resolved_arg';
|
||||||
|
@ -70,7 +71,7 @@ export const metricVis = () => ({
|
||||||
argType: 'toggle',
|
argType: 'toggle',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
resolve({ context }: any) {
|
resolve({ context }: any): ResolvedColumns {
|
||||||
if (getState(context) !== 'ready') {
|
if (getState(context) !== 'ready') {
|
||||||
return { columns: [] };
|
return { columns: [] };
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
|
import { ResolvedColumns } from '../../../public/expression_types/arg';
|
||||||
|
|
||||||
import { ViewStrings } from '../../../i18n';
|
import { ViewStrings } from '../../../i18n';
|
||||||
import { getState, getValue } from '../../../public/lib/resolved_arg';
|
import { getState, getValue } from '../../../public/lib/resolved_arg';
|
||||||
|
@ -82,7 +83,7 @@ export const tagcloud = () => ({
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
resolve({ context }: any) {
|
resolve({ context }: any): ResolvedColumns {
|
||||||
if (getState(context) !== 'ready') {
|
if (getState(context) !== 'ready') {
|
||||||
return { columns: [] };
|
return { columns: [] };
|
||||||
}
|
}
|
||||||
|
|
100
x-pack/plugins/canvas/canvas_plugin_src/uis/views/heatmap.ts
Normal file
100
x-pack/plugins/canvas/canvas_plugin_src/uis/views/heatmap.ts
Normal file
|
@ -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', []) };
|
||||||
|
},
|
||||||
|
});
|
|
@ -32,6 +32,8 @@ import { shape } from './shape';
|
||||||
import { table } from './table';
|
import { table } from './table';
|
||||||
// @ts-expect-error untyped local
|
// @ts-expect-error untyped local
|
||||||
import { timefilterControl } from './timefilterControl';
|
import { timefilterControl } from './timefilterControl';
|
||||||
|
import { heatmap } from './heatmap';
|
||||||
|
|
||||||
import { SetupInitializer } from '../../plugin';
|
import { SetupInitializer } from '../../plugin';
|
||||||
|
|
||||||
export const viewSpecs = [
|
export const viewSpecs = [
|
||||||
|
@ -48,6 +50,7 @@ export const viewSpecs = [
|
||||||
shape,
|
shape,
|
||||||
table,
|
table,
|
||||||
timefilterControl,
|
timefilterControl,
|
||||||
|
heatmap,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const viewInitializers = [metricInitializer];
|
export const viewInitializers = [metricInitializer];
|
||||||
|
|
|
@ -238,4 +238,12 @@ export const getElementStrings = (): ElementStringDict => ({
|
||||||
defaultMessage: 'Metric visualization',
|
defaultMessage: 'Metric visualization',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
heatmap: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.heatmapDisplayName', {
|
||||||
|
defaultMessage: 'Heatmap',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.heatmapHelpText', {
|
||||||
|
defaultMessage: 'Heatmap visualization',
|
||||||
|
}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -244,6 +244,16 @@ export const ArgumentStrings = {
|
||||||
defaultMessage: 'Custom',
|
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: {
|
Percentage: {
|
||||||
getDisplayName: () =>
|
getDisplayName: () =>
|
||||||
i18n.translate('xpack.canvas.uis.arguments.percentageTitle', {
|
i18n.translate('xpack.canvas.uis.arguments.percentageTitle', {
|
||||||
|
@ -591,6 +601,106 @@ export const ModelStrings = {
|
||||||
defaultMessage: 'Data along the vertical axis. Usually a number',
|
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 = {
|
export const TransformStrings = {
|
||||||
|
@ -1349,4 +1459,91 @@ export const ViewStrings = {
|
||||||
defaultMessage: 'Background',
|
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',
|
||||||
|
}),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { EuiButtonIcon } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { Popover } from '../popover';
|
import { Popover } from '../popover';
|
||||||
import { ArgAdd } from '../arg_add';
|
import { ArgAdd } from '../arg_add';
|
||||||
import type { Arg } from '../../expression_types/arg';
|
|
||||||
|
|
||||||
const strings = {
|
const strings = {
|
||||||
getAddAriaLabel: () =>
|
getAddAriaLabel: () =>
|
||||||
|
@ -20,8 +19,10 @@ const strings = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ArgOptions {
|
export interface ArgOptions {
|
||||||
arg: Arg;
|
name?: string;
|
||||||
|
displayName?: string;
|
||||||
|
help?: string;
|
||||||
onValueAdd: () => void;
|
onValueAdd: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,9 +50,9 @@ export const ArgAddPopover: FC<Props> = ({ options }) => {
|
||||||
{({ closePopover }) =>
|
{({ closePopover }) =>
|
||||||
options.map((opt) => (
|
options.map((opt) => (
|
||||||
<ArgAdd
|
<ArgAdd
|
||||||
key={`${opt.arg.name}-add`}
|
key={`${opt.name}-add`}
|
||||||
displayName={opt.arg.displayName ?? ''}
|
displayName={opt.displayName ?? ''}
|
||||||
help={opt.arg.help ?? ''}
|
help={opt.help ?? ''}
|
||||||
onValueAdd={() => {
|
onValueAdd={() => {
|
||||||
opt.onValueAdd();
|
opt.onValueAdd();
|
||||||
closePopover();
|
closePopover();
|
||||||
|
|
|
@ -6,3 +6,4 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { ArgAddPopover } from './arg_add_popover';
|
export { ArgAddPopover } from './arg_add_popover';
|
||||||
|
export type { ArgOptions } from './arg_add_popover';
|
||||||
|
|
|
@ -9,10 +9,11 @@ import React, { useState, useEffect, useCallback, useRef, memo, ReactPortal } fr
|
||||||
import deepEqual from 'react-fast-compare';
|
import deepEqual from 'react-fast-compare';
|
||||||
import usePrevious from 'react-use/lib/usePrevious';
|
import usePrevious from 'react-use/lib/usePrevious';
|
||||||
import useEffectOnce from 'react-use/lib/useEffectOnce';
|
import useEffectOnce from 'react-use/lib/useEffectOnce';
|
||||||
|
import { ExpressionAstExpression, ExpressionValue } from 'src/plugins/expressions';
|
||||||
import { ExpressionFormHandlers } from '../../../common/lib/expression_form_handlers';
|
import { ExpressionFormHandlers } from '../../../common/lib/expression_form_handlers';
|
||||||
import { UpdatePropsRef } from '../../../types/arguments';
|
import { UpdatePropsRef } from '../../../types/arguments';
|
||||||
|
|
||||||
interface ArgTemplateFormProps {
|
export interface ArgTemplateFormProps {
|
||||||
template?: (
|
template?: (
|
||||||
domNode: HTMLElement,
|
domNode: HTMLElement,
|
||||||
config: ArgTemplateFormProps['argumentProps'],
|
config: ArgTemplateFormProps['argumentProps'],
|
||||||
|
@ -24,10 +25,13 @@ interface ArgTemplateFormProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
setLabel: (label: string) => void;
|
setLabel: (label: string) => void;
|
||||||
expand?: boolean;
|
expand?: boolean;
|
||||||
|
argValue: any;
|
||||||
setExpand?: (expand: boolean) => void;
|
setExpand?: (expand: boolean) => void;
|
||||||
onValueRemove?: (argName: string, argIndex: string) => void;
|
onValueRemove?: () => void;
|
||||||
|
onValueChange: (value: any) => void;
|
||||||
resetErrorState: () => void;
|
resetErrorState: () => void;
|
||||||
renderError: () => void;
|
renderError: () => void;
|
||||||
|
argResolver: (ast: ExpressionAstExpression) => Promise<ExpressionValue>;
|
||||||
};
|
};
|
||||||
handlers?: { [key: string]: (...args: any[]) => any };
|
handlers?: { [key: string]: (...args: any[]) => any };
|
||||||
error?: unknown;
|
error?: unknown;
|
||||||
|
|
|
@ -39,6 +39,7 @@ const strings = {
|
||||||
defaultMessage: 'Save',
|
defaultMessage: 'Save',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export class DatasourceComponent extends PureComponent {
|
export class DatasourceComponent extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
args: PropTypes.object.isRequired,
|
args: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { get } from 'lodash';
|
||||||
import { datasourceRegistry } from '../../expression_types';
|
import { datasourceRegistry } from '../../expression_types';
|
||||||
import { getServerFunctions } from '../../state/selectors/app';
|
import { getServerFunctions } from '../../state/selectors/app';
|
||||||
import { getSelectedElement, getSelectedPage } from '../../state/selectors/workpad';
|
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';
|
import { Datasource as Component } from './datasource';
|
||||||
|
|
||||||
const DatasourceComponent = (props) => {
|
const DatasourceComponent = (props) => {
|
||||||
|
@ -52,7 +52,6 @@ const mapStateToProps = (state) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
dispatchArgumentAtIndex: (props) => (arg) => dispatch(setArgumentAtIndex({ ...props, arg })),
|
|
||||||
dispatchAstAtIndex:
|
dispatchAstAtIndex:
|
||||||
({ index, element, pageId }) =>
|
({ index, element, pageId }) =>
|
||||||
(ast) => {
|
(ast) => {
|
||||||
|
@ -63,7 +62,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||||
|
|
||||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||||
const { element, pageId, functionDefinitions } = stateProps;
|
const { element, pageId, functionDefinitions } = stateProps;
|
||||||
const { dispatchArgumentAtIndex, dispatchAstAtIndex } = dispatchProps;
|
const { dispatchAstAtIndex } = dispatchProps;
|
||||||
|
|
||||||
const getDataTableFunctionsByName = (name) =>
|
const getDataTableFunctionsByName = (name) =>
|
||||||
functionDefinitions.find((fn) => fn.name === name && fn.type === 'datatable');
|
functionDefinitions.find((fn) => fn.name === name && fn.type === 'datatable');
|
||||||
|
@ -106,11 +105,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||||
element,
|
element,
|
||||||
index: datasourceAst && datasourceAst.expressionIndex,
|
index: datasourceAst && datasourceAst.expressionIndex,
|
||||||
}),
|
}),
|
||||||
setDatasourceArgs: dispatchArgumentAtIndex({
|
|
||||||
pageId,
|
|
||||||
element,
|
|
||||||
index: datasourceAst && datasourceAst.expressionIndex,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,15 @@ type FunctionFormComponentProps = RenderArgData;
|
||||||
export const FunctionFormComponent: FunctionComponent<FunctionFormComponentProps> = (props) => {
|
export const FunctionFormComponent: FunctionComponent<FunctionFormComponentProps> = (props) => {
|
||||||
const passedProps = {
|
const passedProps = {
|
||||||
name: props.name,
|
name: props.name,
|
||||||
|
removable: props.removable,
|
||||||
argResolver: props.argResolver,
|
argResolver: props.argResolver,
|
||||||
args: props.args,
|
args: props.args,
|
||||||
|
id: props.id,
|
||||||
|
nestedFunctionsArgs: props.nestedFunctionsArgs,
|
||||||
argType: props.argType,
|
argType: props.argType,
|
||||||
argTypeDef: props.argTypeDef,
|
argTypeDef: props.argTypeDef,
|
||||||
filterGroups: props.filterGroups,
|
filterGroups: props.filterGroups,
|
||||||
context: props.context,
|
context: props.context,
|
||||||
expressionIndex: props.expressionIndex,
|
|
||||||
expressionType: props.expressionType,
|
expressionType: props.expressionType,
|
||||||
nextArgType: props.nextArgType,
|
nextArgType: props.nextArgType,
|
||||||
nextExpressionType: props.nextExpressionType,
|
nextExpressionType: props.nextExpressionType,
|
||||||
|
@ -27,6 +29,7 @@ export const FunctionFormComponent: FunctionComponent<FunctionFormComponentProps
|
||||||
onValueAdd: props.onValueAdd,
|
onValueAdd: props.onValueAdd,
|
||||||
onValueChange: props.onValueChange,
|
onValueChange: props.onValueChange,
|
||||||
onValueRemove: props.onValueRemove,
|
onValueRemove: props.onValueRemove,
|
||||||
|
onContainerRemove: props.onContainerRemove,
|
||||||
updateContext: props.updateContext,
|
updateContext: props.updateContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { FC, useCallback, useEffect } from 'react';
|
||||||
import usePrevious from 'react-use/lib/usePrevious';
|
import usePrevious from 'react-use/lib/usePrevious';
|
||||||
import { Loading } from '../loading';
|
import { Loading } from '../loading';
|
||||||
import { CanvasElement, ExpressionContext } from '../../../types';
|
import { CanvasElement, ExpressionContext } from '../../../types';
|
||||||
|
@ -18,9 +18,7 @@ interface FunctionFormContextPendingProps {
|
||||||
updateContext: (element?: CanvasElement) => void;
|
updateContext: (element?: CanvasElement) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FunctionFormContextPending: React.FunctionComponent<
|
export const FunctionFormContextPending: FC<FunctionFormContextPendingProps> = (props) => {
|
||||||
FunctionFormContextPendingProps
|
|
||||||
> = (props) => {
|
|
||||||
const { contextExpression, expressionType, context, updateContext } = props;
|
const { contextExpression, expressionType, context, updateContext } = props;
|
||||||
const prevContextExpression = usePrevious(contextExpression);
|
const prevContextExpression = usePrevious(contextExpression);
|
||||||
const fetchContext = useCallback(
|
const fetchContext = useCallback(
|
||||||
|
|
|
@ -19,8 +19,8 @@ import { getId } from '../../lib/get_id';
|
||||||
import { createAsset } from '../../state/actions/assets';
|
import { createAsset } from '../../state/actions/assets';
|
||||||
import {
|
import {
|
||||||
fetchContext,
|
fetchContext,
|
||||||
setArgumentAtIndex,
|
setArgument as setArgumentValue,
|
||||||
addArgumentValueAtIndex,
|
addArgumentValue,
|
||||||
deleteArgumentAtIndex,
|
deleteArgumentAtIndex,
|
||||||
// @ts-expect-error untyped local
|
// @ts-expect-error untyped local
|
||||||
} from '../../state/actions/elements';
|
} from '../../state/actions/elements';
|
||||||
|
@ -34,24 +34,29 @@ import { getAssets } from '../../state/selectors/assets';
|
||||||
// @ts-expect-error unconverted lib
|
// @ts-expect-error unconverted lib
|
||||||
import { findExistingAsset } from '../../lib/find_existing_asset';
|
import { findExistingAsset } from '../../lib/find_existing_asset';
|
||||||
import { FunctionForm as Component } from './function_form';
|
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';
|
import { State, ExpressionContext, CanvasElement, AssetType } from '../../../types';
|
||||||
|
|
||||||
interface FunctionFormProps {
|
interface FunctionFormProps {
|
||||||
name: string;
|
name: string;
|
||||||
argResolver: (ast: ExpressionAstExpression) => Promise<ExpressionValue>;
|
argResolver: (ast: ExpressionAstExpression) => Promise<ExpressionValue>;
|
||||||
args: Record<string, Array<string | Ast>> | null;
|
args: Args;
|
||||||
|
nestedFunctionsArgs: Args;
|
||||||
argType: ArgType;
|
argType: ArgType;
|
||||||
argTypeDef: ArgTypeDef;
|
argTypeDef: ArgTypeDef;
|
||||||
expressionIndex: number;
|
expressionIndex: number;
|
||||||
nextArgType?: ArgType;
|
nextArgType?: ArgType;
|
||||||
|
path: string;
|
||||||
|
parentPath: string;
|
||||||
|
removable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FunctionForm: React.FunctionComponent<FunctionFormProps> = (props) => {
|
export const FunctionForm: React.FunctionComponent<FunctionFormProps> = (props) => {
|
||||||
const { expressionIndex, argType, nextArgType } = props;
|
const { expressionIndex, ...restProps } = props;
|
||||||
|
const { nextArgType, path, parentPath, argType } = restProps;
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const context = useSelector<State, ExpressionContext>(
|
const context = useSelector<State, ExpressionContext>(
|
||||||
(state) => getContextForIndex(state, expressionIndex),
|
(state) => getContextForIndex(state, parentPath, expressionIndex),
|
||||||
deepEqual
|
deepEqual
|
||||||
);
|
);
|
||||||
const element = useSelector<State, CanvasElement | undefined>(
|
const element = useSelector<State, CanvasElement | undefined>(
|
||||||
|
@ -67,55 +72,33 @@ export const FunctionForm: React.FunctionComponent<FunctionFormProps> = (props)
|
||||||
|
|
||||||
const addArgument = useCallback(
|
const addArgument = useCallback(
|
||||||
(argName: string, argValue: string | Ast | null) => () => {
|
(argName: string, argValue: string | Ast | null) => () => {
|
||||||
dispatch(
|
dispatch(addArgumentValue({ element, pageId, argName, value: argValue, path }));
|
||||||
addArgumentValueAtIndex({
|
|
||||||
index: expressionIndex,
|
|
||||||
element,
|
|
||||||
pageId,
|
|
||||||
argName,
|
|
||||||
value: argValue,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[dispatch, element, expressionIndex, pageId]
|
[dispatch, element, pageId, path]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateContext = useCallback(
|
const updateContext = useCallback(() => {
|
||||||
() => dispatch(fetchContext(expressionIndex, element)),
|
return dispatch(fetchContext(expressionIndex, element, false, parentPath));
|
||||||
[dispatch, element, expressionIndex]
|
}, [dispatch, element, expressionIndex, parentPath]);
|
||||||
);
|
|
||||||
|
|
||||||
const setArgument = useCallback(
|
const setArgument = useCallback(
|
||||||
(argName: string, valueIndex: number) => (value: string | Ast | null) => {
|
(argName: string, valueIndex: number) => (value: string | Ast | null) => {
|
||||||
dispatch(
|
dispatch(setArgumentValue({ element, pageId, argName, value, valueIndex, path }));
|
||||||
setArgumentAtIndex({
|
|
||||||
index: expressionIndex,
|
|
||||||
element,
|
|
||||||
pageId,
|
|
||||||
argName,
|
|
||||||
value,
|
|
||||||
valueIndex,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[dispatch, element, expressionIndex, pageId]
|
[dispatch, element, pageId, path]
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteArgument = useCallback(
|
const deleteArgument = useCallback(
|
||||||
(argName: string, argIndex: number) => () => {
|
(argName: string, argIndex: number) => () => {
|
||||||
dispatch(
|
dispatch(deleteArgumentAtIndex({ element, pageId, argName, argIndex, path }));
|
||||||
deleteArgumentAtIndex({
|
|
||||||
index: expressionIndex,
|
|
||||||
element,
|
|
||||||
pageId,
|
|
||||||
argName,
|
|
||||||
argIndex,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[dispatch, element, expressionIndex, pageId]
|
[dispatch, element, pageId, path]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const deleteParentArgument = useCallback(() => {
|
||||||
|
dispatch(deleteArgumentAtIndex({ element, pageId, path: parentPath }));
|
||||||
|
}, [dispatch, element, pageId, parentPath]);
|
||||||
|
|
||||||
const onAssetAddDispatch = useCallback(
|
const onAssetAddDispatch = useCallback(
|
||||||
(type: AssetType['type'], content: AssetType['value']) => {
|
(type: AssetType['type'], content: AssetType['value']) => {
|
||||||
// make the ID here and pass it into the action
|
// make the ID here and pass it into the action
|
||||||
|
@ -138,9 +121,11 @@ export const FunctionForm: React.FunctionComponent<FunctionFormProps> = (props)
|
||||||
},
|
},
|
||||||
[assets, onAssetAddDispatch]
|
[assets, onAssetAddDispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
{...props}
|
{...restProps}
|
||||||
|
id={path}
|
||||||
context={context}
|
context={context}
|
||||||
filterGroups={filterGroups}
|
filterGroups={filterGroups}
|
||||||
expressionType={findExpressionType(argType)}
|
expressionType={findExpressionType(argType)}
|
||||||
|
@ -149,6 +134,7 @@ export const FunctionForm: React.FunctionComponent<FunctionFormProps> = (props)
|
||||||
updateContext={updateContext}
|
updateContext={updateContext}
|
||||||
onValueChange={setArgument}
|
onValueChange={setArgument}
|
||||||
onValueRemove={deleteArgument}
|
onValueRemove={deleteArgument}
|
||||||
|
onContainerRemove={deleteParentArgument}
|
||||||
onAssetAdd={onAssetAdd}
|
onAssetAdd={onAssetAdd}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,62 +9,185 @@ import { compose, withProps } from 'recompose';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { toExpression } from '@kbn/interpreter';
|
import { toExpression } from '@kbn/interpreter';
|
||||||
import { interpretAst } from '../../lib/run_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';
|
import { FunctionFormList as Component } from './function_form_list';
|
||||||
|
|
||||||
function normalizeContext(chain) {
|
function normalizeContext(chain) {
|
||||||
if (!Array.isArray(chain) || !chain.length) {
|
if (!Array.isArray(chain) || !chain.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return { type: 'expression', chain };
|
||||||
type: 'expression',
|
|
||||||
chain,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExpression(ast) {
|
function getExpression(ast) {
|
||||||
return ast != null && ast.type === 'expression' ? toExpression(ast) : ast;
|
return ast != null && ast.type === 'expression' ? toExpression(ast) : ast;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArgTypeDef(fn) {
|
const isPureArgumentType = (arg) => !arg.type || arg.type === 'argument';
|
||||||
return modelRegistry.get(fn) || viewRegistry.get(fn) || transformRegistry.get(fn);
|
|
||||||
|
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 functionFormItems = withProps((props) => {
|
||||||
const selectedElement = props.element;
|
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
|
// map argTypes from AST, attaching nextArgType if one exists
|
||||||
const FunctionFormListItems = FunctionFormChain.reduce(
|
const functionsListItems = transformFunctionsToUIConfig(functionsChain, buildPath('', 'ast'));
|
||||||
(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: [] }
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
functionFormItems: FunctionFormListItems.mapped,
|
functionFormItems: functionsListItems.mapped,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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';
|
|
@ -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<Props>, debouncePeriod: number = 150): FC<Props> =>
|
||||||
|
({ 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 <Arg {...{ ...restProps, argValue: localArgValue, onValueChange: setArgValue }} />;
|
||||||
|
};
|
|
@ -11,13 +11,14 @@ import { Ast } from '@kbn/interpreter';
|
||||||
// @ts-expect-error unconverted components
|
// @ts-expect-error unconverted components
|
||||||
import { ArgForm } from '../components/arg_form';
|
import { ArgForm } from '../components/arg_form';
|
||||||
import { argTypeRegistry } from './arg_type_registry';
|
import { argTypeRegistry } from './arg_type_registry';
|
||||||
import type { ArgType, ArgTypeDef, ExpressionType } from './types';
|
import type { Args, ArgType, ArgTypeDef, ArgValue, ExpressionType } from './types';
|
||||||
import {
|
import {
|
||||||
AssetType,
|
AssetType,
|
||||||
CanvasElement,
|
CanvasElement,
|
||||||
ExpressionAstExpression,
|
ExpressionAstExpression,
|
||||||
ExpressionValue,
|
ExpressionValue,
|
||||||
ExpressionContext,
|
ExpressionContext,
|
||||||
|
DatatableColumn,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { BaseFormProps } from './base_form';
|
import { BaseFormProps } from './base_form';
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ interface ArtOwnProps {
|
||||||
multi?: boolean;
|
multi?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
types?: string[];
|
types?: string[];
|
||||||
|
type?: 'model' | 'argument';
|
||||||
default?: string | null;
|
default?: string | null;
|
||||||
resolve?: (...args: any[]) => any;
|
resolve?: (...args: any[]) => any;
|
||||||
options?: {
|
options?: {
|
||||||
|
@ -38,10 +40,25 @@ interface ArtOwnProps {
|
||||||
shapes?: string[];
|
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<T = ResolvedColumns | ResolvedLabels | ResolvedDataurl | {}> {
|
||||||
|
resolved: T;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DataArg {
|
export interface DataArg {
|
||||||
argValue?: string | Ast | null;
|
argValue?: ArgValue | null;
|
||||||
skipRender?: boolean;
|
skipRender?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
valueIndex: number;
|
valueIndex: number;
|
||||||
|
@ -50,16 +67,15 @@ export interface DataArg {
|
||||||
contextExpression?: string;
|
contextExpression?: string;
|
||||||
name: string;
|
name: string;
|
||||||
argResolver: (ast: ExpressionAstExpression) => Promise<ExpressionValue>;
|
argResolver: (ast: ExpressionAstExpression) => Promise<ExpressionValue>;
|
||||||
args: Record<string, Array<string | Ast>> | null;
|
args: Args;
|
||||||
argType: ArgType;
|
argType: ArgType;
|
||||||
argTypeDef?: ArgTypeDef;
|
argTypeDef?: ArgTypeDef;
|
||||||
filterGroups: string[];
|
filterGroups: string[];
|
||||||
context?: ExpressionContext;
|
context?: ExpressionContext;
|
||||||
expressionIndex: number;
|
|
||||||
expressionType: ExpressionType;
|
expressionType: ExpressionType;
|
||||||
nextArgType?: ArgType;
|
nextArgType?: ArgType;
|
||||||
nextExpressionType?: ExpressionType;
|
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;
|
onAssetAdd: (type: AssetType['type'], content: AssetType['value']) => string;
|
||||||
onValueChange: (value: Ast | string) => void;
|
onValueChange: (value: Ast | string) => void;
|
||||||
onValueRemove: () => void;
|
onValueRemove: () => void;
|
||||||
|
@ -81,7 +97,7 @@ export class Arg {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
help?: string;
|
help?: string;
|
||||||
|
|
||||||
constructor(props: ArgProps) {
|
constructor(props: ArgUiConfig) {
|
||||||
const argType = argTypeRegistry.get(props.argType);
|
const argType = argTypeRegistry.get(props.argType);
|
||||||
if (!argType) {
|
if (!argType) {
|
||||||
throw new Error(`Invalid arg type: ${props.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?
|
// TODO: Document what these otherProps are. Maybe make them named arguments?
|
||||||
render(data: DataArg) {
|
render(data: DataArg & ResolvedArgProps) {
|
||||||
const { onValueChange, onValueRemove, argValue, key, label, ...otherProps } = data;
|
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
|
// This is everything the arg_type template needs to render
|
||||||
const templateProps = {
|
const templateProps = {
|
||||||
...otherProps,
|
|
||||||
...this.resolve?.(otherProps),
|
|
||||||
onValueChange,
|
|
||||||
argValue,
|
argValue,
|
||||||
|
argId,
|
||||||
|
onAssetAdd,
|
||||||
|
onValueChange,
|
||||||
typeInstance: this,
|
typeInstance: this,
|
||||||
|
resolved: { ...resolved, ...resolvedProps },
|
||||||
|
argResolver,
|
||||||
|
filterGroups,
|
||||||
};
|
};
|
||||||
|
|
||||||
const formProps = {
|
const formProps = {
|
||||||
key,
|
key,
|
||||||
argTypeInstance: this,
|
argTypeInstance: this,
|
||||||
valueMissing: this.required && argValue == null,
|
valueMissing: this.required && data.argValue == null,
|
||||||
label,
|
label,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
onValueRemove,
|
onValueRemove,
|
||||||
templateProps,
|
templateProps,
|
||||||
argId: key,
|
argId,
|
||||||
options: this.options,
|
options: this.options,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ class Interactive extends React.Component<{}, { argValue: ExpressionAstExpressio
|
||||||
action('onValueChange')(argValue);
|
action('onValueChange')(argValue);
|
||||||
this.setState({ argValue });
|
this.setState({ argValue });
|
||||||
}}
|
}}
|
||||||
labels={array('Series Labels', ['label1', 'label2'])}
|
resolved={{ labels: array('Series Labels', ['label1', 'label2']) }}
|
||||||
typeInstance={{
|
typeInstance={{
|
||||||
name: radios('Type Instance', { default: 'defaultStyle', custom: 'custom' }, 'custom'),
|
name: radios('Type Instance', { default: 'defaultStyle', custom: 'custom' }, 'custom'),
|
||||||
options: {
|
options: {
|
||||||
|
@ -74,7 +74,7 @@ storiesOf('arguments/SeriesStyle/components', module)
|
||||||
.add('extended: defaults', () => (
|
.add('extended: defaults', () => (
|
||||||
<ExtendedTemplate
|
<ExtendedTemplate
|
||||||
argValue={defaultExpression}
|
argValue={defaultExpression}
|
||||||
labels={[]}
|
resolved={{ labels: [] }}
|
||||||
onValueChange={action('onValueChange')}
|
onValueChange={action('onValueChange')}
|
||||||
typeInstance={{
|
typeInstance={{
|
||||||
name: 'defaultStyle',
|
name: 'defaultStyle',
|
||||||
|
|
|
@ -35,6 +35,7 @@ class Interactive extends React.Component<{}, { argValue: ExpressionAstExpressio
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<SimpleTemplate
|
<SimpleTemplate
|
||||||
|
resolved={{ labels: [] }}
|
||||||
argValue={this.state.argValue}
|
argValue={this.state.argValue}
|
||||||
onValueChange={(argValue) => {
|
onValueChange={(argValue) => {
|
||||||
action('onValueChange')(argValue);
|
action('onValueChange')(argValue);
|
||||||
|
@ -64,6 +65,7 @@ storiesOf('arguments/SeriesStyle/components', module)
|
||||||
argValue={defaultExpression}
|
argValue={defaultExpression}
|
||||||
onValueChange={action('onValueChange')}
|
onValueChange={action('onValueChange')}
|
||||||
workpad={getDefaultWorkpad()}
|
workpad={getDefaultWorkpad()}
|
||||||
|
resolved={{ labels: [] }}
|
||||||
typeInstance={{
|
typeInstance={{
|
||||||
name: 'defaultStyle',
|
name: 'defaultStyle',
|
||||||
}}
|
}}
|
||||||
|
@ -72,7 +74,7 @@ storiesOf('arguments/SeriesStyle/components', module)
|
||||||
.add('simple: defaults', () => (
|
.add('simple: defaults', () => (
|
||||||
<SimpleTemplate
|
<SimpleTemplate
|
||||||
argValue={defaultExpression}
|
argValue={defaultExpression}
|
||||||
labels={['label1', 'label2']}
|
resolved={{ labels: ['label1', 'label2'] }}
|
||||||
onValueChange={action('onValueChange')}
|
onValueChange={action('onValueChange')}
|
||||||
workpad={getDefaultWorkpad()}
|
workpad={getDefaultWorkpad()}
|
||||||
typeInstance={{
|
typeInstance={{
|
||||||
|
@ -83,6 +85,7 @@ storiesOf('arguments/SeriesStyle/components', module)
|
||||||
.add('simple: no series', () => (
|
.add('simple: no series', () => (
|
||||||
<SimpleTemplate
|
<SimpleTemplate
|
||||||
argValue={defaultExpression}
|
argValue={defaultExpression}
|
||||||
|
resolved={{ labels: [] }}
|
||||||
onValueChange={action('onValueChange')}
|
onValueChange={action('onValueChange')}
|
||||||
workpad={getDefaultWorkpad()}
|
workpad={getDefaultWorkpad()}
|
||||||
typeInstance={{
|
typeInstance={{
|
||||||
|
@ -94,7 +97,7 @@ storiesOf('arguments/SeriesStyle/components', module)
|
||||||
<SimpleTemplate
|
<SimpleTemplate
|
||||||
argValue={defaultExpression}
|
argValue={defaultExpression}
|
||||||
onValueChange={action('onValueChange')}
|
onValueChange={action('onValueChange')}
|
||||||
labels={['label1', 'label2']}
|
resolved={{ labels: ['label1', 'label2'] }}
|
||||||
workpad={getDefaultWorkpad()}
|
workpad={getDefaultWorkpad()}
|
||||||
typeInstance={{
|
typeInstance={{
|
||||||
name: 'unknown',
|
name: 'unknown',
|
||||||
|
|
|
@ -10,6 +10,7 @@ import PropTypes from 'prop-types';
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSpacer } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSpacer } from '@elastic/eui';
|
||||||
import immutable from 'object-path-immutable';
|
import immutable from 'object-path-immutable';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
|
import { ResolvedArgProps, ResolvedLabels } from '../../arg';
|
||||||
import { ExpressionAstExpression } from '../../../../types';
|
import { ExpressionAstExpression } from '../../../../types';
|
||||||
import { ArgTypesStrings } from '../../../../i18n';
|
import { ArgTypesStrings } from '../../../../i18n';
|
||||||
|
|
||||||
|
@ -24,9 +25,8 @@ export interface Arguments {
|
||||||
}
|
}
|
||||||
export type Argument = keyof Arguments;
|
export type Argument = keyof Arguments;
|
||||||
|
|
||||||
export interface Props {
|
export type Props = {
|
||||||
argValue: ExpressionAstExpression;
|
argValue: ExpressionAstExpression;
|
||||||
labels: string[];
|
|
||||||
onValueChange: (argValue: ExpressionAstExpression) => void;
|
onValueChange: (argValue: ExpressionAstExpression) => void;
|
||||||
typeInstance?: {
|
typeInstance?: {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -34,10 +34,15 @@ export interface Props {
|
||||||
include: string[];
|
include: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
} & ResolvedArgProps<ResolvedLabels>;
|
||||||
|
|
||||||
export const ExtendedTemplate: FunctionComponent<Props> = (props) => {
|
export const ExtendedTemplate: FunctionComponent<Props> = (props) => {
|
||||||
const { typeInstance, onValueChange, labels, argValue } = props;
|
const {
|
||||||
|
typeInstance,
|
||||||
|
onValueChange,
|
||||||
|
resolved: { labels },
|
||||||
|
argValue,
|
||||||
|
} = props;
|
||||||
const chain = get(argValue, 'chain.0', {});
|
const chain = get(argValue, 'chain.0', {});
|
||||||
const chainArgs = get(chain, 'arguments', {});
|
const chainArgs = get(chain, 'arguments', {});
|
||||||
const selectedSeries = get(chainArgs, 'label.0', '');
|
const selectedSeries = get(chainArgs, 'label.0', '');
|
||||||
|
@ -141,5 +146,7 @@ ExtendedTemplate.propTypes = {
|
||||||
onValueChange: PropTypes.func.isRequired,
|
onValueChange: PropTypes.func.isRequired,
|
||||||
argValue: PropTypes.any.isRequired,
|
argValue: PropTypes.any.isRequired,
|
||||||
typeInstance: PropTypes.object,
|
typeInstance: PropTypes.object,
|
||||||
labels: PropTypes.array.isRequired,
|
resolved: PropTypes.shape({
|
||||||
|
labels: PropTypes.array.isRequired,
|
||||||
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,7 @@ import PropTypes from 'prop-types';
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiButtonIcon, EuiText } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiButtonIcon, EuiText } from '@elastic/eui';
|
||||||
import immutable from 'object-path-immutable';
|
import immutable from 'object-path-immutable';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
|
import { ResolvedArgProps, ResolvedLabels } from '../../arg';
|
||||||
import { ColorPickerPopover } from '../../../components/color_picker_popover';
|
import { ColorPickerPopover } from '../../../components/color_picker_popover';
|
||||||
import { TooltipIcon, IconType } from '../../../components/tooltip_icon';
|
import { TooltipIcon, IconType } from '../../../components/tooltip_icon';
|
||||||
import { ExpressionAstExpression, CanvasWorkpad } from '../../../../types';
|
import { ExpressionAstExpression, CanvasWorkpad } from '../../../../types';
|
||||||
|
@ -23,18 +24,23 @@ interface Arguments {
|
||||||
}
|
}
|
||||||
type Argument = keyof Arguments;
|
type Argument = keyof Arguments;
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
argValue: ExpressionAstExpression;
|
argValue: ExpressionAstExpression;
|
||||||
labels?: string[];
|
|
||||||
onValueChange: (argValue: ExpressionAstExpression) => void;
|
onValueChange: (argValue: ExpressionAstExpression) => void;
|
||||||
typeInstance: {
|
typeInstance: {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
workpad: CanvasWorkpad;
|
workpad: CanvasWorkpad;
|
||||||
}
|
} & ResolvedArgProps<ResolvedLabels>;
|
||||||
|
|
||||||
export const SimpleTemplate: FunctionComponent<Props> = (props) => {
|
export const SimpleTemplate: FunctionComponent<Props> = (props) => {
|
||||||
const { typeInstance, argValue, onValueChange, labels, workpad } = props;
|
const {
|
||||||
|
typeInstance,
|
||||||
|
argValue,
|
||||||
|
onValueChange,
|
||||||
|
resolved: { labels },
|
||||||
|
workpad,
|
||||||
|
} = props;
|
||||||
const { name } = typeInstance;
|
const { name } = typeInstance;
|
||||||
const chain = get(argValue, 'chain.0', {});
|
const chain = get(argValue, 'chain.0', {});
|
||||||
const chainArgs = get(chain, 'arguments', {});
|
const chainArgs = get(chain, 'arguments', {});
|
||||||
|
@ -107,7 +113,9 @@ SimpleTemplate.displayName = 'SeriesStyleArgSimpleInput';
|
||||||
|
|
||||||
SimpleTemplate.propTypes = {
|
SimpleTemplate.propTypes = {
|
||||||
argValue: PropTypes.any.isRequired,
|
argValue: PropTypes.any.isRequired,
|
||||||
labels: PropTypes.array,
|
resolved: PropTypes.shape({
|
||||||
|
labels: PropTypes.array.isRequired,
|
||||||
|
}).isRequired,
|
||||||
onValueChange: PropTypes.func.isRequired,
|
onValueChange: PropTypes.func.isRequired,
|
||||||
workpad: PropTypes.shape({
|
workpad: PropTypes.shape({
|
||||||
colors: PropTypes.array.isRequired,
|
colors: PropTypes.array.isRequired,
|
||||||
|
|
|
@ -6,65 +6,71 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ReactElement } from 'react';
|
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 { isPlainObject, uniq, last, compact } from 'lodash';
|
||||||
import { Ast, fromExpression } from '@kbn/interpreter';
|
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
|
// @ts-expect-error unconverted components
|
||||||
import { SidebarSection } from '../components/sidebar/sidebar_section';
|
import { SidebarSection } from '../components/sidebar/sidebar_section';
|
||||||
// @ts-expect-error unconverted components
|
// @ts-expect-error unconverted components
|
||||||
import { SidebarSectionTitle } from '../components/sidebar/sidebar_section_title';
|
import { SidebarSectionTitle } from '../components/sidebar/sidebar_section_title';
|
||||||
import { BaseForm, BaseFormProps } from './base_form';
|
import { BaseForm, BaseFormProps } from './base_form';
|
||||||
import { Arg, ArgProps } from './arg';
|
import { Arg, ArgUiConfig, ResolvedArgProps } from './arg';
|
||||||
import { ArgType, ArgTypeDef, ExpressionType } from './types';
|
import { ArgDisplayType, Args, ArgType, ArgTypeDef, ArgValue, ExpressionType } from './types';
|
||||||
|
import { Model, Transform, View } from '../expression_types';
|
||||||
import {
|
import {
|
||||||
AssetType,
|
AssetType,
|
||||||
CanvasElement,
|
CanvasElement,
|
||||||
DatatableColumn,
|
|
||||||
ExpressionAstExpression,
|
ExpressionAstExpression,
|
||||||
ExpressionContext,
|
ExpressionContext,
|
||||||
ExpressionValue,
|
ExpressionValue,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
import { buildDefaultArgExpr, getArgTypeDef } from '../lib/args';
|
||||||
|
|
||||||
export interface DataArg {
|
export interface ArgWithValues {
|
||||||
arg: Arg | undefined;
|
arg: Arg | undefined;
|
||||||
argValues?: Array<string | Ast | null>;
|
argValues?: Array<ArgValue | null>;
|
||||||
skipRender?: boolean;
|
|
||||||
label?: 'string';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RenderArgData = BaseFormProps & {
|
export type RenderArgData = BaseFormProps & {
|
||||||
argType: ArgType;
|
argType: ArgType;
|
||||||
|
removable?: boolean;
|
||||||
|
type?: ArgDisplayType;
|
||||||
argTypeDef?: ArgTypeDef;
|
argTypeDef?: ArgTypeDef;
|
||||||
args: Record<string, Array<Ast | string>> | null;
|
args: Args;
|
||||||
|
id: string;
|
||||||
|
nestedFunctionsArgs: Args;
|
||||||
argResolver: (ast: ExpressionAstExpression) => Promise<ExpressionValue>;
|
argResolver: (ast: ExpressionAstExpression) => Promise<ExpressionValue>;
|
||||||
context?: ExpressionContext;
|
context?: ExpressionContext;
|
||||||
contextExpression?: string;
|
contextExpression?: string;
|
||||||
expressionIndex: number;
|
|
||||||
expressionType: ExpressionType;
|
expressionType: ExpressionType;
|
||||||
filterGroups: string[];
|
filterGroups: string[];
|
||||||
nextArgType?: ArgType;
|
nextArgType?: ArgType;
|
||||||
nextExpressionType?: ExpressionType;
|
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;
|
onValueChange: (argName: string, argIndex: number) => (value: string | Ast) => void;
|
||||||
onValueRemove: (argName: string, argIndex: number) => () => void;
|
onValueRemove: (argName: string, argIndex: number) => () => void;
|
||||||
|
onContainerRemove: () => void;
|
||||||
onAssetAdd: (type: AssetType['type'], content: AssetType['value']) => string;
|
onAssetAdd: (type: AssetType['type'], content: AssetType['value']) => string;
|
||||||
updateContext: (element?: CanvasElement) => void;
|
updateContext: (element?: CanvasElement) => void;
|
||||||
typeInstance?: ExpressionType;
|
typeInstance?: ExpressionType;
|
||||||
columns?: DatatableColumn[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RenderArgProps = {
|
export type RenderArgProps = {
|
||||||
typeInstance: FunctionForm;
|
typeInstance: FunctionForm;
|
||||||
} & RenderArgData;
|
} & RenderArgData &
|
||||||
|
ResolvedArgProps;
|
||||||
|
|
||||||
export type FunctionFormProps = {
|
export type FunctionFormProps = {
|
||||||
args?: ArgProps[];
|
args?: ArgUiConfig[];
|
||||||
resolve?: (...args: any[]) => any;
|
resolve?: (...args: any[]) => any;
|
||||||
} & BaseFormProps;
|
} & BaseFormProps;
|
||||||
|
|
||||||
export class FunctionForm extends BaseForm {
|
export class FunctionForm extends BaseForm {
|
||||||
args: ArgProps[];
|
/**
|
||||||
|
* UI arguments config
|
||||||
|
*/
|
||||||
|
args: ArgUiConfig[];
|
||||||
resolve: (...args: any[]) => any;
|
resolve: (...args: any[]) => any;
|
||||||
|
|
||||||
constructor(props: FunctionFormProps) {
|
constructor(props: FunctionFormProps) {
|
||||||
|
@ -74,23 +80,21 @@ export class FunctionForm extends BaseForm {
|
||||||
this.resolve = props.resolve || (() => ({}));
|
this.resolve = props.resolve || (() => ({}));
|
||||||
}
|
}
|
||||||
|
|
||||||
renderArg(props: RenderArgProps, dataArg: DataArg) {
|
renderArg(argWithValues: ArgWithValues, props: RenderArgProps) {
|
||||||
const { onValueRemove, onValueChange, ...passedProps } = props;
|
const { onValueRemove, onValueChange, onContainerRemove, id, ...passedProps } = props;
|
||||||
const { arg, argValues, skipRender, label } = dataArg;
|
const { arg, argValues } = argWithValues;
|
||||||
const { argType, expressionIndex } = passedProps;
|
|
||||||
|
|
||||||
// TODO: show some information to the user than an argument was skipped
|
// TODO: show some information to the user than an argument was skipped
|
||||||
if (!arg || skipRender) {
|
if (!arg) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderArgWithProps = (
|
const renderArgWithProps = (
|
||||||
argValue: string | Ast | null,
|
argValue: string | Ast | null,
|
||||||
valueIndex: number
|
valueIndex: number
|
||||||
): ReactElement<any, any> | null =>
|
): ReactElement<any, any> | null =>
|
||||||
arg.render({
|
arg.render({
|
||||||
key: `${argType}-${expressionIndex}-${arg.name}-${valueIndex}`,
|
key: `${id}.${arg.name}.${valueIndex}`,
|
||||||
...passedProps,
|
...passedProps,
|
||||||
label,
|
|
||||||
valueIndex,
|
valueIndex,
|
||||||
onValueChange: onValueChange(arg.name, valueIndex),
|
onValueChange: onValueChange(arg.name, valueIndex),
|
||||||
onValueRemove: onValueRemove(arg.name, valueIndex),
|
onValueRemove: onValueRemove(arg.name, valueIndex),
|
||||||
|
@ -107,21 +111,107 @@ export class FunctionForm extends BaseForm {
|
||||||
return argValues && argValues.map(renderArgWithProps);
|
return argValues && argValues.map(renderArgWithProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Argument adding isn't very good, we should improve this UI
|
getArgDescription({ name, displayName, help }: Arg | ArgTypeDef, argUiConfig: ArgUiConfig) {
|
||||||
getAddableArg(props: RenderArgProps, dataArg: DataArg) {
|
return {
|
||||||
const { onValueAdd } = props;
|
name: argUiConfig.name ?? name ?? '',
|
||||||
const { arg, argValues, skipRender } = dataArg;
|
displayName: argUiConfig.displayName ?? displayName,
|
||||||
|
help: argUiConfig.help ?? help,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// skip arguments that aren't defined in the expression type schema
|
getAddableArgComplex(
|
||||||
if (!arg || arg.required || skipRender) {
|
argUiConfig: ArgUiConfig,
|
||||||
|
argValues: Array<ArgValue | null>,
|
||||||
|
onValueAdd: RenderArgProps['onValueAdd']
|
||||||
|
) {
|
||||||
|
if (argValues && !argUiConfig.multi) {
|
||||||
return null;
|
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<ArgValue | null>,
|
||||||
|
onValueAdd: RenderArgProps['onValueAdd']
|
||||||
|
) {
|
||||||
|
const arg = new Arg(argUiConfig);
|
||||||
|
|
||||||
|
// skip arguments that aren't defined in the expression type schema
|
||||||
|
if (!arg || arg.required) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (argValues && !arg.multi) {
|
if (argValues && !arg.multi) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = arg.default == null ? null : fromExpression(arg.default, 'argument');
|
const value =
|
||||||
return { arg, onValueAdd: onValueAdd(arg.name, value) };
|
arg.default === null || arg.default === undefined
|
||||||
|
? null
|
||||||
|
: fromExpression(arg.default, 'argument');
|
||||||
|
return { ...this.getArgDescription(arg, argUiConfig), onValueAdd: onValueAdd(arg.name, value) };
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ArgOptions[]>((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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(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 };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveArg(...args: unknown[]) {
|
resolveArg(...args: unknown[]) {
|
||||||
|
@ -129,53 +219,58 @@ export class FunctionForm extends BaseForm {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
render(data: RenderArgData) {
|
private isExpressionFunctionForm(
|
||||||
if (!data) {
|
argTypeDef?: ArgTypeDef
|
||||||
data = {
|
): argTypeDef is View | Model | Transform {
|
||||||
args: null,
|
return (
|
||||||
argTypeDef: undefined,
|
!!argTypeDef &&
|
||||||
} as RenderArgData;
|
(argTypeDef instanceof View || argTypeDef instanceof Model || argTypeDef instanceof Transform)
|
||||||
}
|
);
|
||||||
const { args, argTypeDef } = data;
|
}
|
||||||
|
|
||||||
// Don't instaniate these until render time, to give the registries a chance to populate.
|
render(data: RenderArgData = { args: null, argTypeDef: undefined } as RenderArgData) {
|
||||||
const argInstances = this.args.map((argSpec) => new Arg(argSpec));
|
const { args, argTypeDef, nestedFunctionsArgs = {}, removable } = data;
|
||||||
if (args === null || !isPlainObject(args)) {
|
const argsWithValues = this.getArgsWithValues(args, argTypeDef);
|
||||||
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 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 };
|
|
||||||
try {
|
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
|
// 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(
|
const argumentForms = compact(
|
||||||
resolvedDataArgs.map((dataArg) => this.renderArg(props, dataArg))
|
resolvedArgsWithValues.map((argWithValues) => this.renderArg(argWithValues, props))
|
||||||
);
|
|
||||||
const addableArgs = compact(
|
|
||||||
resolvedDataArgs.map((dataArg) => this.getAddableArg(props, dataArg))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const addableArgs = this.getAddableArgs(args, nestedFunctionsArgs, props.onValueAdd);
|
||||||
if (!addableArgs.length && !argumentForms.length) {
|
if (!addableArgs.length && !argumentForms.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarSection>
|
<SidebarSection>
|
||||||
<SidebarSectionTitle title={argTypeDef?.displayName} tip={argTypeDef?.help}>
|
<SidebarSectionTitle title={argTypeDef?.displayName} tip={argTypeDef?.help}>
|
||||||
{addableArgs.length === 0 ? null : <ArgAddPopover options={addableArgs} />}
|
<EuiFormRow>
|
||||||
|
<EuiFlexGroup direction="row" gutterSize="s">
|
||||||
|
{removable && (
|
||||||
|
<EuiToolTip position="top" content={'Remove'}>
|
||||||
|
<EuiButtonIcon
|
||||||
|
color="text"
|
||||||
|
onClick={() => {
|
||||||
|
props.onContainerRemove();
|
||||||
|
}}
|
||||||
|
iconType="cross"
|
||||||
|
iconSize="s"
|
||||||
|
aria-label={'Remove'}
|
||||||
|
className="canvasArg__remove"
|
||||||
|
/>
|
||||||
|
</EuiToolTip>
|
||||||
|
)}
|
||||||
|
{addableArgs.length === 0 ? null : <ArgAddPopover options={addableArgs} />}
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFormRow>
|
||||||
</SidebarSectionTitle>
|
</SidebarSectionTitle>
|
||||||
{argumentForms}
|
{argumentForms}
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Ast } from '@kbn/interpreter';
|
||||||
import type { Transform } from './transform';
|
import type { Transform } from './transform';
|
||||||
import type { View } from './view';
|
import type { View } from './view';
|
||||||
import type { Datasource } from './datasource';
|
import type { Datasource } from './datasource';
|
||||||
import type { Model } from './model';
|
import type { Model } from './model';
|
||||||
|
|
||||||
export type ArgType = string;
|
export type ArgType = string;
|
||||||
|
export type ArgDisplayType = 'model' | 'argument';
|
||||||
|
|
||||||
export type ArgTypeDef = View | Model | Transform | Datasource;
|
export type ArgTypeDef = View | Model | Transform | Datasource;
|
||||||
|
|
||||||
|
@ -20,3 +22,6 @@ export type { Arg } from './arg';
|
||||||
export type ExpressionType = View | Model | Transform;
|
export type ExpressionType = View | Model | Transform;
|
||||||
|
|
||||||
export type { RenderArgData } from './function_form';
|
export type { RenderArgData } from './function_form';
|
||||||
|
|
||||||
|
export type ArgValue = string | Ast;
|
||||||
|
export type Args = Record<string, Array<ArgValue | null>> | null;
|
||||||
|
|
71
x-pack/plugins/canvas/public/lib/args.ts
Normal file
71
x-pack/plugins/canvas/public/lib/args.ts
Normal file
|
@ -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<string | undefined>) =>
|
||||||
|
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;
|
||||||
|
}
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import immutable from 'object-path-immutable';
|
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 { toExpression, safeElementFromExpression } from '@kbn/interpreter';
|
||||||
import { createThunk } from '../../lib/create_thunk';
|
import { createThunk } from '../../lib/create_thunk';
|
||||||
import {
|
import {
|
||||||
|
@ -30,8 +30,8 @@ const { actionsElements: strings } = ErrorStrings;
|
||||||
|
|
||||||
const { set, del } = immutable;
|
const { set, del } = immutable;
|
||||||
|
|
||||||
export function getSiblingContext(state, elementId, checkIndex) {
|
export function getSiblingContext(state, elementId, checkIndex, path = ['ast.chain']) {
|
||||||
const prevContextPath = [elementId, 'expressionContext', checkIndex];
|
const prevContextPath = [elementId, 'expressionContext', ...path, checkIndex];
|
||||||
const prevContextValue = getResolvedArgsValue(state, prevContextPath);
|
const prevContextValue = getResolvedArgsValue(state, prevContextPath);
|
||||||
|
|
||||||
// if a value is found, return it, along with the index it was found at
|
// 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
|
// 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) {
|
function getBareElement(el, includeId = false) {
|
||||||
|
@ -71,8 +71,9 @@ export const flushContextAfterIndex = createAction('flushContextAfterIndex');
|
||||||
|
|
||||||
export const fetchContext = createThunk(
|
export const fetchContext = createThunk(
|
||||||
'fetchContext',
|
'fetchContext',
|
||||||
({ dispatch, getState }, index, element, fullRefresh = false) => {
|
({ dispatch, getState }, index, element, fullRefresh = false, path) => {
|
||||||
const chain = get(element, 'ast.chain');
|
const pathToTarget = [...path.split('.'), 'chain'];
|
||||||
|
const chain = get(element, pathToTarget);
|
||||||
const invalidIndex = chain ? index >= chain.length : true;
|
const invalidIndex = chain ? index >= chain.length : true;
|
||||||
|
|
||||||
if (!element || !chain || invalidIndex) {
|
if (!element || !chain || invalidIndex) {
|
||||||
|
@ -81,22 +82,18 @@ export const fetchContext = createThunk(
|
||||||
|
|
||||||
// cache context as the previous index
|
// cache context as the previous index
|
||||||
const contextIndex = index - 1;
|
const contextIndex = index - 1;
|
||||||
const contextPath = [element.id, 'expressionContext', contextIndex];
|
const contextPath = [element.id, 'expressionContext', path, contextIndex];
|
||||||
|
|
||||||
// set context state to loading
|
// set context state to loading
|
||||||
dispatch(
|
dispatch(args.setLoading({ path: contextPath }));
|
||||||
args.setLoading({
|
|
||||||
path: contextPath,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// function to walk back up to find the closest context available
|
// 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 } =
|
const { index: prevContextIndex, context: prevContextValue } =
|
||||||
fullRefresh !== true ? getContext() : {};
|
fullRefresh !== true ? getContext() : {};
|
||||||
|
|
||||||
// modify the ast chain passed to the interpreter
|
// 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) {
|
if (prevContextValue != null) {
|
||||||
return i > prevContextIndex && i < index;
|
return i > prevContextIndex && i < index;
|
||||||
}
|
}
|
||||||
|
@ -104,22 +101,10 @@ export const fetchContext = createThunk(
|
||||||
});
|
});
|
||||||
|
|
||||||
const variables = getWorkpadVariablesAsObject(getState());
|
const variables = getWorkpadVariablesAsObject(getState());
|
||||||
|
const elementWithNewAst = set(element, pathToTarget, astChain);
|
||||||
// get context data from a partial AST
|
// get context data from a partial AST
|
||||||
return interpretAst(
|
return interpretAst(elementWithNewAst.ast, variables, prevContextValue).then((value) => {
|
||||||
{
|
dispatch(args.setValue({ path: contextPath, value }));
|
||||||
...element.ast,
|
|
||||||
chain: astChain,
|
|
||||||
},
|
|
||||||
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
|
* Updating the value of the given argument of the element's expression.
|
||||||
// argIndex is the index in multi-value arguments, and is optional. excluding it will cause
|
* @param {string} args.path - the path to the argument at the AST. Example: "ast.chain.0.arguments.some_arg.chain.1.arguments".
|
||||||
// the entire argument from be set to the passed value
|
* @param {string} args.argName - the argument name at the AST.
|
||||||
export const setArgumentAtIndex = createThunk('setArgumentAtIndex', ({ dispatch }, args) => {
|
* @param {number} args.valueIndex - the index of the value in the array of argument's values.
|
||||||
const { index, argName, value, valueIndex, element, pageId } = args;
|
* @param {any} args.value - the value to be set to the AST.
|
||||||
let selector = `ast.chain.${index}.arguments.${argName}`;
|
* @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) {
|
if (valueIndex != null) {
|
||||||
selector += '.' + valueIndex;
|
selector += '.' + valueIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newElement = set(element, selector, value);
|
const newElement = set(element, selector, value);
|
||||||
const newAst = get(newElement, ['ast', 'chain', index]);
|
const pathTerms = path.split('.');
|
||||||
dispatch(setAstAtIndex(index, newAst, element, pageId));
|
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
|
* Adding the value to the given argument of the element's expression.
|
||||||
export const addArgumentValueAtIndex = createThunk(
|
* @param {string} args.path - the path to the argument at the AST. Example: "ast.chain.0.arguments.some_arg.chain.1.arguments".
|
||||||
'addArgumentValueAtIndex',
|
* @param {string} args.argName - the argument name at the given path of the AST.
|
||||||
({ dispatch }, args) => {
|
* @param {any} args.value - the value to be added to the array of argument's values at the AST.
|
||||||
const { index, argName, value, element } = args;
|
* @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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const values = get(element, ['ast', 'chain', index, 'arguments', argName], []);
|
|
||||||
const newValue = values.concat(value);
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
setArgumentAtIndex({
|
|
||||||
...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) => {
|
export const deleteArgumentAtIndex = createThunk('deleteArgumentAtIndex', ({ dispatch }, args) => {
|
||||||
const { index, element, pageId, argName, argIndex } = args;
|
const { element, pageId, argName, argIndex, path } = args;
|
||||||
const curVal = get(element, ['ast', 'chain', index, 'arguments', argName]);
|
const pathTerms = path.split('.');
|
||||||
|
const argumentChainPath = pathTerms.slice(0, 3);
|
||||||
const newElement =
|
const argumnentChainIndex = last(argumentChainPath);
|
||||||
|
const curVal = get(element, [...pathTerms, argName]);
|
||||||
|
let newElement =
|
||||||
argIndex != null && curVal.length > 1
|
argIndex != null && curVal.length > 1
|
||||||
? // if more than one val, remove the specified val
|
? // 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
|
: // otherwise, remove the entire key
|
||||||
del(element, `ast.chain.${index}.arguments.${argName}`);
|
del(element, argName ? `${path}.${argName}` : path);
|
||||||
|
|
||||||
dispatch(setAstAtIndex(index, get(newElement, ['ast', 'chain', index]), element, pageId));
|
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(argumnentChainIndex, get(newElement, argumentChainPath), element, pageId));
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -67,41 +67,48 @@ describe('getSiblingContext', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stateWithDefaultPath = {
|
||||||
|
transient: {
|
||||||
|
resolvedArgs: {
|
||||||
|
'element-foo': {
|
||||||
|
expressionContext: {
|
||||||
|
'ast.chain': state.transient.resolvedArgs['element-foo'].expressionContext,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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', () => {
|
it('should find context when a previous context value is found', () => {
|
||||||
// pointseries map
|
// pointseries map
|
||||||
expect(getSiblingContext(state, 'element-foo', 2)).toEqual({
|
expect(getSiblingContext(state, 'element-foo', 2, [])).toEqual(expectedElement);
|
||||||
index: 2,
|
expect(getSiblingContext(stateWithDefaultPath, 'element-foo', 2)).toEqual(expectedElement);
|
||||||
context: {
|
expect(getSiblingContext(stateWithDefaultPath, 'element-foo', 2, ['ast.chain'])).toEqual(
|
||||||
type: 'pointseries',
|
expectedElement
|
||||||
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 not found', () => {
|
it('should find context when a previous context value is not found', () => {
|
||||||
// pointseries map
|
// pointseries map
|
||||||
expect(getSiblingContext(state, 'element-foo', 1000)).toEqual({
|
expect(getSiblingContext(state, 'element-foo', 1000, [])).toEqual(expectedElement);
|
||||||
index: 2,
|
expect(getSiblingContext(stateWithDefaultPath, 'element-foo', 1000)).toEqual(expectedElement);
|
||||||
context: {
|
expect(getSiblingContext(stateWithDefaultPath, 'element-foo', 1000, ['ast.chain'])).toEqual(
|
||||||
type: 'pointseries',
|
expectedElement
|
||||||
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' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -456,7 +456,7 @@ export function getResolvedArgs(state: State, elementId: string, path: any): any
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedResolvedArgs(state: State, path: any): any {
|
export function getSelectedResolvedArgs(state: State, path: Array<string | number>): any {
|
||||||
const elementId = getSelectedElementId(state);
|
const elementId = getSelectedElementId(state);
|
||||||
|
|
||||||
if (elementId) {
|
if (elementId) {
|
||||||
|
@ -464,8 +464,12 @@ export function getSelectedResolvedArgs(state: State, path: any): any {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContextForIndex(state: State, index: number): ExpressionContext {
|
export function getContextForIndex(
|
||||||
return getSelectedResolvedArgs(state, ['expressionContext', index - 1]);
|
state: State,
|
||||||
|
parentPath: string,
|
||||||
|
index: number
|
||||||
|
): ExpressionContext {
|
||||||
|
return getSelectedResolvedArgs(state, ['expressionContext', parentPath, index - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRefreshInterval(state: State): number {
|
export function getRefreshInterval(state: State): number {
|
||||||
|
|
|
@ -432,14 +432,10 @@ describe('heatmap', () => {
|
||||||
// grid
|
// grid
|
||||||
strokeWidth: [],
|
strokeWidth: [],
|
||||||
strokeColor: [],
|
strokeColor: [],
|
||||||
cellHeight: [],
|
|
||||||
cellWidth: [],
|
|
||||||
// cells
|
// cells
|
||||||
isCellLabelVisible: [false],
|
isCellLabelVisible: [false],
|
||||||
// Y-axis
|
// Y-axis
|
||||||
isYAxisLabelVisible: [true],
|
isYAxisLabelVisible: [true],
|
||||||
yAxisLabelWidth: [],
|
|
||||||
yAxisLabelColor: [],
|
|
||||||
// X-axis
|
// X-axis
|
||||||
isXAxisLabelVisible: [true],
|
isXAxisLabelVisible: [true],
|
||||||
},
|
},
|
||||||
|
|
|
@ -356,18 +356,10 @@ export const getHeatmapVisualization = ({
|
||||||
strokeColor: state.gridConfig.strokeColor
|
strokeColor: state.gridConfig.strokeColor
|
||||||
? [state.gridConfig.strokeColor]
|
? [state.gridConfig.strokeColor]
|
||||||
: [],
|
: [],
|
||||||
cellHeight: state.gridConfig.cellHeight ? [state.gridConfig.cellHeight] : [],
|
|
||||||
cellWidth: state.gridConfig.cellWidth ? [state.gridConfig.cellWidth] : [],
|
|
||||||
// cells
|
// cells
|
||||||
isCellLabelVisible: [state.gridConfig.isCellLabelVisible],
|
isCellLabelVisible: [state.gridConfig.isCellLabelVisible],
|
||||||
// Y-axis
|
// Y-axis
|
||||||
isYAxisLabelVisible: [state.gridConfig.isYAxisLabelVisible],
|
isYAxisLabelVisible: [state.gridConfig.isYAxisLabelVisible],
|
||||||
yAxisLabelWidth: state.gridConfig.yAxisLabelWidth
|
|
||||||
? [state.gridConfig.yAxisLabelWidth]
|
|
||||||
: [],
|
|
||||||
yAxisLabelColor: state.gridConfig.yAxisLabelColor
|
|
||||||
? [state.gridConfig.yAxisLabelColor]
|
|
||||||
: [],
|
|
||||||
// X-axis
|
// X-axis
|
||||||
isXAxisLabelVisible: state.gridConfig.isXAxisLabelVisible
|
isXAxisLabelVisible: state.gridConfig.isXAxisLabelVisible
|
||||||
? [state.gridConfig.isXAxisLabelVisible]
|
? [state.gridConfig.isXAxisLabelVisible]
|
||||||
|
|
|
@ -1956,15 +1956,11 @@
|
||||||
"expressionMetricVis.function.metric.help": "メトリックディメンションの構成です。",
|
"expressionMetricVis.function.metric.help": "メトリックディメンションの構成です。",
|
||||||
"expressionMetricVis.function.percentageMode.help": "百分率モードでメトリックを表示します。colorRange を設定する必要があります。",
|
"expressionMetricVis.function.percentageMode.help": "百分率モードでメトリックを表示します。colorRange を設定する必要があります。",
|
||||||
"expressionMetricVis.function.showLabels.help": "メトリック値の下にラベルを表示します。",
|
"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.isCellLabelVisible.help": "指定单元格标签是否可见。",
|
||||||
"expressionHeatmap.function.args.grid.isXAxisLabelVisible.help": "指定 X 轴标签是否可见。",
|
"expressionHeatmap.function.args.grid.isXAxisLabelVisible.help": "指定 X 轴标签是否可见。",
|
||||||
"expressionHeatmap.function.args.grid.isYAxisLabelVisible.help": "指定 Y 轴标签是否可见。",
|
"expressionHeatmap.function.args.grid.isYAxisLabelVisible.help": "指定 Y 轴标签是否可见。",
|
||||||
"expressionHeatmap.function.args.grid.strokeColor.help": "指定网格笔画颜色",
|
"expressionHeatmap.function.args.grid.strokeColor.help": "指定网格笔画颜色",
|
||||||
"expressionHeatmap.function.args.grid.strokeWidth.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.isVisible.help": "指定图例是否可见。",
|
||||||
"expressionHeatmap.function.args.legend.maxLines.help": "指定每个图例项的行数。",
|
"expressionHeatmap.function.args.legend.maxLines.help": "指定每个图例项的行数。",
|
||||||
"expressionHeatmap.function.args.legend.position.help": "指定图例位置。",
|
"expressionHeatmap.function.args.legend.position.help": "指定图例位置。",
|
||||||
|
|
|
@ -1964,15 +1964,11 @@
|
||||||
"expressionMetric.functions.metricHelpText": "在标签上显示数字。",
|
"expressionMetric.functions.metricHelpText": "在标签上显示数字。",
|
||||||
"expressionMetric.renderer.metric.displayName": "指标",
|
"expressionMetric.renderer.metric.displayName": "指标",
|
||||||
"expressionMetric.renderer.metric.helpDescription": "在标签上呈现数字",
|
"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.isCellLabelVisible.help": "指定单元格标签是否可见。",
|
||||||
"expressionHeatmap.function.args.grid.isXAxisLabelVisible.help": "指定 X 轴标签是否可见。",
|
"expressionHeatmap.function.args.grid.isXAxisLabelVisible.help": "指定 X 轴标签是否可见。",
|
||||||
"expressionHeatmap.function.args.grid.isYAxisLabelVisible.help": "指定 Y 轴标签是否可见。",
|
"expressionHeatmap.function.args.grid.isYAxisLabelVisible.help": "指定 Y 轴标签是否可见。",
|
||||||
"expressionHeatmap.function.args.grid.strokeColor.help": "指定网格笔画颜色",
|
"expressionHeatmap.function.args.grid.strokeColor.help": "指定网格笔画颜色",
|
||||||
"expressionHeatmap.function.args.grid.strokeWidth.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.isVisible.help": "指定图例是否可见。",
|
||||||
"expressionHeatmap.function.args.legend.maxLines.help": "指定每个图例项的行数。",
|
"expressionHeatmap.function.args.legend.maxLines.help": "指定每个图例项的行数。",
|
||||||
"expressionHeatmap.function.args.legend.position.help": "指定图例位置。",
|
"expressionHeatmap.function.args.legend.position.help": "指定图例位置。",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue