diff --git a/superset-frontend/plugins/plugin-chart-handlebars/package.json b/superset-frontend/plugins/plugin-chart-handlebars/package.json index c83be8bfdd86..da88ada90bae 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/package.json +++ b/superset-frontend/plugins/plugin-chart-handlebars/package.json @@ -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" } diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/Handlebars.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/Handlebars.tsx index c14e925056be..c219e4256e48 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/Handlebars.tsx +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/Handlebars.tsx @@ -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
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` 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 ? `` @@ -58,13 +41,6 @@ export default function Handlebars(props: HandlebarsProps) { const rootElem = createRef(); - // 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 ( diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/components/Handlebars/HandlebarsViewer.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/components/Handlebars/HandlebarsViewer.tsx index 6b3a69b0c731..67ddb83439ff 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/components/Handlebars/HandlebarsViewer.tsx +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/components/Handlebars/HandlebarsViewer.tsx @@ -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); +}); diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts index e6b215ede3e6..562f654431b5 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts @@ -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([ 100, 200, ]); + +export const debounceFunc = debounce( + (func: (val: string) => void, source: string) => func(source), + SLOW_DEBOUNCE, +); diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx index 32b3a55a79fa..da0ba7d589b1 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx @@ -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, { diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx index 0582bfc23f9b..fd24bb75fbb2 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx @@ -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 = { diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/groupBy.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/groupBy.tsx index 0df08bc1d46c..e3bea44b64c9 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/groupBy.tsx +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/groupBy.tsx @@ -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'], diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx index 4d86cdc928fe..efe027b86fb3 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx @@ -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 (
{props.label} @@ -47,7 +45,7 @@ const HandlebarsTemplateControl = ( theme="dark" value={val} onChange={source => { - updateConfig(source || ''); + debounceFunc(props.onChange, source || ''); }} />
@@ -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: `
    - {{#each data}} -
  • {{this}}
  • - {{/each}} -
`, + default: `
    + {{#each data}} +
  • {{stringify this}}
  • + {{/each}} +
`, isInt: false, renderTrigger: true, diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/includeTime.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/includeTime.ts index 7004f45fe3be..9525cc1acf2d 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/includeTime.ts +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/includeTime.ts @@ -30,5 +30,6 @@ export const includeTimeControlSetItem: ControlSetItem = { ), default: false, visibility: isAggMode, + resetOnHide: false, }, }; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts index 701dc27aae1f..2c28d92742b3 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts @@ -34,5 +34,6 @@ export const timeSeriesLimitMetricControlSetItem: ControlSetItem = { name: 'timeseries_limit_metric', override: { visibility: isAggMode, + resetOnHide: false, }, }; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx index 88777c9c3173..7df35e6a668c 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx @@ -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, }, }; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx index 728934d71910..b7c8f8e2406b 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx @@ -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, }, }; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx index 4d6f259eeb50..d3776e77827d 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx @@ -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) => { props?.value ? props?.value : props?.default ? props?.default : '', ); - const updateConfig = (source: string) => { - props.onChange(source); - }; return (
{props.label} @@ -46,7 +44,7 @@ const StyleControl = (props: CustomControlConfig) => { mode="css" value={val} onChange={source => { - updateConfig(source || ''); + debounceFunc(props.onChange, source || ''); }} />
@@ -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, diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/transformProps.ts index cb83e112d863..fe0e5329a7cc 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/transformProps.ts @@ -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, }; } diff --git a/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts index 24aa3c3745a2..f9bab5a91a5b 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts @@ -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({ 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, }), ); });