mirror of https://github.com/apache/superset.git
fix(plugin-chart-handlebars): fix overflow, debounce and control reset (#19879)
* fix(plugin-chart-handlebars): fix overflow * add debounce, fix reset controls * fix deps * remove redundant code * improve examples * add last missing resetOnHides * fix test * use isPlainObject
This commit is contained in:
parent
1d50665da0
commit
d5ea537b0e
|
@ -26,19 +26,20 @@
|
|||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@superset-ui/chart-controls": "0.18.25",
|
||||
"@superset-ui/core": "0.18.25",
|
||||
"ace-builds": "^1.4.13",
|
||||
"emotion": "^11.0.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"react-ace": "^9.4.4"
|
||||
"handlebars": "^4.7.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"ace-builds": "^1.4.14",
|
||||
"lodash": "^4.17.11",
|
||||
"moment": "^2.26.0",
|
||||
"react": "^16.13.1",
|
||||
"react-ace": "^9.4.4",
|
||||
"react-dom": "^16.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/jest": "^26.0.0",
|
||||
"jest": "^26.0.1"
|
||||
}
|
||||
|
|
|
@ -17,36 +17,19 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { styled } from '@superset-ui/core';
|
||||
import React, { createRef, useEffect } from 'react';
|
||||
import React, { createRef } from 'react';
|
||||
import { HandlebarsViewer } from './components/Handlebars/HandlebarsViewer';
|
||||
import { HandlebarsProps, HandlebarsStylesProps } from './types';
|
||||
|
||||
// The following Styles component is a <div> element, which has been styled using Emotion
|
||||
// For docs, visit https://emotion.sh/docs/styled
|
||||
|
||||
// Theming variables are provided for your use via a ThemeProvider
|
||||
// imported from @superset-ui/core. For variables available, please visit
|
||||
// https://github.com/apache-superset/superset-ui/blob/master/packages/superset-ui-core/src/style/index.ts
|
||||
|
||||
const Styles = styled.div<HandlebarsStylesProps>`
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
border-radius: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
height: ${({ height }) => height};
|
||||
width: ${({ width }) => width};
|
||||
overflow-y: scroll;
|
||||
height: ${({ height }) => height}px;
|
||||
width: ${({ width }) => width}px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
/**
|
||||
* ******************* WHAT YOU CAN BUILD HERE *******************
|
||||
* In essence, a chart is given a few key ingredients to work with:
|
||||
* * Data: provided via `props.data`
|
||||
* * A DOM element
|
||||
* * FormData (your controls!) provided as props by transformProps.ts
|
||||
*/
|
||||
|
||||
export default function Handlebars(props: HandlebarsProps) {
|
||||
// height and width are the height and width of the DOM element as it exists in the dashboard.
|
||||
// There is also a `data` prop, which is, of course, your DATA 🎉
|
||||
const { data, height, width, formData } = props;
|
||||
const styleTemplateSource = formData.styleTemplate
|
||||
? `<style>${formData.styleTemplate}</style>`
|
||||
|
@ -58,13 +41,6 @@ export default function Handlebars(props: HandlebarsProps) {
|
|||
|
||||
const rootElem = createRef<HTMLDivElement>();
|
||||
|
||||
// Often, you just want to get a hold of the DOM and go nuts.
|
||||
// Here, you can do that with createRef, and the useEffect hook.
|
||||
useEffect(() => {
|
||||
// const root = rootElem.current as HTMLElement;
|
||||
// console.log('Plugin element', root);
|
||||
});
|
||||
|
||||
return (
|
||||
<Styles ref={rootElem} height={height} width={width}>
|
||||
<HandlebarsViewer data={{ data }} templateSource={templateSource} />
|
||||
|
|
|
@ -20,6 +20,7 @@ import { SafeMarkdown, styled } from '@superset-ui/core';
|
|||
import Handlebars from 'handlebars';
|
||||
import moment from 'moment';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { isPlainObject } from 'lodash';
|
||||
|
||||
export interface HandlebarsViewerProps {
|
||||
templateSource: string;
|
||||
|
@ -64,3 +65,11 @@ Handlebars.registerHelper('dateFormat', function (context, block) {
|
|||
const f = block.hash.format || 'YYYY-MM-DD';
|
||||
return moment(context).format(f);
|
||||
});
|
||||
|
||||
// usage: {{ }}
|
||||
Handlebars.registerHelper('stringify', (obj: any, obj2: any) => {
|
||||
// calling without an argument
|
||||
if (obj2 === undefined)
|
||||
throw Error('Please call with an object. Example: `stringify myObj`');
|
||||
return isPlainObject(obj) ? JSON.stringify(obj) : String(obj);
|
||||
});
|
||||
|
|
|
@ -16,8 +16,9 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { debounce } from 'lodash';
|
||||
import { formatSelectOptions } from '@superset-ui/chart-controls';
|
||||
import { addLocaleData, t } from '@superset-ui/core';
|
||||
import { addLocaleData, SLOW_DEBOUNCE, t } from '@superset-ui/core';
|
||||
import i18n from './i18n';
|
||||
|
||||
addLocaleData(i18n);
|
||||
|
@ -35,3 +36,8 @@ export const PAGE_SIZE_OPTIONS = formatSelectOptions<number>([
|
|||
100,
|
||||
200,
|
||||
]);
|
||||
|
||||
export const debounceFunc = debounce(
|
||||
(func: (val: string) => void, source: string) => func(source),
|
||||
SLOW_DEBOUNCE,
|
||||
);
|
||||
|
|
|
@ -50,81 +50,6 @@ import { styleControlSetItem } from './controls/style';
|
|||
addLocaleData(i18n);
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
/**
|
||||
* The control panel is split into two tabs: "Query" and
|
||||
* "Chart Options". The controls that define the inputs to
|
||||
* the chart data request, such as columns and metrics, usually
|
||||
* reside within "Query", while controls that affect the visual
|
||||
* appearance or functionality of the chart are under the
|
||||
* "Chart Options" section.
|
||||
*
|
||||
* There are several predefined controls that can be used.
|
||||
* Some examples:
|
||||
* - groupby: columns to group by (tranlated to GROUP BY statement)
|
||||
* - series: same as groupby, but single selection.
|
||||
* - metrics: multiple metrics (translated to aggregate expression)
|
||||
* - metric: sane as metrics, but single selection
|
||||
* - adhoc_filters: filters (translated to WHERE or HAVING
|
||||
* depending on filter type)
|
||||
* - row_limit: maximum number of rows (translated to LIMIT statement)
|
||||
*
|
||||
* If a control panel has both a `series` and `groupby` control, and
|
||||
* the user has chosen `col1` as the value for the `series` control,
|
||||
* and `col2` and `col3` as values for the `groupby` control,
|
||||
* the resulting query will contain three `groupby` columns. This is because
|
||||
* we considered `series` control a `groupby` query field and its value
|
||||
* will automatically append the `groupby` field when the query is generated.
|
||||
*
|
||||
* It is also possible to define custom controls by importing the
|
||||
* necessary dependencies and overriding the default parameters, which
|
||||
* can then be placed in the `controlSetRows` section
|
||||
* of the `Query` section instead of a predefined control.
|
||||
*
|
||||
* import { validateNonEmpty } from '@superset-ui/core';
|
||||
* import {
|
||||
* sharedControls,
|
||||
* ControlConfig,
|
||||
* ControlPanelConfig,
|
||||
* } from '@superset-ui/chart-controls';
|
||||
*
|
||||
* const myControl: ControlConfig<'SelectControl'> = {
|
||||
* name: 'secondary_entity',
|
||||
* config: {
|
||||
* ...sharedControls.entity,
|
||||
* type: 'SelectControl',
|
||||
* label: t('Secondary Entity'),
|
||||
* mapStateToProps: state => ({
|
||||
* sharedControls.columnChoices(state.datasource)
|
||||
* .columns.filter(c => c.groupby)
|
||||
* })
|
||||
* validators: [validateNonEmpty],
|
||||
* },
|
||||
* }
|
||||
*
|
||||
* In addition to the basic drop down control, there are several predefined
|
||||
* control types (can be set via the `type` property) that can be used. Some
|
||||
* commonly used examples:
|
||||
* - SelectControl: Dropdown to select single or multiple values,
|
||||
usually columns
|
||||
* - MetricsControl: Dropdown to select metrics, triggering a modal
|
||||
to define Metric details
|
||||
* - AdhocFilterControl: Control to choose filters
|
||||
* - CheckboxControl: A checkbox for choosing true/false values
|
||||
* - SliderControl: A slider with min/max values
|
||||
* - TextControl: Control for text data
|
||||
*
|
||||
* For more control input types, check out the `incubator-superset` repo
|
||||
* and open this file: superset-frontend/src/explore/components/controls/index.js
|
||||
*
|
||||
* To ensure all controls have been filled out correctly, the following
|
||||
* validators are provided
|
||||
* by the `@superset-ui/core/lib/validator`:
|
||||
* - validateNonEmpty: must have at least one value
|
||||
* - validateInteger: must be an integer value
|
||||
* - validateNumber: must be an intger or decimal value
|
||||
*/
|
||||
|
||||
// For control input types, see: superset-frontend/src/explore/components/controls/index.js
|
||||
controlPanelSections: [
|
||||
sections.legacyTimeseriesTime,
|
||||
{
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
import React from 'react';
|
||||
import { getQueryMode, isRawMode } from './shared';
|
||||
|
||||
export const allColumns: typeof sharedControls.groupby = {
|
||||
const allColumns: typeof sharedControls.groupby = {
|
||||
type: 'SelectControl',
|
||||
label: t('Columns'),
|
||||
description: t('Columns to display'),
|
||||
|
@ -52,6 +52,7 @@ export const allColumns: typeof sharedControls.groupby = {
|
|||
: [],
|
||||
}),
|
||||
visibility: isRawMode,
|
||||
resetOnHide: false,
|
||||
};
|
||||
|
||||
const dndAllColumns: typeof sharedControls.groupby = {
|
||||
|
@ -75,6 +76,7 @@ const dndAllColumns: typeof sharedControls.groupby = {
|
|||
return newState;
|
||||
},
|
||||
visibility: isRawMode,
|
||||
resetOnHide: false,
|
||||
};
|
||||
|
||||
export const allColumnsControlSetItem: ControlSetItem = {
|
||||
|
|
|
@ -28,6 +28,7 @@ export const groupByControlSetItem: ControlSetItem = {
|
|||
name: 'groupby',
|
||||
override: {
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
mapStateToProps: (state: ControlPanelState, controlState: ControlState) => {
|
||||
const { controls } = state;
|
||||
const originalMapStateToProps = sharedControls?.groupby?.mapStateToProps;
|
||||
|
@ -37,7 +38,6 @@ export const groupByControlSetItem: ControlSetItem = {
|
|||
controls.percent_metrics?.value,
|
||||
controlState.value,
|
||||
]);
|
||||
|
||||
return newState;
|
||||
},
|
||||
rerender: ['metrics', 'percent_metrics'],
|
||||
|
|
|
@ -25,6 +25,7 @@ import { t, validateNonEmpty } from '@superset-ui/core';
|
|||
import React from 'react';
|
||||
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
|
||||
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
|
||||
import { debounceFunc } from '../../consts';
|
||||
|
||||
interface HandlebarsCustomControlProps {
|
||||
value: string;
|
||||
|
@ -37,9 +38,6 @@ const HandlebarsTemplateControl = (
|
|||
props?.value ? props?.value : props?.default ? props?.default : '',
|
||||
);
|
||||
|
||||
const updateConfig = (source: string) => {
|
||||
props.onChange(source);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader>{props.label}</ControlHeader>
|
||||
|
@ -47,7 +45,7 @@ const HandlebarsTemplateControl = (
|
|||
theme="dark"
|
||||
value={val}
|
||||
onChange={source => {
|
||||
updateConfig(source || '');
|
||||
debounceFunc(props.onChange, source || '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -61,11 +59,11 @@ export const handlebarsTemplateControlSetItem: ControlSetItem = {
|
|||
type: HandlebarsTemplateControl,
|
||||
label: t('Handlebars Template'),
|
||||
description: t('A handlebars template that is applied to the data'),
|
||||
default: `<ul class="data_list">
|
||||
{{#each data}}
|
||||
<li>{{this}}</li>
|
||||
{{/each}}
|
||||
</ul>`,
|
||||
default: `<ul class="data-list">
|
||||
{{#each data}}
|
||||
<li>{{stringify this}}</li>
|
||||
{{/each}}
|
||||
</ul>`,
|
||||
isInt: false,
|
||||
renderTrigger: true,
|
||||
|
||||
|
|
|
@ -30,5 +30,6 @@ export const includeTimeControlSetItem: ControlSetItem = {
|
|||
),
|
||||
default: false,
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -34,5 +34,6 @@ export const timeSeriesLimitMetricControlSetItem: ControlSetItem = {
|
|||
name: 'timeseries_limit_metric',
|
||||
override: {
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@ const percentMetrics: typeof sharedControls.metrics = {
|
|||
),
|
||||
multi: true,
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
mapStateToProps: ({ datasource, controls }, controlState) => ({
|
||||
columns: datasource?.columns || [],
|
||||
savedMetrics: datasource?.metrics || [],
|
||||
|
@ -86,6 +87,7 @@ export const metricsControlSetItem: ControlSetItem = {
|
|||
]),
|
||||
}),
|
||||
rerender: ['groupby', 'percent_metrics'],
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -99,5 +101,6 @@ export const showTotalsControlSetItem: ControlSetItem = {
|
|||
'Show total aggregations of selected metrics. Note that row limit does not apply to the result.',
|
||||
),
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -32,6 +32,7 @@ export const orderByControlSetItem: ControlSetItem = {
|
|||
choices: datasource?.order_by_choices || [],
|
||||
}),
|
||||
visibility: isRawMode,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -43,5 +44,6 @@ export const orderDescendingControlSetItem: ControlSetItem = {
|
|||
default: true,
|
||||
description: t('Whether to sort descending or ascending'),
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -25,6 +25,7 @@ import { t } from '@superset-ui/core';
|
|||
import React from 'react';
|
||||
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
|
||||
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
|
||||
import { debounceFunc } from '../../consts';
|
||||
|
||||
interface StyleCustomControlProps {
|
||||
value: string;
|
||||
|
@ -35,9 +36,6 @@ const StyleControl = (props: CustomControlConfig<StyleCustomControlProps>) => {
|
|||
props?.value ? props?.value : props?.default ? props?.default : '',
|
||||
);
|
||||
|
||||
const updateConfig = (source: string) => {
|
||||
props.onChange(source);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader>{props.label}</ControlHeader>
|
||||
|
@ -46,7 +44,7 @@ const StyleControl = (props: CustomControlConfig<StyleCustomControlProps>) => {
|
|||
mode="css"
|
||||
value={val}
|
||||
onChange={source => {
|
||||
updateConfig(source || '');
|
||||
debounceFunc(props.onChange, source || '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -60,7 +58,11 @@ export const styleControlSetItem: ControlSetItem = {
|
|||
type: StyleControl,
|
||||
label: t('CSS Styles'),
|
||||
description: t('CSS applied to the chart'),
|
||||
default: '',
|
||||
default: `/*
|
||||
.data-list {
|
||||
background-color: yellow;
|
||||
}
|
||||
*/`,
|
||||
isInt: false,
|
||||
renderTrigger: true,
|
||||
|
||||
|
|
|
@ -19,49 +19,13 @@
|
|||
import { ChartProps, TimeseriesDataRecord } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
/**
|
||||
* This function is called after a successful response has been
|
||||
* received from the chart data endpoint, and is used to transform
|
||||
* the incoming data prior to being sent to the Visualization.
|
||||
*
|
||||
* The transformProps function is also quite useful to return
|
||||
* additional/modified props to your data viz component. The formData
|
||||
* can also be accessed from your Handlebars.tsx file, but
|
||||
* doing supplying custom props here is often handy for integrating third
|
||||
* party libraries that rely on specific props.
|
||||
*
|
||||
* A description of properties in `chartProps`:
|
||||
* - `height`, `width`: the height/width of the DOM element in which
|
||||
* the chart is located
|
||||
* - `formData`: the chart data request payload that was sent to the
|
||||
* backend.
|
||||
* - `queriesData`: the chart data response payload that was received
|
||||
* from the backend. Some notable properties of `queriesData`:
|
||||
* - `data`: an array with data, each row with an object mapping
|
||||
* the column/alias to its value. Example:
|
||||
* `[{ col1: 'abc', metric1: 10 }, { col1: 'xyz', metric1: 20 }]`
|
||||
* - `rowcount`: the number of rows in `data`
|
||||
* - `query`: the query that was issued.
|
||||
*
|
||||
* Please note: the transformProps function gets cached when the
|
||||
* application loads. When making changes to the `transformProps`
|
||||
* function during development with hot reloading, changes won't
|
||||
* be seen until restarting the development server.
|
||||
*/
|
||||
const { width, height, formData, queriesData } = chartProps;
|
||||
const data = queriesData[0].data as TimeseriesDataRecord[];
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
|
||||
data: data.map(item => ({
|
||||
...item,
|
||||
// convert epoch to native Date
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
__timestamp: new Date(item.__timestamp as number),
|
||||
})),
|
||||
// and now your control data, manipulated as needed, and passed through as props!
|
||||
data,
|
||||
formData,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,15 +31,12 @@ describe('Handlebars tranformProps', () => {
|
|||
height: 500,
|
||||
viz_type: 'handlebars',
|
||||
};
|
||||
const data = [{ name: 'Hulk', sum__num: 1, __timestamp: 599616000000 }];
|
||||
const chartProps = new ChartProps<QueryFormData>({
|
||||
formData,
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData: [
|
||||
{
|
||||
data: [{ name: 'Hulk', sum__num: 1, __timestamp: 599616000000 }],
|
||||
},
|
||||
],
|
||||
queriesData: [{ data }],
|
||||
});
|
||||
|
||||
it('should tranform chart props for viz', () => {
|
||||
|
@ -47,9 +44,7 @@ describe('Handlebars tranformProps', () => {
|
|||
expect.objectContaining({
|
||||
width: 800,
|
||||
height: 600,
|
||||
data: [
|
||||
{ name: 'Hulk', sum__num: 1, __timestamp: new Date(599616000000) },
|
||||
],
|
||||
data,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue