diff --git a/plugins/plugin-chart-handlebars/README.md b/plugins/plugin-chart-handlebars/README.md
new file mode 100644
index 0000000000..f6beff307c
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/README.md
@@ -0,0 +1,55 @@
+## @superset-ui/plugin-chart-handlebars
+
+[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-handlebars.svg?style=flat-square)](https://www.npmjs.com/package/@superset-ui/plugin-chart-handlebars)
+
+This plugin provides Write a handlebars template to render the data for Superset.
+
+### Usage
+
+Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to
+lookup this chart throughout the app.
+
+```js
+import HandlebarsChartPlugin from '@superset-ui/plugin-chart-handlebars';
+
+new HandlebarsChartPlugin().configure({ key: 'handlebars' }).register();
+```
+
+Then use it via `SuperChart`. See
+[storybook](https://apache-superset.github.io/superset-ui/?selectedKind=plugin-chart-handlebars) for
+more details.
+
+```js
+
+```
+
+### File structure generated
+
+```
+├── package.json
+├── README.md
+├── tsconfig.json
+├── src
+│ ├── Handlebars.tsx
+│ ├── images
+│ │ └── thumbnail.png
+│ ├── index.ts
+│ ├── plugin
+│ │ ├── buildQuery.ts
+│ │ ├── controlPanel.ts
+│ │ ├── index.ts
+│ │ └── transformProps.ts
+│ └── types.ts
+├── test
+│ └── index.test.ts
+└── types
+ └── external.d.ts
+```
diff --git a/plugins/plugin-chart-handlebars/package.json b/plugins/plugin-chart-handlebars/package.json
new file mode 100644
index 0000000000..939f170229
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@superset-ui/plugin-chart-handlebars",
+ "version": "0.0.0",
+ "description": "Superset Chart - Write a handlebars template to render the data",
+ "sideEffects": false,
+ "main": "lib/index.js",
+ "module": "esm/index.js",
+ "files": [
+ "esm",
+ "lib"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/apache-superset/superset-ui.git"
+ },
+ "keywords": [
+ "superset"
+ ],
+ "author": "Superset",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/apache-superset/superset-ui/issues"
+ },
+ "homepage": "https://github.com/apache-superset/superset-ui#readme",
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@superset-ui/chart-controls": "0.18.22",
+ "@superset-ui/core": "0.18.22",
+ "ace-builds": "^1.4.13",
+ "emotion": "^11.0.0",
+ "handlebars": "^4.7.7",
+ "react-ace": "^9.4.4"
+ },
+ "peerDependencies": {
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
+ },
+ "devDependencies": {
+ "@types/jest": "^26.0.0",
+ "jest": "^26.0.1"
+ }
+}
diff --git a/plugins/plugin-chart-handlebars/src/Handlebars.tsx b/plugins/plugin-chart-handlebars/src/Handlebars.tsx
new file mode 100644
index 0000000000..b517cb41a2
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/Handlebars.tsx
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { styled } from '@superset-ui/core';
+import React, { createRef, useEffect } 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;
+`;
+
+/**
+ * ******************* 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
+ ? ``
+ : '';
+ const handlebarTemplateSource = formData.handlebarsTemplate
+ ? formData.handlebarsTemplate
+ : '{{data}}';
+ const templateSource = `${handlebarTemplateSource}\n${styleTemplateSource} `;
+
+ 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 (
+
+ {props.headerText}
+
+
+ );
+}
diff --git a/plugins/plugin-chart-handlebars/src/components/CodeEditor/CodeEditor.tsx b/plugins/plugin-chart-handlebars/src/components/CodeEditor/CodeEditor.tsx
new file mode 100644
index 0000000000..adcab298d8
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/components/CodeEditor/CodeEditor.tsx
@@ -0,0 +1,61 @@
+import React, { FC } from 'react';
+import AceEditor, { IAceEditorProps } from 'react-ace';
+
+// must go after AceEditor import
+import 'ace-builds/src-min-noconflict/mode-handlebars';
+import 'ace-builds/src-min-noconflict/mode-css';
+import 'ace-builds/src-noconflict/theme-github';
+import 'ace-builds/src-noconflict/theme-monokai';
+
+export type CodeEditorMode = 'handlebars' | 'css';
+export type CodeEditorTheme = 'light' | 'dark';
+
+export interface CodeEditorProps extends IAceEditorProps {
+ mode?: CodeEditorMode;
+ theme?: CodeEditorTheme;
+ name?: string;
+}
+
+export const CodeEditor: FC = ({
+ mode,
+ theme,
+ name,
+ width,
+ height,
+ value,
+ ...rest
+}: CodeEditorProps) => {
+ const m_name = name || Math.random().toString(36).substring(7);
+ const m_theme = theme === 'light' ? 'github' : 'monokai';
+ const m_mode = mode || 'handlebars';
+ const m_height = height || '300px';
+ const m_width = width || '100%';
+
+ return (
+
+ );
+};
diff --git a/plugins/plugin-chart-handlebars/src/components/ControlHeader/controlHeader.tsx b/plugins/plugin-chart-handlebars/src/components/ControlHeader/controlHeader.tsx
new file mode 100644
index 0000000000..5297e0cdf0
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/components/ControlHeader/controlHeader.tsx
@@ -0,0 +1,15 @@
+import React, { ReactNode } from 'react';
+
+interface ControlHeaderProps {
+ children: ReactNode;
+}
+
+export const ControlHeader = ({
+ children,
+}: ControlHeaderProps): JSX.Element => (
+
+);
diff --git a/plugins/plugin-chart-handlebars/src/components/Handlebars/HandlebarsViewer.tsx b/plugins/plugin-chart-handlebars/src/components/Handlebars/HandlebarsViewer.tsx
new file mode 100644
index 0000000000..ea6d413fd6
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/components/Handlebars/HandlebarsViewer.tsx
@@ -0,0 +1,26 @@
+import { SafeMarkdown } from '@superset-ui/core';
+import Handlebars from 'handlebars';
+import React, { useMemo, useState } from 'react';
+
+export interface HandlebarsViewerProps {
+ templateSource: string;
+ data: any;
+}
+
+export const HandlebarsViewer = ({
+ templateSource,
+ data,
+}: HandlebarsViewerProps) => {
+ const [renderedTemplate, setRenderedTemplate] = useState('');
+
+ useMemo(() => {
+ const template = Handlebars.compile(templateSource);
+ const result = template(data);
+ setRenderedTemplate(result);
+ }, [templateSource, data]);
+
+ if (renderedTemplate) {
+ return ;
+ }
+ return Loading...
;
+};
diff --git a/plugins/plugin-chart-handlebars/src/consts.ts b/plugins/plugin-chart-handlebars/src/consts.ts
new file mode 100644
index 0000000000..e6b215ede3
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/consts.ts
@@ -0,0 +1,37 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { formatSelectOptions } from '@superset-ui/chart-controls';
+import { addLocaleData, t } from '@superset-ui/core';
+import i18n from './i18n';
+
+addLocaleData(i18n);
+
+export const PAGE_SIZE_OPTIONS = formatSelectOptions([
+ [0, t('page_size.all')],
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 10,
+ 20,
+ 50,
+ 100,
+ 200,
+]);
diff --git a/plugins/plugin-chart-handlebars/src/i18n.ts b/plugins/plugin-chart-handlebars/src/i18n.ts
new file mode 100644
index 0000000000..e8e1657f1d
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/i18n.ts
@@ -0,0 +1,47 @@
+import { Locale } from '@superset-ui/core';
+
+const en = {
+ 'Query Mode': [''],
+ Aggregate: [''],
+ 'Raw Records': [''],
+ 'Emit Filter Events': [''],
+ 'Show Cell Bars': [''],
+ 'page_size.show': ['Show'],
+ 'page_size.all': ['All'],
+ 'page_size.entries': ['entries'],
+ 'table.previous_page': ['Previous'],
+ 'table.next_page': ['Next'],
+ 'search.num_records': ['%s record', '%s records...'],
+};
+
+const translations: Partial> = {
+ en,
+ fr: {
+ 'Query Mode': [''],
+ Aggregate: [''],
+ 'Raw Records': [''],
+ 'Emit Filter Events': [''],
+ 'Show Cell Bars': [''],
+ 'page_size.show': ['Afficher'],
+ 'page_size.all': ['tous'],
+ 'page_size.entries': ['entrées'],
+ 'table.previous_page': ['Précédent'],
+ 'table.next_page': ['Suivante'],
+ 'search.num_records': ['%s enregistrement', '%s enregistrements...'],
+ },
+ zh: {
+ 'Query Mode': ['查询模式'],
+ Aggregate: ['分组聚合'],
+ 'Raw Records': ['原始数据'],
+ 'Emit Filter Events': ['关联看板过滤器'],
+ 'Show Cell Bars': ['为指标添加条状图背景'],
+ 'page_size.show': ['每页显示'],
+ 'page_size.all': ['全部'],
+ 'page_size.entries': ['条'],
+ 'table.previous_page': ['上一页'],
+ 'table.next_page': ['下一页'],
+ 'search.num_records': ['%s条记录...'],
+ },
+};
+
+export default translations;
diff --git a/plugins/plugin-chart-handlebars/src/images/thumbnail.png b/plugins/plugin-chart-handlebars/src/images/thumbnail.png
new file mode 100644
index 0000000000..342bc23206
Binary files /dev/null and b/plugins/plugin-chart-handlebars/src/images/thumbnail.png differ
diff --git a/plugins/plugin-chart-handlebars/src/index.ts b/plugins/plugin-chart-handlebars/src/index.ts
new file mode 100644
index 0000000000..c39fe12b95
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/index.ts
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+// eslint-disable-next-line import/prefer-default-export
+export { default as HandlebarsChartPlugin } from './plugin';
+/**
+ * Note: this file exports the default export from Handlebars.tsx.
+ * If you want to export multiple visualization modules, you will need to
+ * either add additional plugin folders (similar in structure to ./plugin)
+ * OR export multiple instances of `ChartPlugin` extensions in ./plugin/index.ts
+ * which in turn load exports from Handlebars.tsx
+ */
diff --git a/plugins/plugin-chart-handlebars/src/plugin/buildQuery.ts b/plugins/plugin-chart-handlebars/src/plugin/buildQuery.ts
new file mode 100644
index 0000000000..43ab986d30
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/buildQuery.ts
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { buildQueryContext, QueryFormData } from '@superset-ui/core';
+
+export default function buildQuery(formData: QueryFormData) {
+ const { metric, sort_by_metric } = formData;
+
+ return buildQueryContext(formData, baseQueryObject => [
+ {
+ ...baseQueryObject,
+ ...(sort_by_metric && { orderby: [[metric, false]] }),
+ },
+ ]);
+}
diff --git a/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx b/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx
new file mode 100644
index 0000000000..16511cb34c
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx
@@ -0,0 +1,158 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+ ControlPanelConfig,
+ emitFilterControl,
+ sections,
+} from '@superset-ui/chart-controls';
+import { addLocaleData, t } from '@superset-ui/core';
+import i18n from '../i18n';
+import { AllColumnsControlSetItem } from './controls/columns';
+import { GroupByControlSetItem } from './controls/groupBy';
+import { HandlbarsTemplateControlSetItem } from './controls/handlebarTemplate';
+import { IncludeTimeControlSetItem } from './controls/includeTime';
+import {
+ RowLimitControlSetItem,
+ TimeSeriesLimitMetricControlSetItem,
+} from './controls/limits';
+import {
+ MetricsControlSetItem,
+ PercentMetricsControlSetItem,
+ ShowTotalsControlSetItem,
+} from './controls/metrics';
+import {
+ OrderByControlSetItem,
+ OrderDescendingControlSetItem,
+} from './controls/orderBy';
+import {
+ ServerPageLengthControlSetItem,
+ ServerPaginationControlSetRow,
+} from './controls/pagination';
+import { QueryModeControlSetItem } from './controls/queryMode';
+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,
+ {
+ label: t('Query'),
+ expanded: true,
+ controlSetRows: [
+ [QueryModeControlSetItem],
+ [GroupByControlSetItem],
+ [MetricsControlSetItem, AllColumnsControlSetItem],
+ [PercentMetricsControlSetItem],
+ [TimeSeriesLimitMetricControlSetItem, OrderByControlSetItem],
+ ServerPaginationControlSetRow,
+ [RowLimitControlSetItem, ServerPageLengthControlSetItem],
+ [IncludeTimeControlSetItem, OrderDescendingControlSetItem],
+ [ShowTotalsControlSetItem],
+ ['adhoc_filters'],
+ emitFilterControl,
+ ],
+ },
+ {
+ label: t('Options'),
+ expanded: true,
+ controlSetRows: [
+ [HandlbarsTemplateControlSetItem],
+ [StyleControlSetItem],
+ ],
+ },
+ ],
+};
+
+export default config;
diff --git a/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx b/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx
new file mode 100644
index 0000000000..60dc573ddb
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx
@@ -0,0 +1,67 @@
+import {
+ ColumnOption,
+ ControlSetItem,
+ ExtraControlProps,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import {
+ ensureIsArray,
+ FeatureFlag,
+ isFeatureEnabled,
+ t,
+} from '@superset-ui/core';
+import React from 'react';
+import { getQueryMode, isRawMode } from './shared';
+
+export const all_columns: typeof sharedControls.groupby = {
+ type: 'SelectControl',
+ label: t('Columns'),
+ description: t('Columns to display'),
+ multi: true,
+ freeForm: true,
+ allowAll: true,
+ commaChoosesOption: false,
+ default: [],
+ optionRenderer: c => ,
+ valueRenderer: c => ,
+ valueKey: 'column_name',
+ mapStateToProps: ({ datasource, controls }, controlState) => ({
+ options: datasource?.columns || [],
+ queryMode: getQueryMode(controls),
+ externalValidationErrors:
+ isRawMode({ controls }) && ensureIsArray(controlState.value).length === 0
+ ? [t('must have a value')]
+ : [],
+ }),
+ visibility: isRawMode,
+};
+
+export const dnd_all_columns: typeof sharedControls.groupby = {
+ type: 'DndColumnSelect',
+ label: t('Columns'),
+ description: t('Columns to display'),
+ default: [],
+ mapStateToProps({ datasource, controls }, controlState) {
+ const newState: ExtraControlProps = {};
+ if (datasource) {
+ const options = datasource.columns;
+ newState.options = Object.fromEntries(
+ options.map(option => [option.column_name, option]),
+ );
+ }
+ newState.queryMode = getQueryMode(controls);
+ newState.externalValidationErrors =
+ isRawMode({ controls }) && ensureIsArray(controlState.value).length === 0
+ ? [t('must have a value')]
+ : [];
+ return newState;
+ },
+ visibility: isRawMode,
+};
+
+export const AllColumnsControlSetItem: ControlSetItem = {
+ name: 'all_columns',
+ config: isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP)
+ ? dnd_all_columns
+ : all_columns,
+};
diff --git a/plugins/plugin-chart-handlebars/src/plugin/controls/groupBy.tsx b/plugins/plugin-chart-handlebars/src/plugin/controls/groupBy.tsx
new file mode 100644
index 0000000000..5e27f911b7
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/controls/groupBy.tsx
@@ -0,0 +1,27 @@
+import {
+ ControlPanelState,
+ ControlSetItem,
+ ControlState,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import { isAggMode, validateAggControlValues } from './shared';
+
+export const GroupByControlSetItem: ControlSetItem = {
+ name: 'groupby',
+ override: {
+ visibility: isAggMode,
+ mapStateToProps: (state: ControlPanelState, controlState: ControlState) => {
+ const { controls } = state;
+ const originalMapStateToProps = sharedControls?.groupby?.mapStateToProps;
+ const newState = originalMapStateToProps?.(state, controlState) ?? {};
+ newState.externalValidationErrors = validateAggControlValues(controls, [
+ controls.metrics?.value,
+ controls.percent_metrics?.value,
+ controlState.value,
+ ]);
+
+ return newState;
+ },
+ rerender: ['metrics', 'percent_metrics'],
+ },
+};
diff --git a/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx b/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx
new file mode 100644
index 0000000000..30bab34c33
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx
@@ -0,0 +1,60 @@
+import {
+ ControlConfig,
+ ControlSetItem,
+ CustomControlConfig,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import { t, validateNonEmpty } from '@superset-ui/core';
+import React from 'react';
+import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
+import { ControlHeader } from '../../components/ControlHeader/controlHeader';
+
+interface HandlebarsCustomControlProps {
+ value: string;
+}
+
+const HandlbarsTemplateControl = (
+ props: CustomControlConfig,
+) => {
+ const val = String(
+ props?.value ? props?.value : props?.default ? props?.default : '',
+ );
+
+ const updateConfig = (source: string) => {
+ props.onChange(source);
+ };
+ return (
+
+ {props.label}
+ {
+ updateConfig(source || '');
+ }}
+ />
+
+ );
+};
+const handlebarsTemplateControlConfig: ControlConfig = {
+ ...sharedControls.entity,
+ type: HandlbarsTemplateControl,
+ label: t('Handlebars Template'),
+ description: t('A handlebars template that is applied to the data'),
+ default: `
+ {{#each data}}
+ - {{this}}
+ {{/each}}
+
`,
+ isInt: false,
+
+ validators: [validateNonEmpty],
+ mapStateToProps: ({ controls }) => ({
+ value: controls?.handlebars_template?.value,
+ }),
+};
+
+export const HandlbarsTemplateControlSetItem: ControlSetItem = {
+ name: 'handlebarsTemplate',
+ config: handlebarsTemplateControlConfig,
+};
diff --git a/plugins/plugin-chart-handlebars/src/plugin/controls/includeTime.ts b/plugins/plugin-chart-handlebars/src/plugin/controls/includeTime.ts
new file mode 100644
index 0000000000..8cbc377dd4
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/controls/includeTime.ts
@@ -0,0 +1,16 @@
+import { ControlSetItem } from '@superset-ui/chart-controls';
+import { t } from '@superset-ui/core';
+import { isAggMode } from './shared';
+
+export const IncludeTimeControlSetItem: ControlSetItem = {
+ name: 'include_time',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Include time'),
+ description: t(
+ 'Whether to include the time granularity as defined in the time section',
+ ),
+ default: false,
+ visibility: isAggMode,
+ },
+};
diff --git a/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts b/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts
new file mode 100644
index 0000000000..718268d097
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts
@@ -0,0 +1,20 @@
+import {
+ ControlPanelsContainerProps,
+ ControlSetItem,
+} from '@superset-ui/chart-controls';
+import { isAggMode } from './shared';
+
+export const RowLimitControlSetItem: ControlSetItem = {
+ name: 'row_limit',
+ override: {
+ visibility: ({ controls }: ControlPanelsContainerProps) =>
+ !controls?.server_pagination?.value,
+ },
+};
+
+export const TimeSeriesLimitMetricControlSetItem: ControlSetItem = {
+ name: 'timeseries_limit_metric',
+ override: {
+ visibility: isAggMode,
+ },
+};
diff --git a/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx b/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx
new file mode 100644
index 0000000000..cd85118641
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx
@@ -0,0 +1,85 @@
+import {
+ ControlPanelState,
+ ControlSetItem,
+ ControlState,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
+import { getQueryMode, isAggMode, validateAggControlValues } from './shared';
+
+const percent_metrics: typeof sharedControls.metrics = {
+ type: 'MetricsControl',
+ label: t('Percentage metrics'),
+ description: t(
+ 'Metrics for which percentage of total are to be displayed. Calculated from only data within the row limit.',
+ ),
+ multi: true,
+ visibility: isAggMode,
+ mapStateToProps: ({ datasource, controls }, controlState) => ({
+ columns: datasource?.columns || [],
+ savedMetrics: datasource?.metrics || [],
+ datasource,
+ datasourceType: datasource?.type,
+ queryMode: getQueryMode(controls),
+ externalValidationErrors: validateAggControlValues(controls, [
+ controls.groupby?.value,
+ controls.metrics?.value,
+ controlState.value,
+ ]),
+ }),
+ rerender: ['groupby', 'metrics'],
+ default: [],
+ validators: [],
+};
+
+const dnd_percent_metrics = {
+ ...percent_metrics,
+ type: 'DndMetricSelect',
+};
+
+export const PercentMetricsControlSetItem: ControlSetItem = {
+ name: 'percent_metrics',
+ config: {
+ ...(isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP)
+ ? dnd_percent_metrics
+ : percent_metrics),
+ },
+};
+
+export const MetricsControlSetItem: ControlSetItem = {
+ name: 'metrics',
+ override: {
+ validators: [],
+ visibility: isAggMode,
+ mapStateToProps: (
+ { controls, datasource, form_data }: ControlPanelState,
+ controlState: ControlState,
+ ) => ({
+ columns: datasource?.columns.filter(c => c.filterable) || [],
+ savedMetrics: datasource?.metrics || [],
+ // current active adhoc metrics
+ selectedMetrics:
+ form_data.metrics || (form_data.metric ? [form_data.metric] : []),
+ datasource,
+ externalValidationErrors: validateAggControlValues(controls, [
+ controls.groupby?.value,
+ controls.percent_metrics?.value,
+ controlState.value,
+ ]),
+ }),
+ rerender: ['groupby', 'percent_metrics'],
+ },
+};
+
+export const ShowTotalsControlSetItem: ControlSetItem = {
+ name: 'show_totals',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Show totals'),
+ default: false,
+ description: t(
+ 'Show total aggregations of selected metrics. Note that row limit does not apply to the result.',
+ ),
+ visibility: isAggMode,
+ },
+};
diff --git a/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx b/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx
new file mode 100644
index 0000000000..70cc27fba0
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx
@@ -0,0 +1,29 @@
+import { ControlSetItem } from '@superset-ui/chart-controls';
+import { t } from '@superset-ui/core';
+import { isAggMode, isRawMode } from './shared';
+
+export const OrderByControlSetItem: ControlSetItem = {
+ name: 'order_by_cols',
+ config: {
+ type: 'SelectControl',
+ label: t('Ordering'),
+ description: t('Order results by selected columns'),
+ multi: true,
+ default: [],
+ mapStateToProps: ({ datasource }) => ({
+ choices: datasource?.order_by_choices || [],
+ }),
+ visibility: isRawMode,
+ },
+};
+
+export const OrderDescendingControlSetItem: ControlSetItem = {
+ name: 'order_desc',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Sort descending'),
+ default: true,
+ description: t('Whether to sort descending or ascending'),
+ visibility: isAggMode,
+ },
+};
diff --git a/plugins/plugin-chart-handlebars/src/plugin/controls/pagination.tsx b/plugins/plugin-chart-handlebars/src/plugin/controls/pagination.tsx
new file mode 100644
index 0000000000..e4db1ba7b4
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/controls/pagination.tsx
@@ -0,0 +1,39 @@
+import {
+ ControlPanelsContainerProps,
+ ControlSetItem,
+ ControlSetRow,
+} from '@superset-ui/chart-controls';
+import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
+import { PAGE_SIZE_OPTIONS } from '../../consts';
+
+export const ServerPaginationControlSetRow: ControlSetRow =
+ isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) ||
+ isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS)
+ ? [
+ {
+ name: 'server_pagination',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Server pagination'),
+ description: t(
+ 'Enable server side pagination of results (experimental feature)',
+ ),
+ default: false,
+ },
+ },
+ ]
+ : [];
+
+export const ServerPageLengthControlSetItem: ControlSetItem = {
+ name: 'server_page_length',
+ config: {
+ type: 'SelectControl',
+ freeForm: true,
+ label: t('Server Page Length'),
+ default: 10,
+ choices: PAGE_SIZE_OPTIONS,
+ description: t('Rows per page, 0 means no pagination'),
+ visibility: ({ controls }: ControlPanelsContainerProps) =>
+ Boolean(controls?.server_pagination?.value),
+ },
+};
diff --git a/plugins/plugin-chart-handlebars/src/plugin/controls/queryMode.tsx b/plugins/plugin-chart-handlebars/src/plugin/controls/queryMode.tsx
new file mode 100644
index 0000000000..0646195a34
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/controls/queryMode.tsx
@@ -0,0 +1,24 @@
+import {
+ ControlConfig,
+ ControlSetItem,
+ QueryModeLabel,
+} from '@superset-ui/chart-controls';
+import { QueryMode, t } from '@superset-ui/core';
+import { getQueryMode } from './shared';
+
+const queryMode: ControlConfig<'RadioButtonControl'> = {
+ type: 'RadioButtonControl',
+ label: t('Query mode'),
+ default: null,
+ options: [
+ [QueryMode.aggregate, QueryModeLabel[QueryMode.aggregate]],
+ [QueryMode.raw, QueryModeLabel[QueryMode.raw]],
+ ],
+ mapStateToProps: ({ controls }) => ({ value: getQueryMode(controls) }),
+ rerender: ['all_columns', 'groupby', 'metrics', 'percent_metrics'],
+};
+
+export const QueryModeControlSetItem: ControlSetItem = {
+ name: 'query_mode',
+ config: queryMode,
+};
diff --git a/plugins/plugin-chart-handlebars/src/plugin/controls/shared.ts b/plugins/plugin-chart-handlebars/src/plugin/controls/shared.ts
new file mode 100644
index 0000000000..f4b1ae6879
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/controls/shared.ts
@@ -0,0 +1,43 @@
+import {
+ ControlPanelsContainerProps,
+ ControlStateMapping,
+} from '@superset-ui/chart-controls';
+import {
+ ensureIsArray,
+ QueryFormColumn,
+ QueryMode,
+ t,
+} from '@superset-ui/core';
+
+export function getQueryMode(controls: ControlStateMapping): QueryMode {
+ const mode = controls?.query_mode?.value;
+ if (mode === QueryMode.aggregate || mode === QueryMode.raw) {
+ return mode as QueryMode;
+ }
+ const rawColumns = controls?.all_columns?.value as
+ | QueryFormColumn[]
+ | undefined;
+ const hasRawColumns = rawColumns && rawColumns.length > 0;
+ return hasRawColumns ? QueryMode.raw : QueryMode.aggregate;
+}
+
+/**
+ * Visibility check
+ */
+export function isQueryMode(mode: QueryMode) {
+ return ({ controls }: Pick) =>
+ getQueryMode(controls) === mode;
+}
+
+export const isAggMode = isQueryMode(QueryMode.aggregate);
+export const isRawMode = isQueryMode(QueryMode.raw);
+
+export const validateAggControlValues = (
+ controls: ControlStateMapping,
+ values: any[],
+) => {
+ const areControlsEmpty = values.every(val => ensureIsArray(val).length === 0);
+ return areControlsEmpty && isAggMode({ controls })
+ ? [t('Group By, Metrics or Percentage Metrics must have a value')]
+ : [];
+};
diff --git a/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx b/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx
new file mode 100644
index 0000000000..c460f30fb9
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx
@@ -0,0 +1,55 @@
+import {
+ ControlConfig,
+ ControlSetItem,
+ CustomControlConfig,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import { t } from '@superset-ui/core';
+import React from 'react';
+import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
+import { ControlHeader } from '../../components/ControlHeader/controlHeader';
+
+interface StyleCustomControlProps {
+ value: string;
+}
+
+const StyleControl = (props: CustomControlConfig) => {
+ const val = String(
+ props?.value ? props?.value : props?.default ? props?.default : '',
+ );
+
+ const updateConfig = (source: string) => {
+ props.onChange(source);
+ };
+ return (
+
+ {props.label}
+ {
+ updateConfig(source || '');
+ }}
+ />
+
+ );
+};
+const styleControlConfig: ControlConfig = {
+ ...sharedControls.entity,
+ type: StyleControl,
+ label: t('CSS Styles'),
+ description: t('CSS applied to the chart'),
+ default: '',
+ isInt: false,
+
+ validators: [],
+ mapStateToProps: ({ controls }) => ({
+ value: controls?.handlebars_template?.value,
+ }),
+};
+
+export const StyleControlSetItem: ControlSetItem = {
+ name: 'styleTemplate',
+ config: styleControlConfig,
+};
diff --git a/plugins/plugin-chart-handlebars/src/plugin/index.ts b/plugins/plugin-chart-handlebars/src/plugin/index.ts
new file mode 100644
index 0000000000..db5ad528f8
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/index.ts
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
+import thumbnail from '../images/thumbnail.png';
+import buildQuery from './buildQuery';
+import controlPanel from './controlPanel';
+import transformProps from './transformProps';
+
+export default class HandlebarsChartPlugin extends ChartPlugin {
+ /**
+ * The constructor is used to pass relevant metadata and callbacks that get
+ * registered in respective registries that are used throughout the library
+ * and application. A more thorough description of each property is given in
+ * the respective imported file.
+ *
+ * It is worth noting that `buildQuery` and is optional, and only needed for
+ * advanced visualizations that require either post processing operations
+ * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries.
+ */
+ constructor() {
+ const metadata = new ChartMetadata({
+ description: 'Write a handlebars template to render the data',
+ name: t('Handlebars'),
+ thumbnail,
+ });
+
+ super({
+ buildQuery,
+ controlPanel,
+ loadChart: () => import('../Handlebars'),
+ metadata,
+ transformProps,
+ });
+ }
+}
diff --git a/plugins/plugin-chart-handlebars/src/plugin/transformProps.ts b/plugins/plugin-chart-handlebars/src/plugin/transformProps.ts
new file mode 100644
index 0000000000..cb83e112d8
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/plugin/transformProps.ts
@@ -0,0 +1,67 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+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!
+ formData,
+ };
+}
diff --git a/plugins/plugin-chart-handlebars/src/types.ts b/plugins/plugin-chart-handlebars/src/types.ts
new file mode 100644
index 0000000000..bfe64fab40
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/src/types.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { ColumnConfig } from '@superset-ui/chart-controls';
+import {
+ QueryFormData,
+ QueryFormMetric,
+ QueryMode,
+ TimeGranularity,
+ TimeseriesDataRecord,
+} from '@superset-ui/core';
+
+export interface HandlebarsStylesProps {
+ height: number;
+ width: number;
+}
+
+interface HandlebarsCustomizeProps {
+ headerText: string;
+ handlebarsTemplate?: string;
+ styleTemplate?: string;
+}
+
+export type HandlebarsQueryFormData = QueryFormData &
+ HandlebarsStylesProps &
+ HandlebarsCustomizeProps & {
+ align_pn?: boolean;
+ color_pn?: boolean;
+ include_time?: boolean;
+ include_search?: boolean;
+ query_mode?: QueryMode;
+ page_length?: string | number | null; // null means auto-paginate
+ metrics?: QueryFormMetric[] | null;
+ percent_metrics?: QueryFormMetric[] | null;
+ timeseries_limit_metric?: QueryFormMetric[] | QueryFormMetric | null;
+ groupby?: QueryFormMetric[] | null;
+ all_columns?: QueryFormMetric[] | null;
+ order_desc?: boolean;
+ table_timestamp_format?: string;
+ emit_filter?: boolean;
+ granularity_sqla?: string;
+ time_grain_sqla?: TimeGranularity;
+ column_config?: Record;
+ };
+
+export type HandlebarsProps = HandlebarsStylesProps &
+ HandlebarsCustomizeProps & {
+ data: TimeseriesDataRecord[];
+ // add typing here for the props you pass in from transformProps.ts!
+ formData: HandlebarsQueryFormData;
+ };
diff --git a/plugins/plugin-chart-handlebars/test/index.test.ts b/plugins/plugin-chart-handlebars/test/index.test.ts
new file mode 100644
index 0000000000..9121daeca4
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/test/index.test.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { HandlebarsChartPlugin } from '../src';
+
+/**
+ * The example tests in this file act as a starting point, and
+ * we encourage you to build more. These tests check that the
+ * plugin loads properly, and focus on `transformProps`
+ * to ake sure that data, controls, and props are all
+ * treated correctly (e.g. formData from plugin controls
+ * properly transform the data and/or any resulting props).
+ */
+describe('@superset-ui/plugin-chart-handlebars', () => {
+ it('exists', () => {
+ expect(HandlebarsChartPlugin).toBeDefined();
+ });
+});
diff --git a/plugins/plugin-chart-handlebars/test/plugin/buildQuery.test.ts b/plugins/plugin-chart-handlebars/test/plugin/buildQuery.test.ts
new file mode 100644
index 0000000000..33ca12b66d
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/test/plugin/buildQuery.test.ts
@@ -0,0 +1,16 @@
+import buildQuery from '../../src/plugin/buildQuery';
+
+describe('Handlebars buildQuery', () => {
+ const formData = {
+ datasource: '5__table',
+ granularity_sqla: 'ds',
+ series: 'foo',
+ viz_type: 'my_chart',
+ };
+
+ it('should build groupby with series in form data', () => {
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+ expect(query.groupby).toEqual(['foo']);
+ });
+});
diff --git a/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts b/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts
new file mode 100644
index 0000000000..cf6d6cc111
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts
@@ -0,0 +1,38 @@
+import { ChartProps } from '@superset-ui/core';
+import transformProps from '../../src/plugin/transformProps';
+
+describe('Handlebars tranformProps', () => {
+ const formData = {
+ colorScheme: 'bnbColors',
+ datasource: '3__table',
+ granularity_sqla: 'ds',
+ metric: 'sum__num',
+ series: 'name',
+ boldText: true,
+ headerFontSize: 'xs',
+ headerText: 'my text',
+ };
+ const chartProps = new ChartProps({
+ formData,
+ width: 800,
+ height: 600,
+ queriesData: [
+ {
+ data: [{ name: 'Hulk', sum__num: 1, __timestamp: 599616000000 }],
+ },
+ ],
+ });
+
+ it('should tranform chart props for viz', () => {
+ expect(transformProps(chartProps)).toEqual({
+ width: 800,
+ height: 600,
+ boldText: true,
+ headerFontSize: 'xs',
+ headerText: 'my text',
+ data: [
+ { name: 'Hulk', sum__num: 1, __timestamp: new Date(599616000000) },
+ ],
+ });
+ });
+});
diff --git a/plugins/plugin-chart-handlebars/tsconfig.json b/plugins/plugin-chart-handlebars/tsconfig.json
new file mode 100644
index 0000000000..b6bfaa2d98
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "declarationDir": "lib",
+ "outDir": "lib",
+ "rootDir": "src"
+ },
+ "exclude": [
+ "lib",
+ "test"
+ ],
+ "extends": "../../tsconfig.json",
+ "include": [
+ "src/**/*",
+ "types/**/*",
+ "../../types/**/*"
+ ],
+ "references": [
+ {
+ "path": "../../packages/superset-ui-chart-controls"
+ },
+ {
+ "path": "../../packages/superset-ui-core"
+ }
+ ]
+}
diff --git a/plugins/plugin-chart-handlebars/types/external.d.ts b/plugins/plugin-chart-handlebars/types/external.d.ts
new file mode 100644
index 0000000000..0935dbbd80
--- /dev/null
+++ b/plugins/plugin-chart-handlebars/types/external.d.ts
@@ -0,0 +1,4 @@
+declare module '*.png' {
+ const value: any;
+ export default value;
+}