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