diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 046e1536e87d5..9ca8a5ca29250 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 c477a1d6e3e15..5cf75e7c44ff4 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 0000000000000..5b5468cc053a4 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-handlebars/README.md @@ -0,0 +1,74 @@ + + +## @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 + +``` + +### 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 0000000000000..c83be8bfdd86c --- /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 0000000000000..c14e925056be6 --- /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
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 ( + + + + ); +} 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 0000000000000..5128fd8275388 --- /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 = ({ + 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/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 0000000000000..2dac822f8f2bb --- /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 => ( +
+
+ {children} +
+
+); 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 0000000000000..6b3a69b0c731f --- /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}; + } + + if (renderedTemplate) { + return ; + } + return

Loading...

; +}; + +// 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 0000000000000..e6b215ede3e66 --- /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([ + [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 0000000000000..5d015b5665975 --- /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> = { + 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 0000000000000..342bc23206413 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 0000000000000..c39fe12b95253 --- /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 0000000000000..36bcb965158f4 --- /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 0000000000000..32b3a55a79fa1 --- /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 0000000000000..0582bfc23f9bf --- /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 => , + 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, +}; + +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 0000000000000..0df08bc1d46ce --- /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 0000000000000..4d86cdc928fe2 --- /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, +) => { + const val = String( + props?.value ? props?.value : props?.default ? props?.default : '', + ); + + const updateConfig = (source: string) => { + props.onChange(source); + }; + return ( +
+ {props.label} + { + updateConfig(source || ''); + }} + /> +
+ ); +}; + +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: `
    + {{#each data}} +
  • {{this}}
  • + {{/each}} +
`, + 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 0000000000000..7004f45fe3bed --- /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 0000000000000..701dc27aae1f2 --- /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 0000000000000..88777c9c3173a --- /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 0000000000000..728934d71910c --- /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 0000000000000..bf4c1207174d1 --- /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 0000000000000..b895b97f28a42 --- /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 0000000000000..5f364a2880b8c --- /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) => + 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 0000000000000..4d6f259eeb501 --- /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) => { + const val = String( + props?.value ? props?.value : props?.default ? props?.default : '', + ); + + const updateConfig = (source: string) => { + props.onChange(source); + }; + return ( +
+ {props.label} + { + updateConfig(source || ''); + }} + /> +
+ ); +}; + +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 0000000000000..db5ad528f8f6a --- /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 0000000000000..cb83e112d863d --- /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 0000000000000..2a363059fa7d8 --- /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; + }; + +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 0000000000000..9121daeca4d91 --- /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 0000000000000..217ee50485f8a --- /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 0000000000000..24aa3c3745a21 --- /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({ + 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 0000000000000..b6bfaa2d98446 --- /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 0000000000000..8f7985ceaf135 --- /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 dc3736ff1728b..837cd98a7aa53 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, ], });