mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Canvas] Expression metrisVis
workpad arguments. (#114808)
* Added element. * Added metric_vis model. * Removed translations. * Added translations to the ui element. * added metricVis name. * Added font ui argument. * Added all arguments except palette. * Added palette. * Added stops_palette. * Fixed bug with label. * Removed unused labels. * Fixed mistake with table and input. * added first_datatable type. * Changed first_datatable to lens_multitable. * Fixed the mistake in the name. * One more fix. * Adde the ability to extend the default palette list to add custom after change the color. * Added small refactor of the palette to intergrate stops_palette with the ability to scale the solution. * Code reorganization at palette. * Added export. * Added color stops with functionality of update. * Added optimizations for rerendering of sidebar at canvas. * Added workpad. * Fixed bug. * Added direct onChange listener. * Experiment on updating props. * Updated the behaviour of color stop rerendering flow. * Fixed some bugs. * Added fixes for unexpected behavior. * Telemetry tests refactored. * Regrouped. * Added removable option and fixed bug with colors on palette change. * Added validation for input. * Fixed behaviour with rangeMin and rangeMax. * Added fix and comment, * Fixed a bug with percentage mode. * Changed from 60 pt to 80 px. * Fixed bug with continuity. * Added one more fix. * Added metricVis element translations. * Fixed types * Reverted fontSize to 60. * Added fontUnit to the font expression and added pt option to the metricVis expression. * Added comment to the metricVis expression at to_ast of vis_types/metric. * Fixed tests. * Fixed i18n mistake. * Added translations to labels. * Small refactor of palette picker. * updated snapshot. * Added support of changing continuity and range. * Added fix for the datasource form. * UpdatePropsRef generic type added. * Simplified the code. * Added padding between color stops. * Updated behaviour of the metricVis when adding bucket. * Remove ref on unmount to avoid leaks. * Added `Labels` as default. * Commented out metricVis translation for now. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
22492bea71
commit
3f73eb5ec3
48 changed files with 1354 additions and 502 deletions
|
@ -32,6 +32,7 @@ import { verticalBarChart } from './vert_bar_chart';
|
|||
import { verticalProgressBar } from './vertical_progress_bar';
|
||||
import { verticalProgressPill } from './vertical_progress_pill';
|
||||
import { tagCloud } from './tag_cloud';
|
||||
import { metricVis } from './metric_vis';
|
||||
|
||||
import { SetupInitializer } from '../plugin';
|
||||
import { ElementFactory } from '../../types';
|
||||
|
@ -73,3 +74,9 @@ export const initializeElements: SetupInitializer<ElementFactory[]> = (core, plu
|
|||
];
|
||||
return applyElementStrings(specs);
|
||||
};
|
||||
|
||||
// For testing purpose. Will be removed after exposing `metricVis` element.
|
||||
export const initializeElementsSpec: SetupInitializer<ElementFactory[]> = (core, plugins) => {
|
||||
const specs = initializeElements(core, plugins);
|
||||
return [...applyElementStrings([metricVis]), ...specs];
|
||||
};
|
||||
|
|
|
@ -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 metricVis: ElementFactory = () => ({
|
||||
name: 'metricVis',
|
||||
displayName: '(New) Metric Vis',
|
||||
type: 'chart',
|
||||
help: 'Metric visualization',
|
||||
icon: 'visMetric',
|
||||
expression: `filters
|
||||
| demodata
|
||||
| head 1
|
||||
| metricVis metric={visdimension "percent_uptime"} colorMode="Labels"
|
||||
| render`,
|
||||
});
|
|
@ -29,6 +29,7 @@ storiesOf('arguments/Palette', module).add('default', () => (
|
|||
}}
|
||||
onValueChange={action('onValueChange')}
|
||||
renderError={action('renderError')}
|
||||
typeInstance={{}}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
|
|
@ -16,7 +16,7 @@ import { imageUpload } from './image_upload';
|
|||
// @ts-expect-error untyped local
|
||||
import { number } from './number';
|
||||
import { numberFormatInitializer } from './number_format';
|
||||
import { palette } from './palette';
|
||||
import { palette, stopsPalette } from './palette';
|
||||
// @ts-expect-error untyped local
|
||||
import { percentage } from './percentage';
|
||||
// @ts-expect-error untyped local
|
||||
|
@ -42,6 +42,7 @@ export const args = [
|
|||
imageUpload,
|
||||
number,
|
||||
palette,
|
||||
stopsPalette,
|
||||
percentage,
|
||||
range,
|
||||
select,
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; 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 { get } from 'lodash';
|
||||
import { getType } from '@kbn/interpreter/common';
|
||||
import { ExpressionAstFunction, ExpressionAstExpression } from 'src/plugins/expressions';
|
||||
import { PalettePicker } from '../../../public/components/palette_picker';
|
||||
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
|
||||
import { ArgumentStrings } from '../../../i18n';
|
||||
import { identifyPalette, ColorPalette } from '../../../common/lib';
|
||||
|
||||
const { Palette: strings } = ArgumentStrings;
|
||||
|
||||
interface Props {
|
||||
onValueChange: (value: ExpressionAstExpression) => void;
|
||||
argValue: ExpressionAstExpression;
|
||||
renderError: () => void;
|
||||
argId?: string;
|
||||
}
|
||||
|
||||
export const PaletteArgInput: FC<Props> = ({ onValueChange, argId, argValue, renderError }) => {
|
||||
// TODO: This is weird, its basically a reimplementation of what the interpretter would return.
|
||||
// Probably a better way todo this, and maybe a better way to handle template type objects in general?
|
||||
const astToPalette = ({ chain }: { chain: ExpressionAstFunction[] }): ColorPalette | null => {
|
||||
if (chain.length !== 1 || chain[0].function !== 'palette') {
|
||||
renderError();
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const colors = chain[0].arguments._.map((astObj) => {
|
||||
if (getType(astObj) !== 'string') {
|
||||
renderError();
|
||||
}
|
||||
return astObj;
|
||||
}) as string[];
|
||||
|
||||
const gradient = get(chain[0].arguments.gradient, '[0]') as boolean;
|
||||
const palette = identifyPalette({ colors, gradient });
|
||||
|
||||
if (palette) {
|
||||
return palette;
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'custom',
|
||||
label: strings.getCustomPaletteLabel(),
|
||||
colors,
|
||||
gradient,
|
||||
} as any as ColorPalette;
|
||||
} catch (e) {
|
||||
renderError();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleChange = (palette: ColorPalette): void => {
|
||||
const astObj: ExpressionAstExpression = {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'palette',
|
||||
arguments: {
|
||||
_: palette.colors,
|
||||
gradient: [palette.gradient],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
onValueChange(astObj);
|
||||
};
|
||||
|
||||
const palette = astToPalette(argValue);
|
||||
|
||||
if (!palette) {
|
||||
renderError();
|
||||
return null;
|
||||
}
|
||||
|
||||
return <PalettePicker id={argId} palette={palette} onChange={handleChange} />;
|
||||
};
|
||||
|
||||
PaletteArgInput.propTypes = {
|
||||
argId: PropTypes.string,
|
||||
onValueChange: PropTypes.func.isRequired,
|
||||
argValue: PropTypes.any.isRequired,
|
||||
renderError: PropTypes.func,
|
||||
};
|
||||
|
||||
export const palette = () => ({
|
||||
name: 'palette',
|
||||
displayName: strings.getDisplayName(),
|
||||
help: strings.getHelp(),
|
||||
default:
|
||||
'{palette #882E72 #B178A6 #D6C1DE #1965B0 #5289C7 #7BAFDE #4EB265 #90C987 #CAE0AB #F7EE55 #F6C141 #F1932D #E8601C #DC050C}',
|
||||
simpleTemplate: templateFromReactComponent(PaletteArgInput),
|
||||
});
|
|
@ -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 { PaletteArgInput, SimplePaletteArgInput, palette, stopsPalette } from './palette';
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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 { ExpressionAstExpression } from 'src/plugins/expressions';
|
||||
import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component';
|
||||
import { ArgumentStrings } from '../../../../i18n';
|
||||
import { ColorPalette } from '../../../../common/lib';
|
||||
import { astToPalette } from './utils';
|
||||
import { ColorPaletteName, getPaletteType } from './palette_types';
|
||||
import { CustomColorPalette } from '../../../../public/components/palette_picker';
|
||||
|
||||
const { Palette: strings, StopsPalette: stopsPaletteStrings } = ArgumentStrings;
|
||||
|
||||
interface Props {
|
||||
onValueChange: (value: ExpressionAstExpression) => void;
|
||||
argValue: ExpressionAstExpression;
|
||||
renderError: () => void;
|
||||
argId?: string;
|
||||
typeInstance: {
|
||||
options?: {
|
||||
type?: ColorPaletteName;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const PaletteArgInput: FC<Props> = ({
|
||||
onValueChange,
|
||||
argId,
|
||||
argValue,
|
||||
renderError,
|
||||
typeInstance,
|
||||
}) => {
|
||||
const handleChange = (palette: ColorPalette | CustomColorPalette): void => {
|
||||
let colorStopsPaletteConfig = {};
|
||||
if (palette.stops?.length) {
|
||||
colorStopsPaletteConfig = {
|
||||
stop: palette.stops,
|
||||
...(palette.range ? { range: [palette.range] } : {}),
|
||||
...(palette.continuity ? { continuity: [palette.continuity] } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const astObj: ExpressionAstExpression = {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'palette',
|
||||
arguments: {
|
||||
_: palette.colors,
|
||||
gradient: [palette.gradient],
|
||||
...colorStopsPaletteConfig,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
onValueChange(astObj);
|
||||
};
|
||||
|
||||
const palette = astToPalette(argValue, renderError);
|
||||
if (!palette) {
|
||||
renderError();
|
||||
return null;
|
||||
}
|
||||
|
||||
const PalettePicker = getPaletteType(typeInstance.options?.type);
|
||||
return <PalettePicker id={argId} palette={palette} onChange={handleChange} />;
|
||||
};
|
||||
|
||||
export const SimplePaletteArgInput: FC<Props> = (props) => {
|
||||
const { typeInstance } = props;
|
||||
const { type, ...restOptions } = typeInstance.options ?? {};
|
||||
return (
|
||||
<PaletteArgInput {...props} typeInstance={{ ...props.typeInstance, options: restOptions }} />
|
||||
);
|
||||
};
|
||||
|
||||
export const StopsPaletteArgInput: FC<Props> = (props) => (
|
||||
<PaletteArgInput
|
||||
{...props}
|
||||
typeInstance={{
|
||||
...props.typeInstance,
|
||||
options: { ...(props.typeInstance.options ?? {}), type: 'stops' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
PaletteArgInput.propTypes = {
|
||||
argId: PropTypes.string,
|
||||
onValueChange: PropTypes.func.isRequired,
|
||||
argValue: PropTypes.any.isRequired,
|
||||
renderError: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultPaletteOptions = {
|
||||
default:
|
||||
'{palette #882E72 #B178A6 #D6C1DE #1965B0 #5289C7 #7BAFDE #4EB265 #90C987 #CAE0AB #F7EE55 #F6C141 #F1932D #E8601C #DC050C}',
|
||||
};
|
||||
|
||||
export const palette = () => ({
|
||||
name: 'palette',
|
||||
displayName: strings.getDisplayName(),
|
||||
help: strings.getHelp(),
|
||||
simpleTemplate: templateFromReactComponent(SimplePaletteArgInput),
|
||||
...defaultPaletteOptions,
|
||||
});
|
||||
|
||||
export const stopsPalette = () => ({
|
||||
name: 'stops_palette',
|
||||
help: stopsPaletteStrings.getHelp(),
|
||||
displayName: stopsPaletteStrings.getDisplayName(),
|
||||
template: templateFromReactComponent(StopsPaletteArgInput),
|
||||
...defaultPaletteOptions,
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { PalettePicker, StopsPalettePicker } from '../../../../public/components/palette_picker';
|
||||
|
||||
const DEFAULT_PALETTE = 'default';
|
||||
const STOPS_PALETTE = 'stops';
|
||||
|
||||
export type ColorPaletteName = typeof DEFAULT_PALETTE | typeof STOPS_PALETTE;
|
||||
|
||||
const paletteTypes = {
|
||||
[DEFAULT_PALETTE]: PalettePicker,
|
||||
[STOPS_PALETTE]: StopsPalettePicker,
|
||||
};
|
||||
|
||||
export const getPaletteType = (type: ColorPaletteName = DEFAULT_PALETTE) =>
|
||||
paletteTypes[type] ?? paletteTypes[DEFAULT_PALETTE];
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { getType } from '@kbn/interpreter/common';
|
||||
import { ExpressionAstArgument, ExpressionAstFunction } from 'src/plugins/expressions';
|
||||
import { identifyPalette, ColorPalette, identifyPartialPalette } from '../../../../common/lib';
|
||||
import { ArgumentStrings } from '../../../../i18n';
|
||||
|
||||
const { Palette: strings } = ArgumentStrings;
|
||||
|
||||
export const CUSTOM_PALETTE = 'custom';
|
||||
|
||||
interface PaletteParams {
|
||||
colors: string[];
|
||||
gradient: boolean;
|
||||
stops?: number[];
|
||||
}
|
||||
|
||||
export const createCustomPalette = (
|
||||
paletteParams: PaletteParams
|
||||
): ColorPalette<typeof CUSTOM_PALETTE> => ({
|
||||
id: CUSTOM_PALETTE,
|
||||
label: strings.getCustomPaletteLabel(),
|
||||
...paletteParams,
|
||||
});
|
||||
|
||||
type UnboxArray<T> = T extends Array<infer U> ? U : T;
|
||||
|
||||
function reduceElementsWithType<T extends unknown[]>(
|
||||
arr: any[],
|
||||
arg: ExpressionAstArgument,
|
||||
type: string,
|
||||
onError: () => void
|
||||
) {
|
||||
if (getType(arg) !== type) {
|
||||
onError();
|
||||
}
|
||||
return [...arr, arg as UnboxArray<T>];
|
||||
}
|
||||
|
||||
// TODO: This is weird, its basically a reimplementation of what the interpretter would return.
|
||||
// Probably a better way todo this, and maybe a better way to handle template type objects in general?
|
||||
export const astToPalette = (
|
||||
{ chain }: { chain: ExpressionAstFunction[] },
|
||||
onError: () => void
|
||||
): ColorPalette | ColorPalette<typeof CUSTOM_PALETTE> | null => {
|
||||
if (chain.length !== 1 || chain[0].function !== 'palette') {
|
||||
onError();
|
||||
return null;
|
||||
}
|
||||
|
||||
const { _, stop: argStops, gradient: argGradient, ...restArgs } = chain[0].arguments ?? {};
|
||||
|
||||
try {
|
||||
const colors =
|
||||
_?.reduce<string[]>((args, arg) => {
|
||||
return reduceElementsWithType(args, arg, 'string', onError);
|
||||
}, []) ?? [];
|
||||
|
||||
const stops =
|
||||
argStops?.reduce<number[]>((args, arg) => {
|
||||
return reduceElementsWithType(args, arg, 'number', onError);
|
||||
}, []) ?? [];
|
||||
|
||||
const gradient = !!argGradient?.[0];
|
||||
const palette = (stops.length ? identifyPartialPalette : identifyPalette)({ colors, gradient });
|
||||
const restPreparedArgs = Object.keys(restArgs).reduce<
|
||||
Record<string, ExpressionAstArgument | ExpressionAstArgument[]>
|
||||
>((acc, argName) => {
|
||||
acc[argName] = restArgs[argName]?.length > 1 ? restArgs[argName] : restArgs[argName]?.[0];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (palette) {
|
||||
return {
|
||||
...palette,
|
||||
...restPreparedArgs,
|
||||
stops,
|
||||
};
|
||||
}
|
||||
|
||||
return createCustomPalette({ colors, gradient, stops, ...restPreparedArgs });
|
||||
} catch (e) {
|
||||
onError();
|
||||
}
|
||||
return null;
|
||||
};
|
|
@ -31,9 +31,6 @@ const VisDimensionArgInput: React.FC<VisDimensionArgInputProps> = ({
|
|||
onValueChange,
|
||||
argId,
|
||||
columns,
|
||||
}: {
|
||||
// @todo define types
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
const [value, setValue] = useState(argValue);
|
||||
const confirm = typeInstance?.options?.confirm;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
EuiFormRow,
|
||||
|
@ -27,13 +27,16 @@ import { DataSourceStrings, LUCENE_QUERY_URL } from '../../../i18n';
|
|||
const { ESDocs: strings } = DataSourceStrings;
|
||||
|
||||
const EsdocsDatasource = ({ args, updateArgs, defaultIndex }) => {
|
||||
const setArg = (name, value) => {
|
||||
updateArgs &&
|
||||
updateArgs({
|
||||
...args,
|
||||
...setSimpleArg(name, value),
|
||||
});
|
||||
};
|
||||
const setArg = useCallback(
|
||||
(name, value) => {
|
||||
updateArgs &&
|
||||
updateArgs({
|
||||
...args,
|
||||
...setSimpleArg(name, value),
|
||||
});
|
||||
},
|
||||
[args, updateArgs]
|
||||
);
|
||||
|
||||
// TODO: This is a terrible way of doing defaults. We need to find a way to read the defaults for the function
|
||||
// and set them for the data source UI.
|
||||
|
@ -73,6 +76,12 @@ const EsdocsDatasource = ({ args, updateArgs, defaultIndex }) => {
|
|||
|
||||
const index = getIndex();
|
||||
|
||||
useEffect(() => {
|
||||
if (getSimpleArg('index', args)[0] !== index) {
|
||||
setArg('index', index);
|
||||
}
|
||||
}, [args, index, setArg]);
|
||||
|
||||
const sortOptions = [
|
||||
{ value: 'asc', text: strings.getAscendingOption() },
|
||||
{ value: 'desc', text: strings.getDescendingOption() },
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
import { pointseries } from './point_series';
|
||||
import { math } from './math';
|
||||
import { tagcloud } from './tagcloud';
|
||||
import { metricVis } from './metric_vis';
|
||||
|
||||
export const modelSpecs = [pointseries, math, tagcloud];
|
||||
export const modelSpecs = [pointseries, math, tagcloud, metricVis];
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { ViewStrings } from '../../../i18n';
|
||||
import { getState, getValue } from '../../../public/lib/resolved_arg';
|
||||
|
||||
const { MetricVis: strings } = ViewStrings;
|
||||
|
||||
export const metricVis = () => ({
|
||||
name: 'metricVis',
|
||||
displayName: strings.getDisplayName(),
|
||||
args: [
|
||||
{
|
||||
name: 'metric',
|
||||
displayName: strings.getMetricColumnDisplayName(),
|
||||
help: strings.getMetricColumnHelp(),
|
||||
argType: 'vis_dimension',
|
||||
multi: true,
|
||||
default: `{visdimension}`,
|
||||
},
|
||||
{
|
||||
name: 'bucket',
|
||||
displayName: strings.getBucketColumnDisplayName(),
|
||||
help: strings.getBucketColumnHelp(),
|
||||
argType: 'vis_dimension',
|
||||
default: `{visdimension}`,
|
||||
},
|
||||
{
|
||||
name: 'palette',
|
||||
argType: 'stops_palette',
|
||||
},
|
||||
{
|
||||
name: 'font',
|
||||
displayName: strings.getFontColumnDisplayName(),
|
||||
help: strings.getFontColumnHelp(),
|
||||
argType: 'font',
|
||||
default: `{font size=60 align="center"}`,
|
||||
},
|
||||
{
|
||||
name: 'colorMode',
|
||||
displayName: strings.getColorModeColumnDisplayName(),
|
||||
help: strings.getColorModeColumnHelp(),
|
||||
argType: 'select',
|
||||
default: 'Labels',
|
||||
options: {
|
||||
choices: [
|
||||
{ value: 'None', name: strings.getColorModeNoneOption() },
|
||||
{ value: 'Labels', name: strings.getColorModeLabelOption() },
|
||||
{ value: 'Background', name: strings.getColorModeBackgroundOption() },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'showLabels',
|
||||
displayName: strings.getShowLabelsColumnDisplayName(),
|
||||
help: strings.getShowLabelsColumnHelp(),
|
||||
argType: 'toggle',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
name: 'percentageMode',
|
||||
displayName: strings.getPercentageModeColumnDisplayName(),
|
||||
help: strings.getPercentageModeColumnHelp(),
|
||||
argType: 'toggle',
|
||||
},
|
||||
],
|
||||
resolve({ context }: any) {
|
||||
if (getState(context) !== 'ready') {
|
||||
return { columns: [] };
|
||||
}
|
||||
return { columns: get(getValue(context), 'columns', []) };
|
||||
},
|
||||
});
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { difference, isEqual } from 'lodash';
|
||||
import { LibStrings } from '../../i18n';
|
||||
|
||||
const { Palettes: strings } = LibStrings;
|
||||
|
@ -19,11 +19,14 @@ export type PaletteID = typeof palettes[number]['id'];
|
|||
* An interface representing a color palette in Canvas, with a textual label and a set of
|
||||
* hex values.
|
||||
*/
|
||||
export interface ColorPalette {
|
||||
id: PaletteID;
|
||||
export interface ColorPalette<AdditionalPaletteID extends string = PaletteID> {
|
||||
id: PaletteID | AdditionalPaletteID;
|
||||
label: string;
|
||||
colors: string[];
|
||||
gradient: boolean;
|
||||
stops?: number[];
|
||||
range?: 'number' | 'percent';
|
||||
continuity?: 'above' | 'below' | 'all' | 'none';
|
||||
}
|
||||
|
||||
// This function allows one to create a strongly-typed palette for inclusion in
|
||||
|
@ -52,6 +55,15 @@ export const identifyPalette = (
|
|||
});
|
||||
};
|
||||
|
||||
export const identifyPartialPalette = (
|
||||
input: Pick<ColorPalette, 'colors' | 'gradient'>
|
||||
): ColorPalette | undefined => {
|
||||
return palettes.find((palette) => {
|
||||
const { colors, gradient } = palette;
|
||||
return gradient === input.gradient && difference(input.colors, colors).length === 0;
|
||||
});
|
||||
};
|
||||
|
||||
export const paulTor14 = createPalette({
|
||||
id: 'paul_tor_14',
|
||||
label: 'Paul Tor 14',
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
*/
|
||||
|
||||
import { getElementStrings } from './element_strings';
|
||||
import { initializeElements } from '../../canvas_plugin_src/elements';
|
||||
import { initializeElementsSpec } from '../../canvas_plugin_src/elements';
|
||||
import { coreMock } from '../../../../../src/core/public/mocks';
|
||||
|
||||
const elementSpecs = initializeElements(coreMock.createSetup() as any, {} as any);
|
||||
const elementSpecs = initializeElementsSpec(coreMock.createSetup() as any, {} as any);
|
||||
|
||||
describe('ElementStrings', () => {
|
||||
const elementStrings = getElementStrings();
|
||||
|
|
|
@ -230,4 +230,12 @@ export const getElementStrings = (): ElementStringDict => ({
|
|||
defaultMessage: 'Tagcloud visualization',
|
||||
}),
|
||||
},
|
||||
metricVis: {
|
||||
displayName: i18n.translate('xpack.canvas.elements.metricVisDisplayName', {
|
||||
defaultMessage: '(New) Metric Vis',
|
||||
}),
|
||||
help: i18n.translate('xpack.canvas.elements.metricVisHelpText', {
|
||||
defaultMessage: 'Metric visualization',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -328,6 +328,16 @@ export const ArgumentStrings = {
|
|||
defaultMessage: 'Select column',
|
||||
}),
|
||||
},
|
||||
StopsPalette: {
|
||||
getDisplayName: () =>
|
||||
i18n.translate('xpack.canvas.uis.arguments.stopsPaletteTitle', {
|
||||
defaultMessage: 'Palette picker with bounds',
|
||||
}),
|
||||
getHelp: () =>
|
||||
i18n.translate('xpack.canvas.uis.arguments.stopsPaletteLabel', {
|
||||
defaultMessage: 'Provides colors for the values, based on the bounds',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const DataSourceStrings = {
|
||||
|
@ -1273,4 +1283,70 @@ export const ViewStrings = {
|
|||
defaultMessage: 'Bucket dimension configuration',
|
||||
}),
|
||||
},
|
||||
MetricVis: {
|
||||
getDisplayName: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVisTitle', {
|
||||
defaultMessage: 'Metric Vis',
|
||||
}),
|
||||
getMetricColumnDisplayName: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.metricDisplayName', {
|
||||
defaultMessage: 'Metric',
|
||||
}),
|
||||
getMetricColumnHelp: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.metricHelp', {
|
||||
defaultMessage: 'Metric dimension configuration',
|
||||
}),
|
||||
getBucketColumnDisplayName: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.bucketDisplayName', {
|
||||
defaultMessage: 'Bucket',
|
||||
}),
|
||||
getBucketColumnHelp: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.bucketHelp', {
|
||||
defaultMessage: 'Bucket dimension configuration',
|
||||
}),
|
||||
getFontColumnDisplayName: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.fontDisplayName', {
|
||||
defaultMessage: 'Font',
|
||||
}),
|
||||
getFontColumnHelp: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.fontHelp', {
|
||||
defaultMessage: 'Metric font configuration',
|
||||
}),
|
||||
getPercentageModeColumnDisplayName: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.percentageModeDisplayName', {
|
||||
defaultMessage: 'Enable percentage mode',
|
||||
}),
|
||||
getPercentageModeColumnHelp: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.percentageModeHelp', {
|
||||
defaultMessage: 'Shows metric in percentage mode.',
|
||||
}),
|
||||
getShowLabelsColumnDisplayName: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.showLabelsDisplayName', {
|
||||
defaultMessage: 'Show metric labels',
|
||||
}),
|
||||
getShowLabelsColumnHelp: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.showLabelsHelp', {
|
||||
defaultMessage: 'Shows labels under the metric values.',
|
||||
}),
|
||||
getColorModeColumnDisplayName: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.colorModeDisplayName', {
|
||||
defaultMessage: 'Metric color mode',
|
||||
}),
|
||||
getColorModeColumnHelp: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.colorModeHelp', {
|
||||
defaultMessage: 'Which part of metric to fill with color.',
|
||||
}),
|
||||
getColorModeNoneOption: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.colorMode.noneOption', {
|
||||
defaultMessage: 'None',
|
||||
}),
|
||||
getColorModeLabelOption: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.colorMode.labelsOption', {
|
||||
defaultMessage: 'Labels',
|
||||
}),
|
||||
getColorModeBackgroundOption: () =>
|
||||
i18n.translate('xpack.canvas.uis.views.metricVis.args.colorMode.backgroundOption', {
|
||||
defaultMessage: 'Background',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ErrorBoundary } from '../enhance/error_boundary';
|
||||
import { ArgSimpleForm } from './arg_simple_form';
|
||||
|
@ -39,11 +39,9 @@ export const ArgForm = (props) => {
|
|||
onValueRemove,
|
||||
workpad,
|
||||
assets,
|
||||
renderError,
|
||||
setRenderError,
|
||||
resolvedArgValue,
|
||||
} = props;
|
||||
|
||||
const [renderError, setRenderError] = useState(false);
|
||||
const isMounted = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -62,21 +60,15 @@ export const ArgForm = (props) => {
|
|||
{({ error, resetErrorState }) => {
|
||||
const { template, simpleTemplate } = argTypeInstance.argType;
|
||||
const hasError = Boolean(error) || renderError;
|
||||
|
||||
const argumentProps = {
|
||||
...templateProps,
|
||||
resolvedArgValue,
|
||||
defaultValue: argTypeInstance.default,
|
||||
|
||||
renderError: () => {
|
||||
// TODO: don't do this
|
||||
// It's an ugly hack to avoid React's render cycle and ensure the error happens on the next tick
|
||||
// This is important; Otherwise we end up updating state in the middle of a render cycle
|
||||
Promise.resolve().then(() => {
|
||||
// Provide templates with a renderError method, and wrap the error in a known error type
|
||||
// to stop Kibana's window.error from being called
|
||||
isMounted.current && setRenderError(true);
|
||||
});
|
||||
// Provide templates with a renderError method, and wrap the error in a known error type
|
||||
// to stop Kibana's window.error from being called
|
||||
isMounted.current && setRenderError(true);
|
||||
},
|
||||
error: hasError,
|
||||
setLabel: (label) => isMounted.current && setLabel(label),
|
||||
|
@ -154,7 +146,5 @@ ArgForm.propTypes = {
|
|||
expand: PropTypes.bool,
|
||||
setExpand: PropTypes.func,
|
||||
onValueRemove: PropTypes.func,
|
||||
renderError: PropTypes.bool.isRequired,
|
||||
setRenderError: PropTypes.func.isRequired,
|
||||
resolvedArgValue: PropTypes.any,
|
||||
};
|
||||
|
|
|
@ -7,14 +7,17 @@
|
|||
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import useEffectOnce from 'react-use/lib/useEffectOnce';
|
||||
import { RenderToDom } from '../render_to_dom';
|
||||
import { ExpressionFormHandlers } from '../../../common/lib/expression_form_handlers';
|
||||
import { UpdatePropsRef } from '../../../types/arguments';
|
||||
|
||||
interface ArgTemplateFormProps {
|
||||
template?: (
|
||||
domNode: HTMLElement,
|
||||
config: ArgTemplateFormProps['argumentProps'],
|
||||
handlers: ArgTemplateFormProps['handlers']
|
||||
handlers: ArgTemplateFormProps['handlers'],
|
||||
onMount?: (ref: UpdatePropsRef<ArgTemplateFormProps['argumentProps']> | null) => void
|
||||
) => void;
|
||||
argumentProps: {
|
||||
valueMissing?: boolean;
|
||||
|
@ -42,11 +45,27 @@ export const ArgTemplateForm: React.FunctionComponent<ArgTemplateFormProps> = ({
|
|||
errorTemplate,
|
||||
}) => {
|
||||
const [updatedHandlers, setHandlers] = useState(mergeWithFormHandlers(handlers));
|
||||
const previousError = usePrevious(error);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const prevError = usePrevious(error);
|
||||
const prevMounted = usePrevious(mounted);
|
||||
const mountedArgumentRef = useRef<UpdatePropsRef<ArgTemplateFormProps['argumentProps']>>();
|
||||
|
||||
const domNodeRef = useRef<HTMLElement>();
|
||||
|
||||
useEffectOnce(() => () => {
|
||||
mountedArgumentRef.current = undefined;
|
||||
});
|
||||
|
||||
const renderTemplate = useCallback(
|
||||
(domNode) => template && template(domNode, argumentProps, updatedHandlers),
|
||||
[template, argumentProps, updatedHandlers]
|
||||
(domNode) =>
|
||||
template &&
|
||||
template(domNode, argumentProps, updatedHandlers, (ref) => {
|
||||
if (!mountedArgumentRef.current && ref) {
|
||||
mountedArgumentRef.current = ref;
|
||||
setMounted(true);
|
||||
}
|
||||
}),
|
||||
[argumentProps, template, updatedHandlers]
|
||||
);
|
||||
|
||||
const renderErrorTemplate = useCallback(
|
||||
|
@ -59,22 +78,30 @@ export const ArgTemplateForm: React.FunctionComponent<ArgTemplateFormProps> = ({
|
|||
}, [handlers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousError !== error) {
|
||||
if (!prevError && error) {
|
||||
updatedHandlers.destroy();
|
||||
}
|
||||
}, [previousError, error, updatedHandlers]);
|
||||
}, [prevError, error, updatedHandlers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
if ((!error && prevError && mounted) || (mounted && !prevMounted && !error)) {
|
||||
renderTemplate(domNodeRef.current);
|
||||
}
|
||||
}, [error, renderTemplate, domNodeRef]);
|
||||
}, [error, mounted, prevError, prevMounted, renderTemplate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mountedArgumentRef.current) {
|
||||
mountedArgumentRef.current?.updateProps(argumentProps);
|
||||
}
|
||||
}, [argumentProps]);
|
||||
|
||||
if (error) {
|
||||
mountedArgumentRef.current = undefined;
|
||||
return renderErrorTemplate();
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
mountedArgumentRef.current = undefined;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -82,7 +109,7 @@ export const ArgTemplateForm: React.FunctionComponent<ArgTemplateFormProps> = ({
|
|||
<RenderToDom
|
||||
render={(domNode) => {
|
||||
domNodeRef.current = domNode;
|
||||
renderTemplate(domNode);
|
||||
setMounted(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -19,12 +19,10 @@ export const ArgForm = (props) => {
|
|||
const { argTypeInstance, label: labelFromProps, templateProps } = props;
|
||||
const [label, setLabel] = useState(getLabel(labelFromProps, argTypeInstance));
|
||||
const [resolvedArgValue, setResolvedArgValue] = useState(null);
|
||||
const [renderError, setRenderError] = useState(false);
|
||||
const workpad = useSelector(getWorkpadInfo);
|
||||
const assets = useSelector(getAssets);
|
||||
|
||||
useEffect(() => {
|
||||
setRenderError(false);
|
||||
setResolvedArgValue();
|
||||
}, [templateProps?.argValue]);
|
||||
|
||||
|
@ -37,8 +35,6 @@ export const ArgForm = (props) => {
|
|||
setLabel={setLabel}
|
||||
resolvedArgValue={resolvedArgValue}
|
||||
setResolvedArgValue={setResolvedArgValue}
|
||||
renderError={renderError}
|
||||
setRenderError={setRenderError}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FunctionFormComponent } from './function_form_component';
|
||||
import { FunctionFormComponent as Component } from './function_form_component';
|
||||
import { FunctionUnknown } from './function_unknown';
|
||||
import { FunctionFormContextPending } from './function_form_context_pending';
|
||||
import { FunctionFormContextError } from './function_form_context_error';
|
||||
|
@ -56,5 +56,5 @@ export const FunctionForm: React.FunctionComponent<FunctionFormProps> = (props)
|
|||
);
|
||||
}
|
||||
|
||||
return <FunctionFormComponent {...props} />;
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { Ast } from '@kbn/interpreter/common';
|
||||
import deepEqual from 'react-fast-compare';
|
||||
import {
|
||||
ExpressionAstExpression,
|
||||
ExpressionValue,
|
||||
|
@ -49,15 +50,21 @@ interface FunctionFormProps {
|
|||
export const FunctionForm: React.FunctionComponent<FunctionFormProps> = (props) => {
|
||||
const { expressionIndex, argType, nextArgType } = props;
|
||||
const dispatch = useDispatch();
|
||||
const context = useSelector<State, ExpressionContext>((state) =>
|
||||
getContextForIndex(state, expressionIndex)
|
||||
const context = useSelector<State, ExpressionContext>(
|
||||
(state) => getContextForIndex(state, expressionIndex),
|
||||
deepEqual
|
||||
);
|
||||
const element = useSelector<State, CanvasElement | undefined>((state) =>
|
||||
getSelectedElement(state)
|
||||
const element = useSelector<State, CanvasElement | undefined>(
|
||||
(state) => getSelectedElement(state),
|
||||
deepEqual
|
||||
);
|
||||
const pageId = useSelector<State, string>((state) => getSelectedPage(state));
|
||||
const assets = useSelector<State, State['assets']>((state) => getAssets(state));
|
||||
const filterGroups = useSelector<State, string[]>((state) => getGlobalFilterGroups(state));
|
||||
const pageId = useSelector<State, string>((state) => getSelectedPage(state), shallowEqual);
|
||||
const assets = useSelector<State, State['assets']>((state) => getAssets(state), shallowEqual);
|
||||
const filterGroups = useSelector<State, string[]>(
|
||||
(state) => getGlobalFilterGroups(state),
|
||||
shallowEqual
|
||||
);
|
||||
|
||||
const addArgument = useCallback(
|
||||
(argName: string, argValue: string | Ast | null) => () => {
|
||||
dispatch(
|
||||
|
@ -131,7 +138,6 @@ export const FunctionForm: React.FunctionComponent<FunctionFormProps> = (props)
|
|||
},
|
||||
[assets, onAssetAddDispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
|
|
|
@ -6,3 +6,5 @@
|
|||
*/
|
||||
|
||||
export { PalettePicker } from './palette_picker';
|
||||
export { StopsPalettePicker } from './stops_palette_picker';
|
||||
export type { PalettePickerProps, CustomColorPalette, StopsPalettePickerProps } from './types';
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; 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 { isEqual } from 'lodash';
|
||||
import { EuiColorPalettePicker, EuiColorPalettePickerPaletteProps } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { palettes, ColorPalette } from '../../../common/lib/palettes';
|
||||
|
||||
const strings = {
|
||||
getEmptyPaletteLabel: () =>
|
||||
i18n.translate('xpack.canvas.palettePicker.emptyPaletteLabel', {
|
||||
defaultMessage: 'None',
|
||||
}),
|
||||
getNoPaletteFoundErrorTitle: () =>
|
||||
i18n.translate('xpack.canvas.palettePicker.noPaletteFoundErrorTitle', {
|
||||
defaultMessage: 'Color palette not found',
|
||||
}),
|
||||
};
|
||||
|
||||
interface RequiredProps {
|
||||
id?: string;
|
||||
onChange?: (palette: ColorPalette) => void;
|
||||
palette: ColorPalette;
|
||||
clearable?: false;
|
||||
}
|
||||
|
||||
interface ClearableProps {
|
||||
id?: string;
|
||||
onChange?: (palette: ColorPalette | null) => void;
|
||||
palette: ColorPalette | null;
|
||||
clearable: true;
|
||||
}
|
||||
|
||||
type Props = RequiredProps | ClearableProps;
|
||||
|
||||
const findPalette = (colorPalette: ColorPalette | null, colorPalettes: ColorPalette[] = []) => {
|
||||
const palette = colorPalettes.filter((cp) => cp.id === colorPalette?.id)[0] ?? null;
|
||||
if (palette === null) {
|
||||
return colorPalettes.filter((cp) => isEqual(cp.colors, colorPalette?.colors))[0] ?? null;
|
||||
}
|
||||
|
||||
return palette;
|
||||
};
|
||||
|
||||
export const PalettePicker: FC<Props> = (props) => {
|
||||
const colorPalettes: EuiColorPalettePickerPaletteProps[] = palettes.map((item) => ({
|
||||
value: item.id,
|
||||
title: item.label,
|
||||
type: item.gradient ? 'gradient' : 'fixed',
|
||||
palette: item.colors,
|
||||
}));
|
||||
|
||||
if (props.clearable) {
|
||||
const { palette, onChange = () => {} } = props;
|
||||
|
||||
colorPalettes.unshift({
|
||||
value: 'clear',
|
||||
title: strings.getEmptyPaletteLabel(),
|
||||
type: 'text',
|
||||
});
|
||||
|
||||
const onPickerChange = (value: string) => {
|
||||
const canvasPalette = palettes.find((item) => item.id === value);
|
||||
onChange(canvasPalette || null);
|
||||
};
|
||||
|
||||
const foundPalette = findPalette(palette, palettes);
|
||||
|
||||
return (
|
||||
<EuiColorPalettePicker
|
||||
id={props.id}
|
||||
compressed={true}
|
||||
palettes={colorPalettes}
|
||||
onChange={onPickerChange}
|
||||
valueOfSelected={foundPalette?.id ?? 'clear'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { palette, onChange = () => {} } = props;
|
||||
|
||||
const onPickerChange = (value: string) => {
|
||||
const canvasPalette = palettes.find((item) => item.id === value);
|
||||
|
||||
if (!canvasPalette) {
|
||||
throw new Error(strings.getNoPaletteFoundErrorTitle());
|
||||
}
|
||||
|
||||
onChange(canvasPalette);
|
||||
};
|
||||
|
||||
const foundPalette = findPalette(palette, palettes);
|
||||
|
||||
return (
|
||||
<EuiColorPalettePicker
|
||||
id={props.id}
|
||||
compressed={true}
|
||||
palettes={colorPalettes}
|
||||
onChange={onPickerChange}
|
||||
valueOfSelected={foundPalette?.id}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
PalettePicker.propTypes = {
|
||||
id: PropTypes.string,
|
||||
palette: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
clearable: PropTypes.bool,
|
||||
};
|
|
@ -8,12 +8,13 @@
|
|||
import React, { FC, useState } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { PalettePicker } from '../palette_picker';
|
||||
import { PalettePicker } from '../../palette_picker';
|
||||
|
||||
import { paulTor14, ColorPalette } from '../../../../common/lib/palettes';
|
||||
import { paulTor14, ColorPalette } from '../../../../../common/lib/palettes';
|
||||
import { CustomColorPalette } from '../../types';
|
||||
|
||||
const Interactive: FC = () => {
|
||||
const [palette, setPalette] = useState<ColorPalette | null>(paulTor14);
|
||||
const [palette, setPalette] = useState<ColorPalette | CustomColorPalette | null>(paulTor14);
|
||||
return <PalettePicker palette={palette} onChange={setPalette} clearable={true} />;
|
||||
};
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { EuiColorPalettePicker } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { FC } from 'react';
|
||||
import { ClearableComponentProps } from '../types';
|
||||
import { findPalette, prepareColorPalette } from '../utils';
|
||||
|
||||
const strings = {
|
||||
getEmptyPaletteLabel: () =>
|
||||
i18n.translate('xpack.canvas.palettePicker.emptyPaletteLabel', {
|
||||
defaultMessage: 'None',
|
||||
}),
|
||||
};
|
||||
|
||||
export const ClearablePalettePicker: FC<ClearableComponentProps> = (props) => {
|
||||
const { palette, palettes, onChange = () => {} } = props;
|
||||
const colorPalettes = palettes.map(prepareColorPalette);
|
||||
|
||||
const onPickerChange = (value: string) => {
|
||||
const canvasPalette = palettes.find((item) => item.id === value);
|
||||
onChange(canvasPalette || null);
|
||||
};
|
||||
|
||||
const foundPalette = findPalette(palette ?? null, palettes);
|
||||
|
||||
return (
|
||||
<EuiColorPalettePicker
|
||||
id={props.id}
|
||||
compressed={true}
|
||||
palettes={[
|
||||
{
|
||||
value: 'clear',
|
||||
title: strings.getEmptyPaletteLabel(),
|
||||
type: 'text',
|
||||
},
|
||||
...colorPalettes,
|
||||
]}
|
||||
onChange={onPickerChange}
|
||||
valueOfSelected={foundPalette?.id ?? 'clear'}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { EuiColorPalettePicker } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { FC } from 'react';
|
||||
import { RequiredComponentProps } from '../types';
|
||||
import { findPalette, prepareColorPalette } from '../utils';
|
||||
|
||||
const strings = {
|
||||
getNoPaletteFoundErrorTitle: () =>
|
||||
i18n.translate('xpack.canvas.palettePicker.noPaletteFoundErrorTitle', {
|
||||
defaultMessage: 'Color palette not found',
|
||||
}),
|
||||
};
|
||||
|
||||
export const DefaultPalettePicker: FC<RequiredComponentProps> = (props) => {
|
||||
const { palette, palettes, onChange = () => {} } = props;
|
||||
const colorPalettes = palettes.map(prepareColorPalette);
|
||||
|
||||
const onPickerChange = (value: string) => {
|
||||
const canvasPalette = palettes.find((item) => item.id === value);
|
||||
if (!canvasPalette) {
|
||||
throw new Error(strings.getNoPaletteFoundErrorTitle());
|
||||
}
|
||||
|
||||
onChange(canvasPalette);
|
||||
};
|
||||
|
||||
const foundPalette = findPalette(palette ?? null, palettes);
|
||||
|
||||
return (
|
||||
<EuiColorPalettePicker
|
||||
id={props.id}
|
||||
compressed={true}
|
||||
palettes={colorPalettes}
|
||||
onChange={onPickerChange}
|
||||
valueOfSelected={foundPalette?.id}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 { PalettePicker } from './palette_picker';
|
|
@ -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 } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ClearablePalettePicker } from './clearable_palette_picker';
|
||||
import { palettes as defaultPalettes } from '../../../../common/lib/palettes';
|
||||
import { PalettePickerProps } from '../types';
|
||||
import { DefaultPalettePicker } from './default_palette_picker';
|
||||
|
||||
export const PalettePicker: FC<PalettePickerProps> = (props) => {
|
||||
const { additionalPalettes = [] } = props;
|
||||
const palettes = [...defaultPalettes, ...additionalPalettes];
|
||||
|
||||
if (props.clearable) {
|
||||
return (
|
||||
<ClearablePalettePicker
|
||||
palettes={palettes}
|
||||
palette={props.palette}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DefaultPalettePicker palettes={palettes} palette={props.palette} onChange={props.onChange} />
|
||||
);
|
||||
};
|
||||
|
||||
PalettePicker.propTypes = {
|
||||
id: PropTypes.string,
|
||||
palette: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
clearable: PropTypes.bool,
|
||||
};
|
|
@ -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 { StopsPalettePicker } from './stops_palette_picker';
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonIcon,
|
||||
EuiColorPicker,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { ColorStop } from '../types';
|
||||
|
||||
interface Props {
|
||||
removable?: boolean;
|
||||
stop?: number;
|
||||
color?: string;
|
||||
onDelete: () => void;
|
||||
onChange: (colorStop: ColorStop) => void;
|
||||
}
|
||||
|
||||
interface ValidationResult {
|
||||
color: boolean;
|
||||
stop: boolean;
|
||||
}
|
||||
|
||||
const strings = {
|
||||
getDeleteStopColorLabel: () =>
|
||||
i18n.translate('xpack.canvas.stopsColorPicker.deleteColorStopLabel', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
};
|
||||
|
||||
const isValidColorStop = (colorStop: ColorStop): ValidationResult & { valid: boolean } => {
|
||||
const valid = !isNaN(colorStop.stop);
|
||||
return {
|
||||
valid,
|
||||
stop: valid,
|
||||
color: true,
|
||||
};
|
||||
};
|
||||
|
||||
export const StopColorPicker: FC<Props> = (props) => {
|
||||
const { stop, color, onDelete, onChange, removable = true } = props;
|
||||
|
||||
const [colorStop, setColorStop] = useState<ColorStop>({ stop: stop ?? 0, color: color ?? '' });
|
||||
const [areValidFields, setAreValidFields] = useState<ValidationResult>({
|
||||
stop: true,
|
||||
color: true,
|
||||
});
|
||||
|
||||
const onChangeInput = (updatedColorStop: ColorStop) => {
|
||||
setColorStop(updatedColorStop);
|
||||
};
|
||||
|
||||
const [, cancel] = useDebounce(
|
||||
() => {
|
||||
if (color === colorStop.color && stop === colorStop.stop) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { valid, ...validationResult } = isValidColorStop(colorStop);
|
||||
if (!valid) {
|
||||
setAreValidFields(validationResult);
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(colorStop);
|
||||
},
|
||||
150,
|
||||
[colorStop]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newColorStop = { stop: stop ?? 0, color: color ?? '' };
|
||||
setColorStop(newColorStop);
|
||||
|
||||
const { valid, ...validationResult } = isValidColorStop(newColorStop);
|
||||
setAreValidFields(validationResult);
|
||||
}, [color, stop]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancel();
|
||||
};
|
||||
}, [cancel]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
value={colorStop.stop}
|
||||
min={-Infinity}
|
||||
onChange={({ target: { valueAsNumber } }) =>
|
||||
onChangeInput({ ...colorStop, stop: valueAsNumber })
|
||||
}
|
||||
isInvalid={!areValidFields.stop}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiColorPicker
|
||||
secondaryInputDisplay="top"
|
||||
color={colorStop.color}
|
||||
showAlpha
|
||||
compressed
|
||||
onChange={(newColor) => {
|
||||
onChangeInput({ ...colorStop, color: newColor });
|
||||
}}
|
||||
isInvalid={!areValidFields.color}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
title={strings.getDeleteStopColorLabel()}
|
||||
onClick={onDelete}
|
||||
isDisabled={!removable}
|
||||
aria-label={strings.getDeleteStopColorLabel()}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* 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, useCallback, useMemo } from 'react';
|
||||
import { flowRight, identity } from 'lodash';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui';
|
||||
import useEffectOnce from 'react-use/lib/useEffectOnce';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ColorStop, CustomColorPalette, StopsPalettePickerProps } from '../types';
|
||||
import { PalettePicker } from '../palette_picker';
|
||||
import { StopColorPicker } from './stop_color_picker';
|
||||
import { Palette } from './types';
|
||||
import {
|
||||
reduceColorsByStopsSize,
|
||||
transformPaletteToColorStops,
|
||||
mergeColorStopsWithPalette,
|
||||
deleteColorStop,
|
||||
updateColorStop,
|
||||
addNewColorStop,
|
||||
getOverridenPaletteOptions,
|
||||
} from './utils';
|
||||
import { ColorPalette } from '../../../../common/lib/palettes';
|
||||
|
||||
const strings = {
|
||||
getAddColorStopLabel: () =>
|
||||
i18n.translate('xpack.canvas.stopsPalettePicker.addColorStopLabel', {
|
||||
defaultMessage: 'Add color stop',
|
||||
}),
|
||||
getColorStopsLabel: () =>
|
||||
i18n.translate('xpack.canvas.stopsPalettePicker.colorStopsLabel', {
|
||||
defaultMessage: 'Color stops',
|
||||
}),
|
||||
};
|
||||
|
||||
const defaultStops = [0, 1];
|
||||
const MIN_STOPS = 2;
|
||||
|
||||
export const StopsPalettePicker: FC<StopsPalettePickerProps> = (props) => {
|
||||
const { palette, onChange } = props;
|
||||
const stops = useMemo(
|
||||
() => (!palette?.stops || !palette.stops.length ? defaultStops : palette.stops),
|
||||
[palette?.stops]
|
||||
);
|
||||
|
||||
const colors = useMemo(
|
||||
() => reduceColorsByStopsSize(palette?.colors, stops.length),
|
||||
[palette?.colors, stops.length]
|
||||
);
|
||||
|
||||
const onChangePalette = useCallback(
|
||||
(newPalette: ColorPalette | CustomColorPalette | null) => {
|
||||
if (newPalette) {
|
||||
const newColors = reduceColorsByStopsSize(newPalette?.colors, stops.length);
|
||||
props.onChange?.({
|
||||
...palette,
|
||||
...newPalette,
|
||||
colors: newColors,
|
||||
stops,
|
||||
});
|
||||
}
|
||||
},
|
||||
[palette, props, stops]
|
||||
);
|
||||
|
||||
useEffectOnce(() => {
|
||||
onChangePalette({ ...getOverridenPaletteOptions(), ...palette });
|
||||
});
|
||||
|
||||
const paletteColorStops = useMemo(
|
||||
() => transformPaletteToColorStops({ stops, colors }),
|
||||
[colors, stops]
|
||||
);
|
||||
|
||||
const updatePalette = useCallback(
|
||||
(fn: (colorStops: ColorStop[]) => ColorStop[]) =>
|
||||
flowRight<ColorStop[][], ColorStop[], Palette, void>(
|
||||
onChange ?? identity,
|
||||
mergeColorStopsWithPalette(palette),
|
||||
fn
|
||||
),
|
||||
[onChange, palette]
|
||||
);
|
||||
|
||||
const deleteColorStopAndApply = useCallback(
|
||||
(index: number) => updatePalette(deleteColorStop(index))(paletteColorStops),
|
||||
[paletteColorStops, updatePalette]
|
||||
);
|
||||
|
||||
const updateColorStopAndApply = useCallback(
|
||||
(index: number, colorStop: ColorStop) =>
|
||||
updatePalette(updateColorStop(index, colorStop))(paletteColorStops),
|
||||
[paletteColorStops, updatePalette]
|
||||
);
|
||||
|
||||
const addColorStopAndApply = useCallback(
|
||||
() => updatePalette(addNewColorStop(palette))(paletteColorStops),
|
||||
[palette, paletteColorStops, updatePalette]
|
||||
);
|
||||
|
||||
const stopColorPickers = paletteColorStops.map(({ id, ...rest }, index) => (
|
||||
<EuiFlexItem>
|
||||
<StopColorPicker
|
||||
{...rest}
|
||||
key={index}
|
||||
removable={index >= MIN_STOPS}
|
||||
onDelete={() => deleteColorStopAndApply(index)}
|
||||
onChange={(cp: ColorStop) => updateColorStopAndApply(index, cp)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<PalettePicker
|
||||
additionalPalettes={palette?.id === 'custom' ? [palette] : []}
|
||||
palette={props.palette ?? undefined}
|
||||
onChange={onChangePalette}
|
||||
clearable={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFormRow label={strings.getColorStopsLabel()}>
|
||||
<EuiFlexGroup gutterSize="s" direction="column">
|
||||
{stopColorPickers}
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiButtonEmpty
|
||||
iconType="plusInCircle"
|
||||
color="primary"
|
||||
aria-label={strings.getAddColorStopLabel()}
|
||||
size="xs"
|
||||
flush="left"
|
||||
onClick={addColorStopAndApply}
|
||||
>
|
||||
{strings.getAddColorStopLabel()}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { CustomColorPalette } from '../types';
|
||||
import { ColorPalette } from '../../../../common/lib/palettes';
|
||||
|
||||
export type Palette = ColorPalette | CustomColorPalette;
|
||||
export type PaletteColorStops = Pick<Palette, 'stops' | 'colors'>;
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { zip, take } from 'lodash';
|
||||
import { htmlIdGenerator } from '@elastic/eui';
|
||||
import { ColorPalette } from '../../../../common/lib';
|
||||
import { ColorStop } from '../types';
|
||||
import { Palette, PaletteColorStops } from './types';
|
||||
|
||||
const id = htmlIdGenerator();
|
||||
|
||||
export const getOverridenPaletteOptions = (): Pick<ColorPalette, 'range' | 'continuity'> => ({
|
||||
range: 'number',
|
||||
continuity: 'below',
|
||||
});
|
||||
|
||||
export const createColorStop = (stop: number = 0, color: string = '') => ({
|
||||
stop,
|
||||
color,
|
||||
id: id(),
|
||||
});
|
||||
|
||||
export const transformPaletteToColorStops = ({ stops = [], colors }: PaletteColorStops) =>
|
||||
zip(stops, colors).map(([stop, color]) => createColorStop(stop, color));
|
||||
|
||||
export const mergeColorStopsWithPalette =
|
||||
(palette: Palette) =>
|
||||
(colorStops: ColorStop[]): Palette => {
|
||||
const stopsWithColors = colorStops.reduce<{ colors: string[]; stops: number[] }>(
|
||||
(acc, { color, stop }) => {
|
||||
acc.colors.push(color ?? '');
|
||||
acc.stops.push(stop);
|
||||
return acc;
|
||||
},
|
||||
{ colors: [], stops: [] }
|
||||
);
|
||||
return { ...palette, ...stopsWithColors };
|
||||
};
|
||||
|
||||
export const updateColorStop =
|
||||
(index: number, colorStop: ColorStop) => (colorStops: ColorStop[]) => {
|
||||
colorStops.splice(index, 1, colorStop);
|
||||
return colorStops;
|
||||
};
|
||||
|
||||
export const deleteColorStop = (index: number) => (colorStops: ColorStop[]) => {
|
||||
colorStops.splice(index, 1);
|
||||
return colorStops;
|
||||
};
|
||||
|
||||
export const addNewColorStop = (palette: Palette) => (colorStops: ColorStop[]) => {
|
||||
const lastColorStopIndex = colorStops.length - 1;
|
||||
const lastStop = lastColorStopIndex >= 0 ? colorStops[lastColorStopIndex].stop + 1 : 0;
|
||||
const newIndex = lastColorStopIndex + 1;
|
||||
return [
|
||||
...colorStops,
|
||||
{
|
||||
stop: lastStop,
|
||||
color:
|
||||
palette.colors.length > newIndex + 1
|
||||
? palette.colors[newIndex]
|
||||
: palette.colors[palette.colors.length - 1],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const reduceColorsByStopsSize = (colors: string[] = [], stopsSize: number) => {
|
||||
const reducedColors = take(colors, stopsSize);
|
||||
const colorsLength = reducedColors.length;
|
||||
if (colorsLength === stopsSize) {
|
||||
return reducedColors;
|
||||
}
|
||||
|
||||
return [
|
||||
...reducedColors,
|
||||
...Array(stopsSize - colorsLength).fill(reducedColors[colorsLength - 1] ?? ''),
|
||||
];
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { ColorPalette } from '../../../common/lib/palettes';
|
||||
|
||||
export type CustomColorPalette = ColorPalette<'custom'>;
|
||||
|
||||
export interface RequiredProps {
|
||||
id?: string;
|
||||
onChange?: (palette: ColorPalette | CustomColorPalette) => void;
|
||||
palette: ColorPalette | CustomColorPalette;
|
||||
clearable?: false;
|
||||
additionalPalettes?: Array<ColorPalette | CustomColorPalette>;
|
||||
}
|
||||
|
||||
export interface ClearableProps {
|
||||
id?: string;
|
||||
onChange?: (palette: ColorPalette | CustomColorPalette | null) => void;
|
||||
palette: ColorPalette | CustomColorPalette | null;
|
||||
clearable: true;
|
||||
additionalPalettes?: Array<ColorPalette | CustomColorPalette>;
|
||||
}
|
||||
|
||||
export type PalettePickerProps = RequiredProps | ClearableProps;
|
||||
export type StopsPalettePickerProps = RequiredProps;
|
||||
|
||||
export type ClearableComponentProps = {
|
||||
palettes: Array<ColorPalette | CustomColorPalette>;
|
||||
} & Partial<Pick<ClearableProps, 'onChange' | 'palette'>> &
|
||||
Pick<ClearableProps, 'id'>;
|
||||
|
||||
export type RequiredComponentProps = {
|
||||
palettes: Array<ColorPalette | CustomColorPalette>;
|
||||
} & Partial<Pick<RequiredProps, 'onChange' | 'palette' | 'id'>>;
|
||||
|
||||
export interface ColorStop {
|
||||
color: string;
|
||||
stop: number;
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { EuiColorPalettePickerPaletteProps } from '@elastic/eui';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ColorPalette } from '../../../common/lib/palettes';
|
||||
import { CustomColorPalette } from './types';
|
||||
|
||||
export const findPalette = (
|
||||
colorPalette: ColorPalette | CustomColorPalette | null,
|
||||
colorPalettes: Array<ColorPalette | CustomColorPalette> = []
|
||||
) => {
|
||||
const palette = colorPalettes.filter((cp) => cp.id === colorPalette?.id)[0] ?? null;
|
||||
if (palette === null) {
|
||||
return colorPalettes.filter((cp) => isEqual(cp.colors, colorPalette?.colors))[0] ?? null;
|
||||
}
|
||||
|
||||
return palette;
|
||||
};
|
||||
|
||||
export const prepareColorPalette = ({
|
||||
id,
|
||||
label,
|
||||
gradient,
|
||||
colors,
|
||||
}: ColorPalette | CustomColorPalette): EuiColorPalettePickerPaletteProps => ({
|
||||
value: id,
|
||||
title: label,
|
||||
type: gradient ? 'gradient' : 'fixed',
|
||||
palette: colors,
|
||||
});
|
|
@ -61,7 +61,6 @@ export const ElementSettings: FunctionComponent<Props> = ({ element }) => {
|
|||
),
|
||||
},
|
||||
];
|
||||
|
||||
return <EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} size="s" />;
|
||||
};
|
||||
|
||||
|
|
|
@ -6,31 +6,24 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import deepEqual from 'react-fast-compare';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getElementById, getSelectedPage } from '../../../state/selectors/workpad';
|
||||
import { ElementSettings as Component } from './element_settings.component';
|
||||
import { State, PositionedElement } from '../../../../types';
|
||||
import { State } from '../../../../types';
|
||||
|
||||
interface Props {
|
||||
selectedElementId: string | null;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State, { selectedElementId }: Props): StateProps => ({
|
||||
element: getElementById(state, selectedElementId, getSelectedPage(state)),
|
||||
});
|
||||
export const ElementSettings: React.FC<Props> = ({ selectedElementId }) => {
|
||||
const element = useSelector((state: State) => {
|
||||
return getElementById(state, selectedElementId, getSelectedPage(state));
|
||||
}, deepEqual);
|
||||
|
||||
interface StateProps {
|
||||
element: PositionedElement | undefined;
|
||||
}
|
||||
|
||||
const renderIfElement: React.FunctionComponent<StateProps> = (props) => {
|
||||
if (props.element) {
|
||||
return <Component element={props.element} />;
|
||||
if (element) {
|
||||
return <Component element={element} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const ElementSettings = connect<StateProps, {}, Props, State>(mapStateToProps)(
|
||||
renderIfElement
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { getSelectedToplevelNodes, getSelectedElementId } from '../../../state/selectors/workpad';
|
||||
import { State } from '../../../../types';
|
||||
import { SidebarContent as Component } from './sidebar_content.component';
|
||||
|
@ -16,12 +16,14 @@ interface SidebarContentProps {
|
|||
}
|
||||
|
||||
export const SidebarContent: React.FC<SidebarContentProps> = ({ commit }) => {
|
||||
const selectedToplevelNodes = useSelector<State, string[]>((state) =>
|
||||
getSelectedToplevelNodes(state)
|
||||
const selectedToplevelNodes = useSelector<State, string[]>(
|
||||
(state) => getSelectedToplevelNodes(state),
|
||||
shallowEqual
|
||||
);
|
||||
|
||||
const selectedElementId = useSelector<State, string | null>((state) =>
|
||||
getSelectedElementId(state)
|
||||
const selectedElementId = useSelector<State, string | null>(
|
||||
(state) => getSelectedElementId(state),
|
||||
shallowEqual
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -16,7 +16,7 @@ import { CommitFn } from '../../../types';
|
|||
|
||||
export const WORKPAD_CONTAINER_ID = 'canvasWorkpadContainer';
|
||||
|
||||
interface Props {
|
||||
export interface Props {
|
||||
deselectElement?: MouseEventHandler;
|
||||
isWriteable: boolean;
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
|
||||
import { MouseEventHandler } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
// @ts-expect-error untyped local
|
||||
import { selectToplevelNodes } from '../../state/actions/transient';
|
||||
import { canUserWrite } from '../../state/selectors/app';
|
||||
|
@ -18,6 +18,8 @@ import { State } from '../../../types';
|
|||
|
||||
export { WORKPAD_CONTAINER_ID } from './workpad_app.component';
|
||||
|
||||
const WorkpadAppComponent = withElementsLoadedTelemetry(Component);
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): { deselectElement: MouseEventHandler } => ({
|
||||
deselectElement: (ev) => {
|
||||
ev.stopPropagation();
|
||||
|
@ -31,4 +33,4 @@ export const WorkpadApp = connect(
|
|||
workpad: getWorkpad(state),
|
||||
}),
|
||||
mapDispatchToProps
|
||||
)(withElementsLoadedTelemetry(Component));
|
||||
)(WorkpadAppComponent);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { render } from '@testing-library/react';
|
||||
import {
|
||||
withUnconnectedElementsLoadedTelemetry,
|
||||
|
@ -13,148 +14,137 @@ import {
|
|||
WorkpadLoadedWithErrorsMetric,
|
||||
} from './workpad_telemetry';
|
||||
import { METRIC_TYPE } from '../../lib/ui_metric';
|
||||
import { ResolvedArgType } from '../../../types';
|
||||
import { ExpressionContext, ResolvedArgType } from '../../../types';
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const originalModule = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
useSelector: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const trackMetric = jest.fn();
|
||||
const useSelectorMock = useSelector as jest.Mock;
|
||||
|
||||
const Component = withUnconnectedElementsLoadedTelemetry(() => <div />, trackMetric);
|
||||
|
||||
const mockWorkpad = {
|
||||
id: 'workpadid',
|
||||
pages: [
|
||||
{
|
||||
elements: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }],
|
||||
elements: [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }],
|
||||
},
|
||||
{
|
||||
elements: [{ id: '5' }],
|
||||
elements: [{ id: '4' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const resolvedArgsMatchWorkpad = {
|
||||
'1': {} as ResolvedArgType,
|
||||
'2': {} as ResolvedArgType,
|
||||
'3': {} as ResolvedArgType,
|
||||
'4': {} as ResolvedArgType,
|
||||
'5': {} as ResolvedArgType,
|
||||
};
|
||||
const getMockState = (resolvedArgs: Record<string, ResolvedArgType>) => ({
|
||||
transient: { resolvedArgs },
|
||||
});
|
||||
|
||||
const resolvedArgsNotMatchWorkpad = {
|
||||
'non-matching-id': {} as ResolvedArgType,
|
||||
};
|
||||
const getResolveArgWithState = (state: 'pending' | 'ready' | 'error') =>
|
||||
({
|
||||
expressionRenderable: { value: { as: state, type: 'render' }, state, error: null },
|
||||
expressionContext: {} as ExpressionContext,
|
||||
} as ResolvedArgType);
|
||||
|
||||
const pendingCounts = {
|
||||
pending: 5,
|
||||
error: 0,
|
||||
ready: 0,
|
||||
};
|
||||
const arrayToObject = (array: ResolvedArgType[]) =>
|
||||
array.reduce<Record<number, ResolvedArgType>>((acc, el, index) => {
|
||||
acc[index] = el;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const readyCounts = {
|
||||
pending: 0,
|
||||
error: 0,
|
||||
ready: 5,
|
||||
};
|
||||
const pendingMockState = getMockState(
|
||||
arrayToObject(Array(5).fill(getResolveArgWithState('pending')))
|
||||
);
|
||||
|
||||
const errorCounts = {
|
||||
pending: 0,
|
||||
error: 1,
|
||||
ready: 4,
|
||||
};
|
||||
const readyMockState = getMockState(arrayToObject(Array(5).fill(getResolveArgWithState('ready'))));
|
||||
|
||||
const errorMockState = getMockState(
|
||||
arrayToObject([
|
||||
...Array(4).fill(getResolveArgWithState('ready')),
|
||||
...Array(1).fill(getResolveArgWithState('error')),
|
||||
])
|
||||
);
|
||||
|
||||
const emptyElementsMockState = getMockState({});
|
||||
|
||||
const notMatchedMockState = getMockState({
|
||||
'non-matching-id': getResolveArgWithState('ready'),
|
||||
});
|
||||
|
||||
describe('Elements Loaded Telemetry', () => {
|
||||
beforeEach(() => {
|
||||
trackMetric.mockReset();
|
||||
});
|
||||
|
||||
it('tracks when all resolvedArgs are completed', () => {
|
||||
const { rerender } = render(
|
||||
<Component
|
||||
telemetryElementCounts={pendingCounts}
|
||||
telemetryResolvedArgs={resolvedArgsMatchWorkpad}
|
||||
workpad={mockWorkpad}
|
||||
/>
|
||||
);
|
||||
afterEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
});
|
||||
|
||||
it('tracks when all resolvedArgs are completed', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(pendingMockState);
|
||||
});
|
||||
|
||||
const { rerender } = render(<Component workpad={mockWorkpad} />);
|
||||
expect(trackMetric).not.toBeCalled();
|
||||
|
||||
rerender(
|
||||
<Component
|
||||
telemetryElementCounts={readyCounts}
|
||||
telemetryResolvedArgs={resolvedArgsMatchWorkpad}
|
||||
workpad={mockWorkpad}
|
||||
/>
|
||||
);
|
||||
useSelectorMock.mockClear();
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(readyMockState);
|
||||
});
|
||||
|
||||
rerender(<Component workpad={mockWorkpad} />);
|
||||
expect(trackMetric).toBeCalledWith(METRIC_TYPE.LOADED, WorkpadLoadedMetric);
|
||||
});
|
||||
|
||||
it('only tracks loaded once', () => {
|
||||
const { rerender } = render(
|
||||
<Component
|
||||
telemetryElementCounts={pendingCounts}
|
||||
telemetryResolvedArgs={resolvedArgsMatchWorkpad}
|
||||
workpad={mockWorkpad}
|
||||
/>
|
||||
);
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(pendingMockState);
|
||||
});
|
||||
|
||||
const { rerender } = render(<Component workpad={mockWorkpad} />);
|
||||
expect(trackMetric).not.toBeCalled();
|
||||
|
||||
rerender(
|
||||
<Component
|
||||
telemetryElementCounts={readyCounts}
|
||||
telemetryResolvedArgs={resolvedArgsMatchWorkpad}
|
||||
workpad={mockWorkpad}
|
||||
/>
|
||||
);
|
||||
rerender(
|
||||
<Component
|
||||
telemetryElementCounts={readyCounts}
|
||||
telemetryResolvedArgs={resolvedArgsMatchWorkpad}
|
||||
workpad={mockWorkpad}
|
||||
/>
|
||||
);
|
||||
useSelectorMock.mockClear();
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(readyMockState);
|
||||
});
|
||||
|
||||
rerender(<Component workpad={mockWorkpad} />);
|
||||
rerender(<Component workpad={mockWorkpad} />);
|
||||
expect(trackMetric).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not track if resolvedArgs are never pending', () => {
|
||||
const { rerender } = render(
|
||||
<Component
|
||||
telemetryElementCounts={readyCounts}
|
||||
telemetryResolvedArgs={resolvedArgsMatchWorkpad}
|
||||
workpad={mockWorkpad}
|
||||
/>
|
||||
);
|
||||
|
||||
rerender(
|
||||
<Component
|
||||
telemetryElementCounts={readyCounts}
|
||||
telemetryResolvedArgs={resolvedArgsMatchWorkpad}
|
||||
workpad={mockWorkpad}
|
||||
/>
|
||||
);
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(readyMockState);
|
||||
});
|
||||
|
||||
const { rerender } = render(<Component workpad={mockWorkpad} />);
|
||||
rerender(<Component workpad={mockWorkpad} />);
|
||||
expect(trackMetric).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('tracks if elements are in error state after load', () => {
|
||||
const { rerender } = render(
|
||||
<Component
|
||||
telemetryElementCounts={pendingCounts}
|
||||
telemetryResolvedArgs={resolvedArgsMatchWorkpad}
|
||||
workpad={mockWorkpad}
|
||||
/>
|
||||
);
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(pendingMockState);
|
||||
});
|
||||
|
||||
const { rerender } = render(<Component workpad={mockWorkpad} />);
|
||||
expect(trackMetric).not.toBeCalled();
|
||||
|
||||
rerender(
|
||||
<Component
|
||||
telemetryElementCounts={errorCounts}
|
||||
telemetryResolvedArgs={resolvedArgsMatchWorkpad}
|
||||
workpad={mockWorkpad}
|
||||
/>
|
||||
);
|
||||
useSelectorMock.mockClear();
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(errorMockState);
|
||||
});
|
||||
|
||||
rerender(<Component workpad={mockWorkpad} />);
|
||||
expect(trackMetric).toBeCalledWith(METRIC_TYPE.LOADED, [
|
||||
WorkpadLoadedMetric,
|
||||
WorkpadLoadedWithErrorsMetric,
|
||||
|
@ -166,42 +156,30 @@ describe('Elements Loaded Telemetry', () => {
|
|||
id: 'otherworkpad',
|
||||
pages: [
|
||||
{
|
||||
elements: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }],
|
||||
elements: [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }],
|
||||
},
|
||||
{
|
||||
elements: [{ id: '5' }],
|
||||
elements: [{ id: '4' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<Component
|
||||
telemetryElementCounts={readyCounts}
|
||||
telemetryResolvedArgs={resolvedArgsNotMatchWorkpad}
|
||||
workpad={otherWorkpad}
|
||||
/>
|
||||
);
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(notMatchedMockState);
|
||||
});
|
||||
|
||||
const { rerender } = render(<Component workpad={otherWorkpad} />);
|
||||
expect(trackMetric).not.toBeCalled();
|
||||
|
||||
rerender(
|
||||
<Component
|
||||
telemetryElementCounts={readyCounts}
|
||||
telemetryResolvedArgs={resolvedArgsNotMatchWorkpad}
|
||||
workpad={mockWorkpad}
|
||||
/>
|
||||
);
|
||||
|
||||
rerender(<Component workpad={mockWorkpad} />);
|
||||
expect(trackMetric).not.toBeCalled();
|
||||
|
||||
rerender(
|
||||
<Component
|
||||
telemetryElementCounts={readyCounts}
|
||||
telemetryResolvedArgs={resolvedArgsMatchWorkpad}
|
||||
workpad={mockWorkpad}
|
||||
/>
|
||||
);
|
||||
useSelectorMock.mockClear();
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(readyMockState);
|
||||
});
|
||||
|
||||
rerender(<Component workpad={mockWorkpad} />);
|
||||
expect(trackMetric).toBeCalledWith(METRIC_TYPE.LOADED, WorkpadLoadedMetric);
|
||||
});
|
||||
|
||||
|
@ -211,24 +189,12 @@ describe('Elements Loaded Telemetry', () => {
|
|||
pages: [],
|
||||
};
|
||||
|
||||
const resolvedArgs = {};
|
||||
|
||||
const { rerender } = render(
|
||||
<Component
|
||||
telemetryElementCounts={readyCounts}
|
||||
telemetryResolvedArgs={resolvedArgs}
|
||||
workpad={otherWorkpad}
|
||||
/>
|
||||
);
|
||||
|
||||
rerender(
|
||||
<Component
|
||||
telemetryElementCounts={readyCounts}
|
||||
telemetryResolvedArgs={resolvedArgs}
|
||||
workpad={otherWorkpad}
|
||||
/>
|
||||
);
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(emptyElementsMockState);
|
||||
});
|
||||
|
||||
const { rerender } = render(<Component workpad={otherWorkpad} />);
|
||||
rerender(<Component workpad={otherWorkpad} />);
|
||||
expect(trackMetric).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,21 +6,18 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import deepEqual from 'react-fast-compare';
|
||||
import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric';
|
||||
import { getElementCounts } from '../../state/selectors/workpad';
|
||||
import { getArgs } from '../../state/selectors/resolved_args';
|
||||
import { State } from '../../../types';
|
||||
|
||||
const WorkpadLoadedMetric = 'workpad-loaded';
|
||||
const WorkpadLoadedWithErrorsMetric = 'workpad-loaded-with-errors';
|
||||
|
||||
export { WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric };
|
||||
|
||||
const mapStateToProps = (state: any) => ({
|
||||
telemetryElementCounts: getElementCounts(state),
|
||||
telemetryResolvedArgs: getArgs(state),
|
||||
});
|
||||
|
||||
// TODO: Build out full workpad types
|
||||
/**
|
||||
Individual Page of a Workpad
|
||||
|
@ -47,7 +44,7 @@ interface ResolvedArgs {
|
|||
[keys: string]: any;
|
||||
}
|
||||
|
||||
export interface ElementsLoadedTelemetryProps extends PropsFromRedux {
|
||||
export interface ElementsLoadedTelemetryProps {
|
||||
workpad: Workpad;
|
||||
}
|
||||
|
||||
|
@ -65,33 +62,32 @@ export const withUnconnectedElementsLoadedTelemetry = <P extends {}>(
|
|||
Component: React.ComponentType<P>,
|
||||
trackMetric = trackCanvasUiMetric
|
||||
) =>
|
||||
function ElementsLoadedTelemetry(props: ElementsLoadedTelemetryProps) {
|
||||
const { telemetryElementCounts, workpad, telemetryResolvedArgs, ...other } = props;
|
||||
const { error, pending } = telemetryElementCounts;
|
||||
function ElementsLoadedTelemetry(props: P & ElementsLoadedTelemetryProps) {
|
||||
const { workpad } = props;
|
||||
|
||||
const [currentWorkpadId, setWorkpadId] = useState<string | undefined>(undefined);
|
||||
const [hasReported, setHasReported] = useState(false);
|
||||
const telemetryElementCounts = useSelector(
|
||||
(state: State) => getElementCounts(state),
|
||||
shallowEqual
|
||||
);
|
||||
|
||||
const telemetryResolvedArgs = useSelector((state: State) => getArgs(state), deepEqual);
|
||||
|
||||
const resolvedArgsAreForWorkpad = areAllElementsInResolvedArgs(workpad, telemetryResolvedArgs);
|
||||
const { error, pending } = telemetryElementCounts;
|
||||
const resolved = resolvedArgsAreForWorkpad && pending === 0;
|
||||
|
||||
useEffect(() => {
|
||||
const resolvedArgsAreForWorkpad = areAllElementsInResolvedArgs(
|
||||
workpad,
|
||||
telemetryResolvedArgs
|
||||
);
|
||||
|
||||
if (workpad.id !== currentWorkpadId) {
|
||||
setWorkpadId(workpad.id);
|
||||
|
||||
const workpadElementCount = workpad.pages.reduce(
|
||||
(reduction, page) => reduction + page.elements.length,
|
||||
0
|
||||
);
|
||||
|
||||
if (workpadElementCount === 0 || (resolvedArgsAreForWorkpad && pending === 0)) {
|
||||
setHasReported(true);
|
||||
} else {
|
||||
setHasReported(false);
|
||||
}
|
||||
} else if (!hasReported && pending === 0 && resolvedArgsAreForWorkpad) {
|
||||
setWorkpadId(workpad.id);
|
||||
setHasReported(workpadElementCount === 0 || resolved);
|
||||
} else if (!hasReported && resolved) {
|
||||
if (error > 0) {
|
||||
trackMetric(METRIC_TYPE.LOADED, [WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]);
|
||||
} else {
|
||||
|
@ -99,16 +95,9 @@ export const withUnconnectedElementsLoadedTelemetry = <P extends {}>(
|
|||
}
|
||||
setHasReported(true);
|
||||
}
|
||||
}, [currentWorkpadId, hasReported, error, pending, telemetryResolvedArgs, workpad]);
|
||||
|
||||
return <Component {...(other as P)} workpad={workpad} />;
|
||||
}, [currentWorkpadId, hasReported, error, workpad.id, resolved, workpad.pages]);
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, {});
|
||||
|
||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export const withElementsLoadedTelemetry = <P extends {}>(Component: React.ComponentType<P>) => {
|
||||
const telemetry = withUnconnectedElementsLoadedTelemetry(Component);
|
||||
return connector(telemetry);
|
||||
};
|
||||
export const withElementsLoadedTelemetry = <P extends {}>(Component: React.ComponentType<P>) =>
|
||||
withUnconnectedElementsLoadedTelemetry(Component);
|
||||
|
|
|
@ -12,6 +12,7 @@ import { RenderToDom } from '../components/render_to_dom';
|
|||
import { BaseForm, BaseFormProps } from './base_form';
|
||||
import { ExpressionFormHandlers } from '../../common/lib';
|
||||
import { ExpressionFunction } from '../../types';
|
||||
import { UpdatePropsRef } from '../../types/arguments';
|
||||
|
||||
const defaultTemplate = () => (
|
||||
<div>
|
||||
|
@ -22,7 +23,8 @@ const defaultTemplate = () => (
|
|||
type TemplateFn = (
|
||||
domNode: HTMLElement,
|
||||
config: DatasourceRenderProps,
|
||||
handlers: ExpressionFormHandlers
|
||||
handlers: ExpressionFormHandlers,
|
||||
onMount?: (ref: UpdatePropsRef<DatasourceRenderProps> | null) => void
|
||||
) => void;
|
||||
|
||||
export type DatasourceProps = {
|
||||
|
@ -49,6 +51,8 @@ interface DatasourceWrapperProps {
|
|||
|
||||
const DatasourceWrapper: React.FunctionComponent<DatasourceWrapperProps> = (props) => {
|
||||
const domNodeRef = useRef<HTMLElement>();
|
||||
const datasourceRef = useRef<UpdatePropsRef<DatasourceRenderProps>>();
|
||||
|
||||
const { spec, datasourceProps, handlers } = props;
|
||||
|
||||
const callRenderFn = useCallback(() => {
|
||||
|
@ -58,14 +62,23 @@ const DatasourceWrapper: React.FunctionComponent<DatasourceWrapperProps> = (prop
|
|||
return;
|
||||
}
|
||||
|
||||
template(domNodeRef.current, datasourceProps, handlers);
|
||||
template(domNodeRef.current, datasourceProps, handlers, (ref) => {
|
||||
datasourceRef.current = ref ?? undefined;
|
||||
});
|
||||
}, [datasourceProps, handlers, spec]);
|
||||
|
||||
useEffect(() => {
|
||||
callRenderFn();
|
||||
}, [callRenderFn, props]);
|
||||
}, [callRenderFn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (datasourceRef.current) {
|
||||
datasourceRef.current.updateProps(datasourceProps);
|
||||
}
|
||||
}, [datasourceProps]);
|
||||
|
||||
useEffectOnce(() => () => {
|
||||
datasourceRef.current = undefined;
|
||||
handlers.destroy();
|
||||
});
|
||||
|
||||
|
|
|
@ -140,7 +140,6 @@ export class FunctionForm extends BaseForm {
|
|||
|
||||
// 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`);
|
||||
}
|
||||
|
@ -153,7 +152,6 @@ export class FunctionForm extends BaseForm {
|
|||
// 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 };
|
||||
});
|
||||
|
||||
|
|
|
@ -5,42 +5,72 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ComponentType, FC } from 'react';
|
||||
import React, {
|
||||
ComponentType,
|
||||
forwardRef,
|
||||
ForwardRefRenderFunction,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { unmountComponentAtNode, render } from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { ErrorBoundary } from '../components/enhance/error_boundary';
|
||||
import { ArgumentHandlers } from '../../types/arguments';
|
||||
import { ArgumentHandlers, UpdatePropsRef } from '../../types/arguments';
|
||||
|
||||
export interface Props {
|
||||
renderError: Function;
|
||||
}
|
||||
|
||||
export const templateFromReactComponent = (Component: ComponentType<any>) => {
|
||||
const WrappedComponent: FC<Props> = (props) => (
|
||||
<ErrorBoundary>
|
||||
{({ error }) => {
|
||||
if (error) {
|
||||
props.renderError();
|
||||
return null;
|
||||
}
|
||||
const WrappedComponent: ForwardRefRenderFunction<UpdatePropsRef<Props>, Props> = (props, ref) => {
|
||||
const [updatedProps, setUpdatedProps] = useState<Props>(props);
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<Component {...props} />
|
||||
</I18nProvider>
|
||||
);
|
||||
}}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
useImperativeHandle(ref, () => ({
|
||||
updateProps: (newProps: Props) => {
|
||||
setUpdatedProps(newProps);
|
||||
},
|
||||
}));
|
||||
|
||||
WrappedComponent.propTypes = {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
{({ error }) => {
|
||||
if (error) {
|
||||
props.renderError();
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<Component {...updatedProps} />
|
||||
</I18nProvider>
|
||||
);
|
||||
}}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const ForwardRefWrappedComponent = forwardRef(WrappedComponent);
|
||||
|
||||
ForwardRefWrappedComponent.propTypes = {
|
||||
renderError: PropTypes.func,
|
||||
};
|
||||
|
||||
return (domNode: HTMLElement, config: Props, handlers: ArgumentHandlers) => {
|
||||
return (
|
||||
domNode: HTMLElement,
|
||||
config: Props,
|
||||
handlers: ArgumentHandlers,
|
||||
onMount?: (ref: UpdatePropsRef<Props> | null) => void
|
||||
) => {
|
||||
try {
|
||||
const el = React.createElement(WrappedComponent, config);
|
||||
const el = (
|
||||
<ForwardRefWrappedComponent
|
||||
{...config}
|
||||
ref={(ref) => {
|
||||
onMount?.(ref);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
render(el, domNode, () => {
|
||||
handlers.done();
|
||||
});
|
||||
|
|
|
@ -19,6 +19,10 @@ export interface ArgumentHandlers {
|
|||
onDestroy: GenericCallback;
|
||||
}
|
||||
|
||||
export interface UpdatePropsRef<Props extends {} = {}> {
|
||||
updateProps: (newProps: Props) => void;
|
||||
}
|
||||
|
||||
export interface ArgumentSpec<ArgumentConfig = {}> {
|
||||
/** The argument type */
|
||||
name: string;
|
||||
|
@ -33,13 +37,19 @@ export interface ArgumentSpec<ArgumentConfig = {}> {
|
|||
simpleTemplate?: (
|
||||
domNode: HTMLElement,
|
||||
config: ArgumentConfig,
|
||||
handlers: ArgumentHandlers
|
||||
handlers: ArgumentHandlers,
|
||||
onMount: (ref: UpdatePropsRef<ArgumentConfig> | null) => void
|
||||
) => void;
|
||||
/**
|
||||
* A function that renders a complex/large argument
|
||||
* This is nested in an accordian so it can be expanded/collapsed
|
||||
*/
|
||||
template?: (domNode: HTMLElement, config: ArgumentConfig, handlers: ArgumentHandlers) => void;
|
||||
template?: (
|
||||
domNode: HTMLElement,
|
||||
config: ArgumentConfig,
|
||||
handlers: ArgumentHandlers,
|
||||
onMount: (ref: UpdatePropsRef<ArgumentConfig> | null) => void
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type ArgumentFactory<ArgumentConfig = {}> = () => ArgumentSpec<ArgumentConfig>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue