This is an automated email from the ASF dual-hosted git repository.

villebro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new e632b82395 feat: Adds plugin-chart-handlebars (#17903)
e632b82395 is described below

commit e632b82395bd379e2c4d42cb581972e6fe690a50
Author: Jeremy <[email protected]>
AuthorDate: Tue Apr 26 06:34:28 2022 -0500

    feat: Adds plugin-chart-handlebars (#17903)
    
    * adds: plugin chart handlebars
    
    * adds: handlebars plugin to main presets
    
    * update: npm install
    
    * chore: lint
    
    * adds: dateFormat handlebars helper
    
    * deletes: unused props
    
    * chore: linting plugin-chart-handlebars
    
    * docs: chart-plugin-handlebars
    
    * adds: moment to peer deps
    
    * update: use error handling
    
    * update: inline config, adds renderTrigger
    
    * update: inline config, adds renderTrigger
    
    * camelCase controls
    
    * (plugins-chart-handlebars) adds: missing props
    
    Adds missing propeties in test formData
    
    * (plugin-chart-handlebars) fixes test
    
    * (plugin-handlebars-chart) use numbers for size
    
    * (feature-handlebars-chart) fix viz_type
    
    * (plugin-handlebars-chart) revert
    
    revert the viz_type change. it was in the wrong place.
    
    * fix test and add license headers
    
    Co-authored-by: Ville Brofeldt <[email protected]>
---
 superset-frontend/package-lock.json                |  49 +++++++
 superset-frontend/package.json                     |   1 +
 .../plugins/plugin-chart-handlebars/README.md      |  74 ++++++++++
 .../plugins/plugin-chart-handlebars/package.json   |  45 ++++++
 .../plugin-chart-handlebars/src/Handlebars.tsx     |  73 ++++++++++
 .../src/components/CodeEditor/CodeEditor.tsx       |  80 +++++++++++
 .../src/components/ControlHeader/controlHeader.tsx |  33 +++++
 .../src/components/Handlebars/HandlebarsViewer.tsx |  66 +++++++++
 .../plugins/plugin-chart-handlebars/src/consts.ts  |  37 +++++
 .../plugins/plugin-chart-handlebars/src/i18n.ts    |  65 +++++++++
 .../src/images/thumbnail.png                       | Bin 0 -> 398917 bytes
 .../plugins/plugin-chart-handlebars/src/index.ts   |  27 ++++
 .../src/plugin/buildQuery.ts                       |  31 ++++
 .../src/plugin/controlPanel.tsx                    | 158 +++++++++++++++++++++
 .../src/plugin/controls/columns.tsx                |  85 +++++++++++
 .../src/plugin/controls/groupBy.tsx                |  45 ++++++
 .../src/plugin/controls/handlebarTemplate.tsx      |  77 ++++++++++
 .../src/plugin/controls/includeTime.ts             |  34 +++++
 .../src/plugin/controls/limits.ts                  |  38 +++++
 .../src/plugin/controls/metrics.tsx                | 103 ++++++++++++++
 .../src/plugin/controls/orderBy.tsx                |  47 ++++++
 .../src/plugin/controls/pagination.tsx             |  57 ++++++++
 .../src/plugin/controls/queryMode.tsx              |  42 ++++++
 .../src/plugin/controls/shared.ts                  |  61 ++++++++
 .../src/plugin/controls/style.tsx                  |  72 ++++++++++
 .../plugin-chart-handlebars/src/plugin/index.ts    |  51 +++++++
 .../src/plugin/transformProps.ts                   |  67 +++++++++
 .../plugins/plugin-chart-handlebars/src/types.ts   |  65 +++++++++
 .../plugin-chart-handlebars/test/index.test.ts     |  33 +++++
 .../test/plugin/buildQuery.test.ts                 |  37 +++++
 .../test/plugin/transformProps.test.ts             |  56 ++++++++
 .../plugins/plugin-chart-handlebars/tsconfig.json  |  25 ++++
 .../plugin-chart-handlebars/types/external.d.ts    |  22 +++
 .../src/visualizations/presets/MainPreset.js       |   2 +
 34 files changed, 1758 insertions(+)

diff --git a/superset-frontend/package-lock.json 
b/superset-frontend/package-lock.json
index 046e1536e8..9ca8a5ca29 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -43,6 +43,7 @@
         "@superset-ui/legacy-preset-chart-deckgl": 
"file:./plugins/legacy-preset-chart-deckgl",
         "@superset-ui/legacy-preset-chart-nvd3": 
"file:./plugins/legacy-preset-chart-nvd3",
         "@superset-ui/plugin-chart-echarts": 
"file:./plugins/plugin-chart-echarts",
+        "@superset-ui/plugin-chart-handlebars": 
"file:./plugins/plugin-chart-handlebars",
         "@superset-ui/plugin-chart-pivot-table": 
"file:./plugins/plugin-chart-pivot-table",
         "@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
         "@superset-ui/plugin-chart-word-cloud": 
"file:./plugins/plugin-chart-word-cloud",
@@ -21998,6 +21999,10 @@
       "resolved": "plugins/plugin-chart-echarts",
       "link": true
     },
+    "node_modules/@superset-ui/plugin-chart-handlebars": {
+      "resolved": "plugins/plugin-chart-handlebars",
+      "link": true
+    },
     "node_modules/@superset-ui/plugin-chart-pivot-table": {
       "resolved": "plugins/plugin-chart-pivot-table",
       "link": true
@@ -32492,6 +32497,11 @@
         "node": ">= 4"
       }
     },
+    "node_modules/emotion": {
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/emotion/-/emotion-11.0.0.tgz";,
+      "integrity": 
"sha512-QW3CRqic3aRw1OBOcnvxaHEpCmxtlGwZ5tM9dV5rY3Rn+F41E8EgTPOqJ5VfsqQ5ZXHDs2zSDyUwGI0ZfC2+5A=="
+    },
     "node_modules/emotion-rgba": {
       "version": "0.0.9",
       "resolved": 
"https://registry.npmjs.org/emotion-rgba/-/emotion-rgba-0.0.9.tgz";,
@@ -60299,6 +60309,27 @@
         "react": "^16.13.1"
       }
     },
+    "plugins/plugin-chart-handlebars": {
+      "version": "0.0.0",
+      "license": "Apache-2.0",
+      "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"
+      },
+      "devDependencies": {
+        "@types/jest": "^26.0.0",
+        "jest": "^26.0.1"
+      },
+      "peerDependencies": {
+        "moment": "^2.26.0",
+        "react": "^16.13.1",
+        "react-dom": "^16.13.1"
+      }
+    },
     "plugins/plugin-chart-pivot-table": {
       "name": "@superset-ui/plugin-chart-pivot-table",
       "version": "0.18.25",
@@ -77699,6 +77730,19 @@
         "moment": "^2.26.0"
       }
     },
+    "@superset-ui/plugin-chart-handlebars": {
+      "version": "file:plugins/plugin-chart-handlebars",
+      "requires": {
+        "@superset-ui/chart-controls": "0.18.25",
+        "@superset-ui/core": "0.18.25",
+        "@types/jest": "^26.0.0",
+        "ace-builds": "^1.4.13",
+        "emotion": "^11.0.0",
+        "handlebars": "^4.7.7",
+        "jest": "^26.0.1",
+        "react-ace": "^9.4.4"
+      }
+    },
     "@superset-ui/plugin-chart-pivot-table": {
       "version": "file:plugins/plugin-chart-pivot-table",
       "requires": {
@@ -86171,6 +86215,11 @@
       "resolved": 
"https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz";,
       "integrity": 
"sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
     },
+    "emotion": {
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/emotion/-/emotion-11.0.0.tgz";,
+      "integrity": 
"sha512-QW3CRqic3aRw1OBOcnvxaHEpCmxtlGwZ5tM9dV5rY3Rn+F41E8EgTPOqJ5VfsqQ5ZXHDs2zSDyUwGI0ZfC2+5A=="
+    },
     "emotion-rgba": {
       "version": "0.0.9",
       "resolved": 
"https://registry.npmjs.org/emotion-rgba/-/emotion-rgba-0.0.9.tgz";,
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index c477a1d6e3..5cf75e7c44 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -103,6 +103,7 @@
     "@superset-ui/legacy-preset-chart-deckgl": 
"file:./plugins/legacy-preset-chart-deckgl",
     "@superset-ui/legacy-preset-chart-nvd3": 
"file:./plugins/legacy-preset-chart-nvd3",
     "@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts",
+    "@superset-ui/plugin-chart-handlebars": 
"file:./plugins/plugin-chart-handlebars",
     "@superset-ui/plugin-chart-pivot-table": 
"file:./plugins/plugin-chart-pivot-table",
     "@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
     "@superset-ui/plugin-chart-word-cloud": 
"file:./plugins/plugin-chart-word-cloud",
diff --git a/superset-frontend/plugins/plugin-chart-handlebars/README.md 
b/superset-frontend/plugins/plugin-chart-handlebars/README.md
new file mode 100644
index 0000000000..5b5468cc05
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-handlebars/README.md
@@ -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.
+-->
+
+## @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 renders the data using a handlebars template.
+
+### 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
+<SuperChart
+  chartType="handlebars"
+  width={600}
+  height={600}
+  formData={...}
+  queriesData={[{
+    data: {...},
+  }]}
+/>
+```
+
+### 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/superset-frontend/plugins/plugin-chart-handlebars/package.json 
b/superset-frontend/plugins/plugin-chart-handlebars/package.json
new file mode 100644
index 0000000000..c83be8bfdd
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-handlebars/package.json
@@ -0,0 +1,45 @@
+{
+  "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.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"
+  },
+  "peerDependencies": {
+    "moment": "^2.26.0",
+    "react": "^16.13.1",
+    "react-dom": "^16.13.1"
+  },
+  "devDependencies": {
+    "@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
new file mode 100644
index 0000000000..c14e925056
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-handlebars/src/Handlebars.tsx
@@ -0,0 +1,73 @@
+/**
+ * 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 <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;
+`;
+
+/**
+ * ******************* 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>`
+    : '';
+  const handlebarTemplateSource = formData.handlebarsTemplate
+    ? formData.handlebarsTemplate
+    : '{{data}}';
+  const templateSource = `${handlebarTemplateSource}\n${styleTemplateSource} `;
+
+  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} />
+    </Styles>
+  );
+}
diff --git 
a/superset-frontend/plugins/plugin-chart-handlebars/src/components/CodeEditor/CodeEditor.tsx
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/components/CodeEditor/CodeEditor.tsx
new file mode 100644
index 0000000000..5128fd8275
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/components/CodeEditor/CodeEditor.tsx
@@ -0,0 +1,80 @@
+/**
+ * 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 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<CodeEditorProps> = ({
+  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 (
+    <div className="code-editor" style={{ minHeight: height, width: m_width }}>
+      <AceEditor
+        mode={m_mode}
+        theme={m_theme}
+        name={m_name}
+        height={m_height}
+        width={m_width}
+        fontSize={14}
+        showPrintMargin
+        focus
+        editorProps={{ $blockScrolling: true }}
+        wrapEnabled
+        highlightActiveLine
+        value={value}
+        setOptions={{
+          enableBasicAutocompletion: true,
+          enableLiveAutocompletion: true,
+          enableSnippets: true,
+          showLineNumbers: true,
+          tabSize: 2,
+          showGutter: true,
+        }}
+        {...rest}
+      />
+    </div>
+  );
+};
diff --git 
a/superset-frontend/plugins/plugin-chart-handlebars/src/components/ControlHeader/controlHeader.tsx
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/components/ControlHeader/controlHeader.tsx
new file mode 100644
index 0000000000..2dac822f8f
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/components/ControlHeader/controlHeader.tsx
@@ -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 React, { ReactNode } from 'react';
+
+interface ControlHeaderProps {
+  children: ReactNode;
+}
+
+export const ControlHeader = ({
+  children,
+}: ControlHeaderProps): JSX.Element => (
+  <div className="ControlHeader">
+    <div className="pull-left">
+      <span role="button">{children}</span>
+    </div>
+  </div>
+);
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
new file mode 100644
index 0000000000..6b3a69b0c7
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/components/Handlebars/HandlebarsViewer.tsx
@@ -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 { SafeMarkdown, styled } from '@superset-ui/core';
+import Handlebars from 'handlebars';
+import moment from 'moment';
+import React, { useMemo, useState } from 'react';
+
+export interface HandlebarsViewerProps {
+  templateSource: string;
+  data: any;
+}
+
+export const HandlebarsViewer = ({
+  templateSource,
+  data,
+}: HandlebarsViewerProps) => {
+  const [renderedTemplate, setRenderedTemplate] = useState('');
+  const [error, setError] = useState('');
+
+  useMemo(() => {
+    try {
+      const template = Handlebars.compile(templateSource);
+      const result = template(data);
+      setRenderedTemplate(result);
+      setError('');
+    } catch (error) {
+      setRenderedTemplate('');
+      setError(error.message);
+    }
+  }, [templateSource, data]);
+
+  const Error = styled.pre`
+    white-space: pre-wrap;
+  `;
+
+  if (error) {
+    return <Error>{error}</Error>;
+  }
+
+  if (renderedTemplate) {
+    return <SafeMarkdown source={renderedTemplate} />;
+  }
+  return <p>Loading...</p>;
+};
+
+//  usage: {{dateFormat my_date format="MMMM YYYY"}}
+Handlebars.registerHelper('dateFormat', function (context, block) {
+  const f = block.hash.format || 'YYYY-MM-DD';
+  return moment(context).format(f);
+});
diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts 
b/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts
new file mode 100644
index 0000000000..e6b215ede3
--- /dev/null
+++ b/superset-frontend/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<number>([
+  [0, t('page_size.all')],
+  1,
+  2,
+  3,
+  4,
+  5,
+  10,
+  20,
+  50,
+  100,
+  200,
+]);
diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/i18n.ts 
b/superset-frontend/plugins/plugin-chart-handlebars/src/i18n.ts
new file mode 100644
index 0000000000..5d015b5665
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-handlebars/src/i18n.ts
@@ -0,0 +1,65 @@
+/**
+ * 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 { 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<Record<Locale, typeof en>> = {
+  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/superset-frontend/plugins/plugin-chart-handlebars/src/images/thumbnail.png 
b/superset-frontend/plugins/plugin-chart-handlebars/src/images/thumbnail.png
new file mode 100644
index 0000000000..342bc23206
Binary files /dev/null and 
b/superset-frontend/plugins/plugin-chart-handlebars/src/images/thumbnail.png 
differ
diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/index.ts 
b/superset-frontend/plugins/plugin-chart-handlebars/src/index.ts
new file mode 100644
index 0000000000..c39fe12b95
--- /dev/null
+++ b/superset-frontend/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/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/buildQuery.ts 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/buildQuery.ts
new file mode 100644
index 0000000000..36bcb96515
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/buildQuery.ts
@@ -0,0 +1,31 @@
+/**
+ * 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, groupby } = formData;
+
+  return buildQueryContext(formData, baseQueryObject => [
+    {
+      ...baseQueryObject,
+      ...(sort_by_metric && { orderby: [[metric, false]] }),
+      ...(groupby && { groupby }),
+    },
+  ]);
+}
diff --git 
a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx
new file mode 100644
index 0000000000..32b3a55a79
--- /dev/null
+++ 
b/superset-frontend/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 { handlebarsTemplateControlSetItem } 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: [
+        [handlebarsTemplateControlSetItem],
+        [styleControlSetItem],
+      ],
+    },
+  ],
+};
+
+export default config;
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
new file mode 100644
index 0000000000..0582bfc23f
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx
@@ -0,0 +1,85 @@
+/**
+ * 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 {
+  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 allColumns: typeof sharedControls.groupby = {
+  type: 'SelectControl',
+  label: t('Columns'),
+  description: t('Columns to display'),
+  multi: true,
+  freeForm: true,
+  allowAll: true,
+  commaChoosesOption: false,
+  default: [],
+  optionRenderer: c => <ColumnOption showType column={c} />,
+  valueRenderer: c => <ColumnOption column={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,
+};
+
+const dndAllColumns: 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)
+    ? dndAllColumns
+    : allColumns,
+};
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
new file mode 100644
index 0000000000..0df08bc1d4
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/groupBy.tsx
@@ -0,0 +1,45 @@
+/**
+ * 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 {
+  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/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx
new file mode 100644
index 0000000000..4d86cdc928
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx
@@ -0,0 +1,77 @@
+/**
+ * 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 {
+  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 HandlebarsTemplateControl = (
+  props: CustomControlConfig<HandlebarsCustomControlProps>,
+) => {
+  const val = String(
+    props?.value ? props?.value : props?.default ? props?.default : '',
+  );
+
+  const updateConfig = (source: string) => {
+    props.onChange(source);
+  };
+  return (
+    <div>
+      <ControlHeader>{props.label}</ControlHeader>
+      <CodeEditor
+        theme="dark"
+        value={val}
+        onChange={source => {
+          updateConfig(source || '');
+        }}
+      />
+    </div>
+  );
+};
+
+export const handlebarsTemplateControlSetItem: ControlSetItem = {
+  name: 'handlebarsTemplate',
+  config: {
+    ...sharedControls.entity,
+    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>`,
+    isInt: false,
+    renderTrigger: true,
+
+    validators: [validateNonEmpty],
+    mapStateToProps: ({ controls }) => ({
+      value: controls?.handlebars_template?.value,
+    }),
+  },
+};
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
new file mode 100644
index 0000000000..7004f45fe3
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/includeTime.ts
@@ -0,0 +1,34 @@
+/**
+ * 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 { 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/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts
new file mode 100644
index 0000000000..701dc27aae
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts
@@ -0,0 +1,38 @@
+/**
+ * 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 {
+  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/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx
new file mode 100644
index 0000000000..88777c9c31
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx
@@ -0,0 +1,103 @@
+/**
+ * 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 {
+  ControlPanelState,
+  ControlSetItem,
+  ControlState,
+  sharedControls,
+} from '@superset-ui/chart-controls';
+import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
+import { getQueryMode, isAggMode, validateAggControlValues } from './shared';
+
+const percentMetrics: 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 dndPercentMetrics = {
+  ...percentMetrics,
+  type: 'DndMetricSelect',
+};
+
+export const percentMetricsControlSetItem: ControlSetItem = {
+  name: 'percent_metrics',
+  config: {
+    ...(isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP)
+      ? dndPercentMetrics
+      : percentMetrics),
+  },
+};
+
+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/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx
new file mode 100644
index 0000000000..728934d719
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx
@@ -0,0 +1,47 @@
+/**
+ * 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 { 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/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/pagination.tsx
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/pagination.tsx
new file mode 100644
index 0000000000..bf4c120717
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/pagination.tsx
@@ -0,0 +1,57 @@
+/**
+ * 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 {
+  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/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/queryMode.tsx
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/queryMode.tsx
new file mode 100644
index 0000000000..b895b97f28
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/queryMode.tsx
@@ -0,0 +1,42 @@
+/**
+ * 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 {
+  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/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/shared.ts
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/shared.ts
new file mode 100644
index 0000000000..5f364a2880
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/shared.ts
@@ -0,0 +1,61 @@
+/**
+ * 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 {
+  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<ControlPanelsContainerProps, 'controls'>) =>
+    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/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx
new file mode 100644
index 0000000000..4d6f259eeb
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx
@@ -0,0 +1,72 @@
+/**
+ * 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 {
+  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<StyleCustomControlProps>) => {
+  const val = String(
+    props?.value ? props?.value : props?.default ? props?.default : '',
+  );
+
+  const updateConfig = (source: string) => {
+    props.onChange(source);
+  };
+  return (
+    <div>
+      <ControlHeader>{props.label}</ControlHeader>
+      <CodeEditor
+        theme="dark"
+        mode="css"
+        value={val}
+        onChange={source => {
+          updateConfig(source || '');
+        }}
+      />
+    </div>
+  );
+};
+
+export const styleControlSetItem: ControlSetItem = {
+  name: 'styleTemplate',
+  config: {
+    ...sharedControls.entity,
+    type: StyleControl,
+    label: t('CSS Styles'),
+    description: t('CSS applied to the chart'),
+    default: '',
+    isInt: false,
+    renderTrigger: true,
+
+    validators: [],
+    mapStateToProps: ({ controls }) => ({
+      value: controls?.handlebars_template?.value,
+    }),
+  },
+};
diff --git 
a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/index.ts 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/index.ts
new file mode 100644
index 0000000000..db5ad528f8
--- /dev/null
+++ b/superset-frontend/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/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/transformProps.ts
 
b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/transformProps.ts
new file mode 100644
index 0000000000..cb83e112d8
--- /dev/null
+++ 
b/superset-frontend/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/superset-frontend/plugins/plugin-chart-handlebars/src/types.ts 
b/superset-frontend/plugins/plugin-chart-handlebars/src/types.ts
new file mode 100644
index 0000000000..2a363059fa
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-handlebars/src/types.ts
@@ -0,0 +1,65 @@
+/**
+ * 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 {
+  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;
+    granularitySqla?: string;
+    time_grain_sqla?: TimeGranularity;
+    column_config?: Record<string, ColumnConfig>;
+  };
+
+export type HandlebarsProps = HandlebarsStylesProps &
+  HandlebarsCustomizeProps & {
+    data: TimeseriesDataRecord[];
+    // add typing here for the props you pass in from transformProps.ts!
+    formData: HandlebarsQueryFormData;
+  };
diff --git 
a/superset-frontend/plugins/plugin-chart-handlebars/test/index.test.ts 
b/superset-frontend/plugins/plugin-chart-handlebars/test/index.test.ts
new file mode 100644
index 0000000000..9121daeca4
--- /dev/null
+++ b/superset-frontend/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/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/buildQuery.test.ts
 
b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/buildQuery.test.ts
new file mode 100644
index 0000000000..217ee50485
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/buildQuery.test.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 { HandlebarsQueryFormData } from '../../src/types';
+import buildQuery from '../../src/plugin/buildQuery';
+
+describe('Handlebars buildQuery', () => {
+  const formData: HandlebarsQueryFormData = {
+    datasource: '5__table',
+    granularitySqla: 'ds',
+    groupby: ['foo'],
+    viz_type: 'my_chart',
+    width: 500,
+    height: 500,
+  };
+
+  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/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts
 
b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts
new file mode 100644
index 0000000000..24aa3c3745
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts
@@ -0,0 +1,56 @@
+/**
+ * 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, QueryFormData } from '@superset-ui/core';
+import { HandlebarsQueryFormData } from '../../src/types';
+import transformProps from '../../src/plugin/transformProps';
+
+describe('Handlebars tranformProps', () => {
+  const formData: HandlebarsQueryFormData = {
+    colorScheme: 'bnbColors',
+    datasource: '3__table',
+    granularitySqla: 'ds',
+    metric: 'sum__num',
+    groupby: ['name'],
+    width: 500,
+    height: 500,
+    viz_type: 'handlebars',
+  };
+  const chartProps = new ChartProps<QueryFormData>({
+    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(
+      expect.objectContaining({
+        width: 800,
+        height: 600,
+        data: [
+          { name: 'Hulk', sum__num: 1, __timestamp: new Date(599616000000) },
+        ],
+      }),
+    );
+  });
+});
diff --git a/superset-frontend/plugins/plugin-chart-handlebars/tsconfig.json 
b/superset-frontend/plugins/plugin-chart-handlebars/tsconfig.json
new file mode 100644
index 0000000000..b6bfaa2d98
--- /dev/null
+++ b/superset-frontend/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/superset-frontend/plugins/plugin-chart-handlebars/types/external.d.ts 
b/superset-frontend/plugins/plugin-chart-handlebars/types/external.d.ts
new file mode 100644
index 0000000000..8f7985ceaf
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-handlebars/types/external.d.ts
@@ -0,0 +1,22 @@
+/**
+ * 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.
+ */
+declare module '*.png' {
+  const value: any;
+  export default value;
+}
diff --git a/superset-frontend/src/visualizations/presets/MainPreset.js 
b/superset-frontend/src/visualizations/presets/MainPreset.js
index dc3736ff17..837cd98a7a 100644
--- a/superset-frontend/src/visualizations/presets/MainPreset.js
+++ b/superset-frontend/src/visualizations/presets/MainPreset.js
@@ -78,6 +78,7 @@ import {
   GroupByFilterPlugin,
 } from 'src/filters/components/';
 import { PivotTableChartPlugin as PivotTableChartPluginV2 } from 
'@superset-ui/plugin-chart-pivot-table';
+import { HandlebarsChartPlugin } from '@superset-ui/plugin-chart-handlebars';
 import FilterBoxChartPlugin from '../FilterBox/FilterBoxChartPlugin';
 import TimeTableChartPlugin from '../TimeTable';
 
@@ -164,6 +165,7 @@ export default class MainPreset extends Preset {
         new TimeColumnFilterPlugin().configure({ key: 'filter_timecolumn' }),
         new TimeGrainFilterPlugin().configure({ key: 'filter_timegrain' }),
         new EchartsTreeChartPlugin().configure({ key: 'tree_chart' }),
+        new HandlebarsChartPlugin().configure({ key: 'handlebars' }),
         ...experimentalplugins,
       ],
     });

Reply via email to