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:
Evan Rusackas 2020-07-02 08:52:24 -07:00 committed by Yongjie Zhao
parent a540cc283c
commit fb69984857
28 changed files with 1395 additions and 322 deletions

View File

@ -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

View File

@ -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',
},
],
},
]);

View File

@ -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,
);
}
};

View File

@ -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',
},
];

View File

@ -1,2 +0,0 @@
/* eslint-disable sort-keys, no-magic-numbers */
export default {};

View File

@ -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],
};

View File

@ -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)),
});
}
};

View File

@ -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: {...},
},
}}
/>
```

View File

@ -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"
}
}

View File

@ -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);
});

View File

@ -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
```

View File

@ -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"
}
}

View File

@ -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

View File

@ -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
*/

View File

@ -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,
<% } %> },
]);
}

View File

@ -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;

View File

@ -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,

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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();

View File

@ -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']);
});
});

View File

@ -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)<% } %> }],
});
});
});

View File

@ -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']);
});
});

View File

@ -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']);
});
});

View File

@ -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',
]);

View File

@ -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