mirror of
https://github.com/apache/superset.git
synced 2024-09-19 12:09:42 -04:00
feat: updated viz plugin generator (#636)
* feat: hello world plugin * Fix yeoman generator * Adding Emotion to template and template result * adding `reactify` technique to example code * fixing errant comma * annoying lint issue * React needed for JSX * questionable typing fix * labelling options * freshly generated hello world * linting and freshly generated plugin * add comments to buildQuery * Add docs * nixing word cloud console logs * lint annoyance * a note on multiple plugin exports * second option for building a plugin is now enabled * Officially supporting viz plugins ;) * fresh plugin build * Adding note about controls, and TODOs for example controls * linting * new lock file * adding file structure to readme * more transform props notes * better notes, better linting * Adding a third option for rendering plugin * fresh package rendering * manually updated plugin. Now let's see if the generator matches it! * template changes, fresh plugin render. * moving file tree to the right place * touchups to template (killing third option) * thumbnail update * Spit out form data, for good measure. * more clarity * dummy controls * bold control works, fresh docs, fresh build! * typing for fancy props * bump superset-ui deps to 0.14 * Implement header text and font size * Add tests + mcense headers + inor cleanup * Replace YourPluginName with packageLabel in README * remove trailing spaces * fix hard coded reference to HelloWorld * removing legacy generators * plugin comment tweaks * typescript comments, minor formatting * adding option for badges in readme * generator offers choice of function or class plugin * Add timeseries option * killing the plugin... we'll make that a separate PR. * add new tests * remove unnecessary imports * fix timeseries type * comment on value/label in select options Co-authored-by: Jesse Yang <jesse.yang@airbnb.com> * moving renderTrigger note up to first instance * control-utils -> chart-controls * Wrapper -> Styles nomenclature * Stronger typing (H/T @ktmud) * ControlPanelConfig type on control config * nixing requiresTime * moving Styles component to be external, passing it props * lint nits * typing tweak Co-authored-by: Ville Brofeldt <ville.v.brofeldt@gmail.com> Co-authored-by: Jesse Yang <jesse.yang@airbnb.com>
This commit is contained in:
parent
a540cc283c
commit
fb69984857
@ -7,7 +7,8 @@
|
||||
|
||||
## Installation
|
||||
|
||||
First, install [Yeoman](http://yeoman.io) and `generator-superset` using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)).
|
||||
First, install [Yeoman](http://yeoman.io) and `generator-superset` using
|
||||
[npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)).
|
||||
|
||||
```bash
|
||||
npm install -g yo
|
||||
@ -16,7 +17,7 @@ npm install -g @superset-ui/generator-superset
|
||||
|
||||
## Usage
|
||||
|
||||
Generate a new package in `@superset-ui`
|
||||
Generate a new package or visualization plugin in `@superset-ui`
|
||||
|
||||
```bash
|
||||
cd superset-ui/packages
|
||||
|
@ -25,14 +25,6 @@ module.exports = class extends Generator {
|
||||
name: 'Create superset-ui chart plugin package',
|
||||
value: 'plugin-chart',
|
||||
},
|
||||
{
|
||||
name: 'Create superset-ui-legacy package',
|
||||
value: 'legacy-plugin-chart',
|
||||
},
|
||||
{
|
||||
name: 'Create superset-ui-legacy chart demo in storybook',
|
||||
value: 'legacy-plugin-chart-demo',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
@ -1,40 +0,0 @@
|
||||
/* eslint-disable sort-keys */
|
||||
|
||||
const Generator = require('yeoman-generator');
|
||||
const chalk = require('chalk');
|
||||
const yosay = require('yosay');
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = class extends Generator {
|
||||
async prompting() {
|
||||
// Have Yeoman greet the user.
|
||||
this.log(yosay(`Welcome to the rad ${chalk.red('generator-superset')} generator!`));
|
||||
|
||||
this.option('skipInstall');
|
||||
|
||||
this.answers = await this.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'packageName',
|
||||
message: 'Package name:',
|
||||
default: _.kebabCase(this.appname.replace('legacy plugin chart', '').trim()), // Default to current folder name
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'packageLabel',
|
||||
message: 'Package label:',
|
||||
default: _.upperFirst(_.camelCase(this.appname.replace('legacy plugin chart', '').trim())), // Default to current folder name
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
writing() {
|
||||
this.fs.copyTpl(this.templatePath('index.js'), this.destinationPath('index.js'), this.answers);
|
||||
this.fs.copyTpl(this.templatePath('data.js'), this.destinationPath('data.js'), this.answers);
|
||||
this.fs.copyTpl(
|
||||
this.templatePath('Stories.jsx'),
|
||||
this.destinationPath('Stories.jsx'),
|
||||
this.answers,
|
||||
);
|
||||
}
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
import React from 'react';
|
||||
import { SuperChart } from '@superset-ui/chart';
|
||||
import data from './data';
|
||||
|
||||
export default [
|
||||
{
|
||||
renderStory: () => (
|
||||
<SuperChart
|
||||
chartType="<%= packageName %>"
|
||||
chartProps={{
|
||||
formData: {},
|
||||
height: 400,
|
||||
payload: { data },
|
||||
width: 400,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
storyName: 'Basic',
|
||||
storyPath: 'plugin-chart-<%= packageName %>|<%= packageLabel %>ChartPlugin',
|
||||
},
|
||||
];
|
@ -1,2 +0,0 @@
|
||||
/* eslint-disable sort-keys, no-magic-numbers */
|
||||
export default {};
|
@ -1,8 +0,0 @@
|
||||
import <%= packageLabel %>ChartPlugin from '../../../../superset-ui-legacy-plugin-chart-<%= packageName %>';
|
||||
import Stories from './Stories';
|
||||
|
||||
new <%= packageLabel %>ChartPlugin().configure({ key: '<%= packageName %>' }).register();
|
||||
|
||||
export default {
|
||||
examples: [...Stories],
|
||||
};
|
@ -1,39 +0,0 @@
|
||||
/* eslint-disable sort-keys */
|
||||
|
||||
const Generator = require('yeoman-generator');
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = class extends Generator {
|
||||
async prompting() {
|
||||
this.option('skipInstall');
|
||||
|
||||
this.answers = await this.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'packageName',
|
||||
message: 'Package name:',
|
||||
default: _.kebabCase(this.appname.replace('superset ui legacy plugin chart', '').trim()), // Default to current folder name
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'description',
|
||||
message: 'Description:',
|
||||
default: _.upperFirst(
|
||||
_.startCase(this.appname.replace('superset ui legacy plugin chart', '').trim()),
|
||||
), // Default to current folder name
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
writing() {
|
||||
this.fs.copyTpl(
|
||||
this.templatePath('_package.json'),
|
||||
this.destinationPath('package.json'),
|
||||
this.answers,
|
||||
);
|
||||
this.fs.copyTpl(this.templatePath('README.md'), this.destinationPath('README.md'), {
|
||||
...this.answers,
|
||||
packageLabel: _.upperFirst(_.camelCase(this.answers.packageName)),
|
||||
});
|
||||
}
|
||||
};
|
@ -1,34 +0,0 @@
|
||||
## @superset-ui/legacy-plugin-chart-<%= packageName %>
|
||||
|
||||
[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-<%= packageName %>.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-<%= packageName %>.svg?style=flat-square)
|
||||
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui.svg?path=packages%2Fsuperset-ui-legacy-plugin-chart-<%= packageName %>&style=flat-square)](https://david-dm.org/apache-superset/superset-ui?path=packages/superset-ui-legacy-plugin-chart-<%= packageName %>)
|
||||
|
||||
This plugin provides <%= description %> for Superset.
|
||||
|
||||
### Usage
|
||||
|
||||
Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to lookup this chart throughout the app.
|
||||
|
||||
```js
|
||||
import <%= packageLabel %>ChartPlugin from '@superset-ui/legacy-plugin-chart-<%= packageName %>';
|
||||
|
||||
new <%= packageLabel %>ChartPlugin()
|
||||
.configure({ key: '<%= packageName %>' })
|
||||
.register();
|
||||
```
|
||||
|
||||
Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/superset-ui-legacy/?selectedKind=plugin-chart-<%= packageName %>) for more details.
|
||||
|
||||
```js
|
||||
<SuperChart
|
||||
chartType="<%= packageName %>"
|
||||
chartProps={{
|
||||
width: 600,
|
||||
height: 600,
|
||||
formData: {...},
|
||||
payload: {
|
||||
data: {...},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
```
|
@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "@superset-ui/legacy-plugin-chart-<%= packageName %>",
|
||||
"version": "0.0.0",
|
||||
"description": "Superset Legacy Chart - <%= description %>",
|
||||
"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-legacy.git"
|
||||
},
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
"author": "Superset",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache-superset/superset-ui-legacy/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache-superset/superset-ui-legacy#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@superset-ui/chart": "latest",
|
||||
"@superset-ui/translation": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart": "latest",
|
||||
"@superset-ui/translation": "latest"
|
||||
}
|
||||
}
|
@ -22,6 +22,42 @@ module.exports = class extends Generator {
|
||||
// Default to current folder name
|
||||
default: _.upperFirst(_.startCase(this.appname.replace('plugin chart', '').trim())),
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'componentType',
|
||||
message: 'What type of React component would you like?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Class component',
|
||||
value: 'class',
|
||||
},
|
||||
{
|
||||
name: 'Function component (with hooks)',
|
||||
value: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'chartType',
|
||||
message: 'What type of chart would you like?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Time-series chart',
|
||||
value: 'timeseries',
|
||||
},
|
||||
{
|
||||
name: 'Regular chart',
|
||||
value: 'regular',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'addBadges',
|
||||
message: "Add superset-ui badges to your plugin's README.md",
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@ -37,10 +73,14 @@ module.exports = class extends Generator {
|
||||
['package.erb', 'package.json'],
|
||||
['README.erb', 'README.md'],
|
||||
['src/index.erb', 'src/index.ts'],
|
||||
['src/plugin/buildQuery.erb', 'src/plugin/buildQuery.ts'],
|
||||
['src/plugin/controlPanel.erb', 'src/plugin/controlPanel.ts'],
|
||||
['src/plugin/index.erb', 'src/plugin/index.ts'],
|
||||
['src/plugin/transformProps.txt', 'src/plugin/transformProps.ts'],
|
||||
['src/plugin/transformProps.erb', 'src/plugin/transformProps.ts'],
|
||||
['src/MyChart.erb', `src/${packageLabel}.tsx`],
|
||||
['test/index.erb', 'test/index.test.ts'],
|
||||
['test/plugin/buildQuery.test.erb', 'test/plugin/buildQuery.test.ts'],
|
||||
['test/plugin/transformProps.test.erb', 'test/plugin/transformProps.test.ts'],
|
||||
].forEach(([src, dest]) => {
|
||||
this.fs.copyTpl(this.templatePath(src), this.destinationPath(dest), params);
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
## @superset-ui/plugin-chart-<%= packageName %>
|
||||
|
||||
[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-<%= packageName %>.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/plugin-chart-<%= packageName %>.svg?style=flat-square)
|
||||
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui.svg?path=packages%2Fsuperset-ui-plugin-chart-<%= packageName %>&style=flat-square)](https://david-dm.org/apache-superset/superset-ui?path=packages/superset-ui-plugin-chart-<%= packageName %>)
|
||||
<%if (addBadges) { %>[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-<%= packageName %>.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/plugin-chart-<%= packageName %>.svg?style=flat-square)
|
||||
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui.svg?path=packages%2Fsuperset-ui-plugin-chart-<%= packageName %>&style=flat-square)](https://david-dm.org/apache-superset/superset-ui?path=packages/superset-ui-plugin-chart-<%= packageName %>)<% } %>
|
||||
|
||||
This plugin provides <%= description %> for Superset.
|
||||
|
||||
@ -30,3 +30,25 @@ Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### File structure generated
|
||||
|
||||
```
|
||||
├── README.md
|
||||
├── package.json
|
||||
├── src
|
||||
│ ├── <%= packageLabel %>.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
|
||||
```
|
@ -26,7 +26,16 @@
|
||||
"access": "public"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart": "latest",
|
||||
"@superset-ui/translation": "latest"
|
||||
"@superset-ui/chart": "^0.14.1",
|
||||
"@superset-ui/query": "^0.14.1",
|
||||
"@superset-ui/chart-controls": "^0.14.0",
|
||||
"@superset-ui/translation": "^0.14.0",
|
||||
"@superset-ui/validator": "^0.14.1",
|
||||
"@superset-ui/style": "^0.14.0",
|
||||
"react": "^16.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.0",
|
||||
"jest": "^26.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -16,25 +16,115 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { <%if (componentType == 'class') { %>PureComponent<% } %><%if (componentType == 'function') { %>useEffect<% } %>, createRef } from 'react';
|
||||
import styled, { supersetTheme } from '@superset-ui/style';
|
||||
|
||||
interface <%= packageLabel %>StylesProps {
|
||||
height: number;
|
||||
width: number;
|
||||
headerFontSize: keyof typeof supersetTheme.typography.sizes;
|
||||
boldText: boolean;
|
||||
}
|
||||
|
||||
export type <%= packageLabel %>Props = {
|
||||
height: number;
|
||||
width: number;
|
||||
data: { x: number; y: number }[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: Record<any, any>; // please add additional typing for your data here
|
||||
// add typing here for the props you pass in from transformProps.ts!
|
||||
boldText: boolean;
|
||||
headerFontSize: 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl';
|
||||
headerText: string;
|
||||
};
|
||||
|
||||
export default class <%= packageLabel %> extends React.PureComponent<<%= packageLabel %>Props> {
|
||||
// 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/style. For variables available, please visit
|
||||
// https://github.com/apache-superset/superset-ui/blob/master/packages/superset-ui-style/src/index.ts
|
||||
|
||||
const Styles = styled.div<<%= packageLabel %>StylesProps>`
|
||||
background-color: ${({ theme }) => theme.colors.secondary.light2};
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
border-radius: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
height: ${({ height }) => height};
|
||||
width: ${({ width }) => width};
|
||||
overflow-y: scroll;
|
||||
|
||||
h3 {
|
||||
/* You can use your props to control CSS! */
|
||||
font-size: ${({ theme, headerFontSize }) => theme.typography.sizes[headerFontSize]};
|
||||
font-weight: ${({ theme, boldText }) => theme.typography.weights[boldText ? 'bold' : 'normal']};
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* ******************* 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
|
||||
*/
|
||||
|
||||
<%if (componentType == 'class') { %>export default class <%= packageLabel %> extends PureComponent<<%= packageLabel %>Props> {
|
||||
// Often, you just want to get a hold of the DOM and go nuts.
|
||||
// Here, you can do that with createRef, and componentDidMount.
|
||||
|
||||
rootElem = createRef<HTMLDivElement>();
|
||||
|
||||
componentDidMount() {
|
||||
const root = this.rootElem.current as HTMLElement;
|
||||
console.log('Plugin element', root);
|
||||
}
|
||||
|
||||
render() {
|
||||
// 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 🎉
|
||||
console.log('Approach 1 props', this.props);
|
||||
const { data, height, width } = this.props;
|
||||
|
||||
console.log('Plugin props', this.props);
|
||||
|
||||
return (
|
||||
<div style={{ backgroundColor: '#ffe459', padding: 16, borderRadius: 8, height, width }}>
|
||||
<h3>Hello!</h3>
|
||||
<pre>
|
||||
{JSON.stringify(this.props, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
<Styles
|
||||
ref={this.rootElem}
|
||||
boldText={this.props.boldText}
|
||||
headerFontSize={this.props.headerFontSize}
|
||||
height={height}
|
||||
width={width}
|
||||
>
|
||||
<h3>{this.props.headerText}</h3>
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
</Styles>
|
||||
);
|
||||
}
|
||||
}
|
||||
}<% } %><%if (componentType == 'function') { %>export default function <%= packageLabel %>(props: <%= packageLabel %>Props) {
|
||||
// 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 } = props;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
console.log('Plugin props', props);
|
||||
|
||||
return (
|
||||
<Styles
|
||||
ref={rootElem}
|
||||
boldText={props.boldText}
|
||||
headerFontSize={props.headerFontSize}
|
||||
height={height}
|
||||
width={width}
|
||||
>
|
||||
<h3>{props.headerText}</h3>
|
||||
<pre>${JSON.stringify(data, null, 2)}</pre>
|
||||
</Styles>
|
||||
);
|
||||
}<% } %>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 20 KiB |
@ -1 +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 <%= packageLabel %>ChartPlugin } from './plugin';
|
||||
/**
|
||||
* Note: this file exports the default export from <%= packageLabel %>.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 <%= packageLabel %>.tsx
|
||||
*/
|
||||
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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/query';
|
||||
|
||||
/**
|
||||
* The buildQuery function is used to create an instance of QueryContext that's
|
||||
* sent to the chart data endpoint. In addition to containing information of which
|
||||
* datasource to use, it specifies the type (e.g. full payload, samples, query) and
|
||||
* format (e.g. CSV or JSON) of the result and whether or not to force refresh the data from
|
||||
* the datasource as opposed to using a cached copy of the data, if available.
|
||||
*
|
||||
* More importantly though, QueryContext contains a property `queries`, which is an array of
|
||||
* QueryObjects specifying individual data requests to be made. A QueryObject specifies which
|
||||
* columns, metrics and filters, among others, to use during the query. Usually it will be enough
|
||||
* to specify just one query based on the baseQueryObject, but for some more advanced use cases
|
||||
* it is possible to define post processing operations in the QueryObject, or multiple queries
|
||||
* if a viz needs multiple different result sets.
|
||||
*/
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
<%if (chartType === 'timeseries') { %> // Time series charts need to set the `is_timeseries` flag to true
|
||||
is_timeseries: true,
|
||||
<% } %> },
|
||||
]);
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
/**
|
||||
* 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 { t } from '@superset-ui/translation';
|
||||
import { validateNonEmpty } from '@superset-ui/validator';
|
||||
import { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||
|
||||
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 due
|
||||
* to the `series` control having the property `queryField` set to
|
||||
* `groupby`, which automatically appends the values from the
|
||||
* `series` control to the `groupby` control 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/validator';
|
||||
* 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/validator` package:
|
||||
* - 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: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [['groupby'], ['metrics'], ['adhoc_filters'], ['row_limit', null]],
|
||||
},
|
||||
{
|
||||
label: t('Hello Controls!'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'header_text',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
default: 'Hello, World!',
|
||||
renderTrigger: true,
|
||||
// ^ this makes it apply instantaneously, without triggering a "run query" button
|
||||
label: t('Header Text'),
|
||||
description: t('The text you want to see in the header'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'bold_text',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Bold Text'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('A checkbox to make the '),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'header_font_size',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Font Size'),
|
||||
default: 'xl',
|
||||
choices: [
|
||||
// [value, label]
|
||||
['xxs', 'xx-small'],
|
||||
['xs', 'x-small'],
|
||||
['s', 'small'],
|
||||
['m', 'medium'],
|
||||
['l', 'large'],
|
||||
['xl', 'x-large'],
|
||||
['xxl', 'xx-large'],
|
||||
],
|
||||
renderTrigger: true,
|
||||
description: t('The size of your header font'),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
<%if (chartType === 'timeseries') { %> // Time series charts need to override the `druidTimeSeries` and `sqlaTimeSeries`
|
||||
// sections to add the time grain dropdown.
|
||||
sectionOverrides: {
|
||||
druidTimeSeries: {
|
||||
controlSetRows: [['granularity', 'druid_time_origin'], ['time_range']],
|
||||
},
|
||||
sqlaTimeSeries: {
|
||||
controlSetRows: [['granularity_sqla', 'time_grain_sqla'], ['time_range']],
|
||||
},
|
||||
},<% } %>
|
||||
controlOverrides: {
|
||||
series: {
|
||||
validators: [validateNonEmpty],
|
||||
clearable: false,
|
||||
},
|
||||
row_limit: {
|
||||
default: 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
@ -18,6 +18,8 @@
|
||||
*/
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
|
||||
import buildQuery from './buildQuery';
|
||||
import controlPanel from './controlPanel';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from '../images/thumbnail.png';
|
||||
|
||||
@ -28,8 +30,20 @@ const metadata = new ChartMetadata({
|
||||
});
|
||||
|
||||
export default class <%= packageLabel %>ChartPlugin 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() {
|
||||
super({
|
||||
buildQuery,
|
||||
controlPanel,
|
||||
loadChart: () => import('../<%= packageLabel %>'),
|
||||
metadata,
|
||||
transformProps,
|
||||
|
@ -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 { ChartProps, DataRecord } from '@superset-ui/chart';
|
||||
|
||||
<%if (chartType === 'timeseries') { %>type TimestampType = string | number | Date;
|
||||
|
||||
interface <%= packageLabel %>Datum extends DataRecord {
|
||||
__timestamp: TimestampType;
|
||||
}<% } else { %>type <%= packageLabel %>Datum = DataRecord;<% } %>
|
||||
|
||||
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 <%= packageLabel %>.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.
|
||||
* - `queryData`: the chart data response payload that was received
|
||||
* from the backend. Some notable properties of `queryData`:
|
||||
* - `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, queryData } = chartProps;
|
||||
const data = queryData.data as <%= packageLabel %>Datum[];
|
||||
|
||||
console.log('formData via TransformProps.ts', formData);
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
<%if (chartType === 'timeseries') { %>
|
||||
data: data.map((item: { __timestamp: TimestampType }) => ({
|
||||
...item,
|
||||
// convert epoch to native Date
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
__timestamp: new Date(item.__timestamp),
|
||||
})),<% } else { %> data,<% } %>
|
||||
// and now your control data, manipulated as needed, and passed through as props!
|
||||
boldText: formData.boldText,
|
||||
headerFontSize: formData.headerFontSize,
|
||||
headerText: formData.headerText,
|
||||
};
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { ChartProps } from '@superset-ui/chart';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, queryData } = chartProps;
|
||||
const { color } = formData;
|
||||
const { data } = queryData;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
color,
|
||||
data,
|
||||
};
|
||||
}
|
@ -1,5 +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 { <%= packageLabel %>ChartPlugin } 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-<%= packageName %>', () => {
|
||||
it('exists', () => {
|
||||
expect(<%= packageLabel %>ChartPlugin).toBeDefined();
|
||||
|
@ -0,0 +1,18 @@
|
||||
import 'babel-polyfill';
|
||||
import buildQuery from '../../src/plugin/buildQuery';
|
||||
|
||||
describe('<%= packageLabel %> buildQuery', () => {
|
||||
const formData = {
|
||||
datasource: '5__table',
|
||||
granularity_sqla: 'ds',
|
||||
series: 'foo',
|
||||
viz_type: 'my_chart',
|
||||
queryFields: { series: 'groupby' },
|
||||
};
|
||||
|
||||
it('should build groupby with series in form data', () => {
|
||||
const queryContext = buildQuery(formData);
|
||||
const [query] = queryContext.queries;
|
||||
expect(query.groupby).toEqual(['foo']);
|
||||
});
|
||||
});
|
@ -0,0 +1,35 @@
|
||||
import 'babel-polyfill';
|
||||
import { ChartProps } from '@superset-ui/chart';
|
||||
import transformProps from '../../src/plugin/transformProps';
|
||||
|
||||
describe('<%= packageLabel %> tranformProps', () => {
|
||||
const formData = {
|
||||
colorScheme: 'bnbColors',
|
||||
datasource: '3__table',
|
||||
granularity_sqla: 'ds',
|
||||
metric: 'sum__num',
|
||||
series: 'name',
|
||||
boldText: true,
|
||||
headerFontSize: 'xs',
|
||||
headerText: 'my text',
|
||||
};
|
||||
const chartProps = new ChartProps({
|
||||
formData,
|
||||
width: 800,
|
||||
height: 600,
|
||||
queryData: {
|
||||
data: [{ name: 'Hulk', sum__num: 1<%if (chartType === 'timeseries') { %>, __timestamp: 599616000000<% } %> }],
|
||||
},
|
||||
});
|
||||
|
||||
it('should tranform chart props for viz', () => {
|
||||
expect(transformProps(chartProps)).toEqual({
|
||||
width: 800,
|
||||
height: 600,
|
||||
boldText: true,
|
||||
headerFontSize: 'xs',
|
||||
headerText: 'my text',
|
||||
data: [{ name: 'Hulk', sum__num: 1<%if (chartType === 'timeseries') { %>, __timestamp: new Date(599616000000)<% } %> }],
|
||||
});
|
||||
});
|
||||
});
|
@ -1,35 +0,0 @@
|
||||
const path = require('path');
|
||||
const assert = require('yeoman-assert');
|
||||
const helpers = require('yeoman-test');
|
||||
|
||||
describe('generator-superset:legacy-plugin-chart-demo', () => {
|
||||
let dir;
|
||||
|
||||
beforeAll(() => {
|
||||
dir = process.cwd();
|
||||
|
||||
return helpers
|
||||
.run(path.join(__dirname, '../generators/legacy-plugin-chart-demo'))
|
||||
.withPrompts({ packageName: '4d-pie-chart', packageLabel: '4DPieChart' })
|
||||
.withOptions({ skipInstall: true });
|
||||
});
|
||||
|
||||
/*
|
||||
* Change working directory back to original working directory
|
||||
* after the test has completed.
|
||||
* yeoman tests switch to tmp directory and write files there.
|
||||
* Usually this is fine for solo package.
|
||||
* However, for a monorepo like this one,
|
||||
* it made jest confuses with current directory
|
||||
* (being in tmp directory instead of superset-ui root)
|
||||
* and interferes with other tests in sibling packages
|
||||
* that are run after the yeoman tests.
|
||||
*/
|
||||
afterAll(() => {
|
||||
process.chdir(dir);
|
||||
});
|
||||
|
||||
it('creates files', () => {
|
||||
assert.file(['index.js', 'Stories.jsx']);
|
||||
});
|
||||
});
|
@ -1,35 +0,0 @@
|
||||
const path = require('path');
|
||||
const assert = require('yeoman-assert');
|
||||
const helpers = require('yeoman-test');
|
||||
|
||||
describe('generator-superset:legacy-plugin-chart', () => {
|
||||
let dir;
|
||||
|
||||
beforeAll(() => {
|
||||
dir = process.cwd();
|
||||
|
||||
return helpers
|
||||
.run(path.join(__dirname, '../generators/legacy-plugin-chart'))
|
||||
.withPrompts({ packageName: '4d-pie-chart', description: '4D Pie Chart' })
|
||||
.withOptions({ skipInstall: true });
|
||||
});
|
||||
|
||||
/*
|
||||
* Change working directory back to original working directory
|
||||
* after the test has completed.
|
||||
* yeoman tests switch to tmp directory and write files there.
|
||||
* Usually this is fine for solo package.
|
||||
* However, for a monorepo like this one,
|
||||
* it made jest confuses with current directory
|
||||
* (being in tmp directory instead of superset-ui root)
|
||||
* and interferes with other tests in sibling packages
|
||||
* that are run after the yeoman tests.
|
||||
*/
|
||||
afterAll(() => {
|
||||
process.chdir(dir);
|
||||
});
|
||||
|
||||
it('creates files', () => {
|
||||
assert.file(['package.json']);
|
||||
});
|
||||
});
|
@ -33,11 +33,15 @@ describe('generator-superset:plugin-chart', () => {
|
||||
assert.file([
|
||||
'package.json',
|
||||
'README.md',
|
||||
'src/plugin/buildQuery.ts',
|
||||
'src/plugin/controlPanel.ts',
|
||||
'src/plugin/index.ts',
|
||||
'src/plugin/transformProps.ts',
|
||||
'src/index.ts',
|
||||
'src/ColdMap.tsx',
|
||||
'src/index.ts',
|
||||
'test/index.test.ts',
|
||||
'test/plugin/buildQuery.test.ts',
|
||||
'test/plugin/transformProps.test.ts',
|
||||
'types/external.d.ts',
|
||||
'src/images/thumbnail.png',
|
||||
]);
|
||||
|
@ -205,7 +205,6 @@ type SelectOption = AnyDict | string | [ReactText, ReactNode];
|
||||
type SelectControlType =
|
||||
| 'SelectControl'
|
||||
| 'SelectAsyncControl'
|
||||
| 'SelectControl'
|
||||
| 'MetricsControl'
|
||||
| 'FixedOrMetricControl'
|
||||
| 'AdhocFilterControl'
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user