Skip to content
This repository has been archived by the owner on Dec 10, 2021. It is now read-only.

Commit

Permalink
feat(chart): Add <ChartDataProvider /> (#120)
Browse files Browse the repository at this point in the history
* docs: [demo][connection] add ConfigureCORS story for testing CORS

* docs: [demo][ConfigureCORS] better instructions

* feat: [chart] add mvp DataProvider component

* docs: better CORS story, update webpack for @babel/polyfill

* docs: [chart] add DataProvider story with WordCloudPlugin

* docs: [chart] add DataProvider deets to Readme

* test(chart): move SuperChart.test.jsx => .tsx and instead use @ts-ignore

* fix(connection): point interface.request to client.request

* feat(chart): re-write DataProvider as ChartDataProvider

* docs(demo): re-write LegacyWordCloudStories => ChartDataProviderStories

* refactor(chart): use IDENTITY as ChartPlugin buildQuery default

* feat(chart): support legacy + v1 loadQueryData endpoints in ChartClient

* docs(demo): add sankey + sunburst plugins to ChartDataProvider story

* style(chart): run prettier on SuperChart

* feat(chart): export QueryData type from models/ChartProps

* feat(chart): export Metrics and BaseFormData from types/ChartFormData

* feat(chart): add request option overrides in ChartDataProvider

* fix(chart): use Partial<> for ChartClient request option overrides

* test(chart): add ChartDataProvider tests

* build: include demo pkg in type script

* build: move storybook/mocks to test/fixtures

* build: move json-bigint TS declaration to root

* test(chart): clean up ChartDataProvider test TS

* chore(chart): lint fix SuperChart

* fix(chart): set ChartPlugin.buildQuery default back to undefined

* test(connection): fix expected Client.get call count

* test(chart): fix ChartClient tests and add test for legacy API

* fix(chart): uninitialized typo, change fetching => loading

* docs(chart): update README to final ChartDataProvider API

* docs(chart): fix typo

* test(chart): get ChartDataProvider to one hundo

* feat(chart): add and export more meaningful Datasource type

* feat(chart): use Datasource type in ChartClient
  • Loading branch information
williaster authored Mar 19, 2019
1 parent 34581f3 commit ade9dbe
Show file tree
Hide file tree
Showing 22 changed files with 726 additions and 48 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"build:esm": "NODE_ENV=production beemo babel --extensions=\".js,.jsx,.ts,.tsx\" ./src --out-dir esm/ --esm --minify --workspaces=\"@superset-ui/!(demo|generator-superset)\"",
"build:assets": "node ./scripts/buildAssets.js",
"commit": "git-cz",
"type": "NODE_ENV=production beemo typescript --workspaces=\"@superset-ui/!(demo|generator-superset)\" --noEmit",
"type": "NODE_ENV=production beemo typescript --workspaces=\"@superset-ui/!(generator-superset)\" --noEmit",
"type:dts": "NODE_ENV=production beemo typescript --workspaces=\"@superset-ui/!(demo|generator-superset)\" --emitDeclarationOnly",
"lint": "beemo create-config prettier && beemo eslint \"./packages/*/{src,test,storybook}/**/*.{js,jsx,ts,tsx}\"",
"lint:fix": "beemo create-config prettier && beemo eslint --fix \"./packages/*/{src,test,storybook}/**/*.{js,jsx,ts,tsx}\"",
Expand Down
83 changes: 77 additions & 6 deletions packages/superset-ui-chart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,86 @@ Description

#### Example usage

```js
import { xxx } from '@superset-ui/chart';
```
##### `<ChartDataProvider />`

This component is a React utility wrapper around the `@superset-ui/chart` `ChartClient` and will
generally require you to setup `CORS` (CROSS ORIGIN RESOURCE SHARING) to accept cross-origin
requests from domains outside your `Apache Superset` instance:

1. Configure `CORS` in your `Apache Superset` instance.

a. Enable `CORS` requests to (minimally) the resources defined below.

b. Enable `CORS` requests from the relevant domains (i.e., the app in which you will embed
charts)

```python
# config.py
ENABLE_CORS = True
CORS_OPTIONS = {
'supports_credentials': True,
'allow_headers': [
'X-CSRFToken', 'Content-Type', 'Origin', 'X-Requested-With', 'Accept',
],
'resources': [
'/superset/csrf_token/' # auth
'/api/v1/formData/', # sliceId => formData
'/superset/explore_json/*', # legacy query API, formData => queryData
'/api/v1/query/', # new query API, queryContext => queryData
'/superset/fetch_datasource_metadata/' # datasource metadata

],
'origins': ['http://myappdomain:9001'],
}
```

2. Configure `SupersetClient` in the app where you will embed your charts. You can test this
configuration in the `@superset-ui` storybook.

```javascript
import { SupersetClient } from '@superset-ui/connection';

#### API
SupersetClient.configure({
credentials: 'include',
host: `${SUPERSET_APP_HOST}`,
mode: 'cors',
}).init();
```

3. Register any desired / needed `@superset-ui` chart + color plugins.

```javascript
import WordCloudPlugin from '@superset-ui/plugin-chart-word-cloud';

new WordCloudPlugin().configure({ key: 'word_cloud' }).register();
```

4. Pass `SupersetClient` to the `ChartDataProvider` along with the formData for the desired
visualization type.

```javascript
import { ChartDataProvider } from '@superset-ui/chart';

const render = () => (
<DataProvider client={client} formData={formData}>
{({ loading, error, payload }) => (
<>
{loading && <Loader />}

{error && <RenderError error={error} />}

{payload && (
<SuperChart type={CHART_TYPE} chartProps={{ formData, payload, width, height }} />
)}
</>
)}
</DataProvider>
);
```

`fn(args)`
##### `<SuperChart />`

- Do something
Coming soon.

### Development

Expand Down
49 changes: 30 additions & 19 deletions packages/superset-ui-chart/src/clients/ChartClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,29 @@ import {
SupersetClientClass,
} from '@superset-ui/connection';
import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton';
import getChartMetadataRegistry from '../registries/ChartMetadataRegistrySingleton';
import { AnnotationLayerMetadata } from '../types/Annotation';
import { ChartFormData } from '../types/ChartFormData';
import { QueryData } from '../models/ChartProps';
import { Datasource } from '../types/Datasource';

export type SliceIdAndOrFormData =
| {
sliceId: number;
formData?: Partial<ChartFormData>;
}
| {
formData: ChartFormData;
};
// This expands to Partial<All> & (union of all possible single-property types)
type AtLeastOne<All, Each = { [K in keyof All]: Pick<All, K> }> = Partial<All> & Each[keyof Each];

export type SliceIdAndOrFormData = AtLeastOne<{
sliceId: number;
formData: Partial<ChartFormData>;
}>;

interface AnnotationData {
[key: string]: object;
}

interface ChartData {
export interface ChartData {
annotationData: AnnotationData;
datasource: object;
formData: ChartFormData;
queryData: object;
queryData: QueryData;
}

export default class ChartClient {
Expand All @@ -42,7 +44,10 @@ export default class ChartClient {
this.client = client;
}

loadFormData(input: SliceIdAndOrFormData, options?: RequestConfig): Promise<ChartFormData> {
loadFormData(
input: SliceIdAndOrFormData,
options?: Partial<RequestConfig>,
): Promise<ChartFormData> {
/* If sliceId is provided, use it to fetch stored formData from API */
if ('sliceId' in input) {
const promise = this.client
Expand All @@ -69,28 +74,34 @@ export default class ChartClient {
: Promise.reject(new Error('At least one of sliceId or formData must be specified'));
}

loadQueryData(formData: ChartFormData, options?: RequestConfig): Promise<object> {
const buildQuery = getChartBuildQueryRegistry().get(formData.viz_type);
if (buildQuery) {
loadQueryData(formData: ChartFormData, options?: Partial<RequestConfig>): Promise<object> {
const { viz_type: visType } = formData;

if (getChartMetadataRegistry().has(visType)) {
const { useLegacyApi } = getChartMetadataRegistry().get(visType);
const buildQuery = useLegacyApi ? () => formData : getChartBuildQueryRegistry().get(visType);

return this.client
.post({
endpoint: '/api/v1/query/',
postPayload: { query_context: buildQuery(formData) },
endpoint: useLegacyApi ? '/superset/explore_json/' : '/api/v1/query/',
postPayload: {
[useLegacyApi ? 'form_data' : 'query_context']: buildQuery(formData),
},
...options,
} as RequestConfig)
.then(response => response.json as Json);
}

return Promise.reject(new Error(`Unknown chart type: ${formData.viz_type}`));
return Promise.reject(new Error(`Unknown chart type: ${visType}`));
}

loadDatasource(datasourceKey: string, options?: RequestConfig): Promise<object> {
loadDatasource(datasourceKey: string, options?: Partial<RequestConfig>): Promise<Datasource> {
return this.client
.get({
endpoint: `/superset/fetch_datasource_metadata?datasourceKey=${datasourceKey}`,
...options,
} as RequestConfig)
.then(response => response.json as Json);
.then(response => response.json as Datasource);
}

loadAnnotation(annotationLayer: AnnotationLayerMetadata): Promise<object> {
Expand Down
144 changes: 144 additions & 0 deletions packages/superset-ui-chart/src/components/ChartDataProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/* eslint react/sort-comp: 'off' */
import React, { ReactNode } from 'react';
import { SupersetClientInterface, RequestConfig } from '../../../superset-ui-connection/src/types';

import ChartClient, { SliceIdAndOrFormData } from '../clients/ChartClient';
import { ChartFormData } from '../types/ChartFormData';
import { Datasource } from '../types/Datasource';
import { QueryData } from '../models/ChartProps';

interface Payload {
formData: Partial<ChartFormData>;
queryData: QueryData;
datasource?: Datasource;
}

export interface ProvidedProps {
payload?: Payload;
error?: Error;
loading?: boolean;
}

export type Props =
/** User can pass either one or both of sliceId or formData */
SliceIdAndOrFormData & {
/** Child function called with ProvidedProps */
children: (provided: ProvidedProps) => ReactNode;
/** Superset client which is used to fetch data. It should already be configured and initialized. */
client?: SupersetClientInterface;
/** Will fetch and include datasource metadata for SliceIdAndOrFormData in the payload. */
loadDatasource?: boolean;
/** Callback when an error occurs. Enables wrapping the Provider in an ErrorBoundary. */
onError?: (error: ProvidedProps['error']) => void;
/** Callback when data is loaded. */
onLoaded?: (payload: ProvidedProps['payload']) => void;
/** Hook to override the formData request config. */
formDataRequestOptions?: Partial<RequestConfig>;
/** Hook to override the datasource request config. */
datasourceRequestOptions?: Partial<RequestConfig>;
/** Hook to override the queryData request config. */
queryRequestOptions?: Partial<RequestConfig>;
};

type State = {
status: 'uninitialized' | 'loading' | 'error' | 'loaded';
payload?: ProvidedProps['payload'];
error?: ProvidedProps['error'];
};

class ChartDataProvider extends React.PureComponent<Props, State> {
readonly chartClient: ChartClient;

constructor(props: Props) {
super(props);
this.handleFetchData = this.handleFetchData.bind(this);
this.handleReceiveData = this.handleReceiveData.bind(this);
this.handleError = this.handleError.bind(this);
this.chartClient = new ChartClient({ client: props.client });
this.state = { status: 'uninitialized' };
}

componentDidMount() {
this.handleFetchData();
}

componentDidUpdate(prevProps: Props) {
const { formData, sliceId } = this.props;
if (formData !== prevProps.formData || sliceId !== prevProps.sliceId) {
this.handleFetchData();
}
}

private extractSliceIdAndFormData() {
const { formData, sliceId } = this.props;
const result: any = {};

if (formData) result.formData = formData;
if (sliceId) result.sliceId = sliceId;

return result as SliceIdAndOrFormData;
}

private handleFetchData() {
const {
loadDatasource,
formDataRequestOptions,
datasourceRequestOptions,
queryRequestOptions,
} = this.props;

this.setState({ status: 'loading' }, () => {
try {
this.chartClient
.loadFormData(this.extractSliceIdAndFormData(), formDataRequestOptions)
.then(formData =>
Promise.all([
loadDatasource
? this.chartClient.loadDatasource(formData.datasource, datasourceRequestOptions)
: Promise.resolve(undefined),
this.chartClient.loadQueryData(formData, queryRequestOptions),
]).then(([datasource, queryData]) => ({
datasource,
formData,
queryData,
})),
)
.then(this.handleReceiveData)
.catch(this.handleError);
} catch (error) {
this.handleError(error);
}
});
}

handleReceiveData(data: Payload) {
const { onLoaded } = this.props;
if (onLoaded) onLoaded(data);
this.setState({ payload: data, status: 'loaded' });
}

handleError(error: ProvidedProps['error']) {
const { onError } = this.props;
if (onError) onError(error);
this.setState({ error, status: 'error' });
}

render() {
const { children } = this.props;
const { status, payload, error } = this.state;

switch (status) {
case 'loading':
return children({ loading: true });
case 'loaded':
return children({ payload });
case 'error':
return children({ error });
case 'uninitialized':
default:
return null;
}
}
}

export default ChartDataProvider;
2 changes: 2 additions & 0 deletions packages/superset-ui-chart/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export {
export { default as buildQueryContext } from './query/buildQueryContext';
export { default as DatasourceKey } from './query/DatasourceKey';

export { default as ChartDataProvider } from './components/ChartDataProvider';

export * from './types/Annotation';
export * from './types/Datasource';
export * from './types/ChartFormData';
Expand Down
2 changes: 1 addition & 1 deletion packages/superset-ui-chart/src/models/ChartProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type CamelCaseDatasource = PlainObject;
type SnakeCaseDatasource = PlainObject;
type CamelCaseFormData = PlainObject;
type SnakeCaseFormData = PlainObject;
type QueryData = PlainObject;
export type QueryData = PlainObject;
type Filters = any[];
type HandlerFunction = (...args: any[]) => void;
type ChartPropsSelector = (c: ChartPropsConfig) => ChartProps;
Expand Down
4 changes: 2 additions & 2 deletions packages/superset-ui-chart/src/types/ChartFormData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { AnnotationLayerMetadata } from './Annotation';
// https://github.com/Microsoft/TypeScript/issues/13573
// The Metrics in formData is either a string or a proper metric. It will be
// unified into a proper Metric type during buildQuery (see `/query/Metrics.ts`).
type Metrics = Partial<Record<MetricKey, FormDataMetric | FormDataMetric[]>>;
export type Metrics = Partial<Record<MetricKey, FormDataMetric | FormDataMetric[]>>;

type BaseFormData = {
export type BaseFormData = {
datasource: string;
viz_type: string;
annotation_layers?: AnnotationLayerMetadata[];
Expand Down
13 changes: 12 additions & 1 deletion packages/superset-ui-chart/src/types/Datasource.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
/* eslint-disable import/prefer-default-export, no-unused-vars */
import { Column } from './Column';
import { Metric } from './Metric';

export enum DatasourceType {
Table = 'table',
Druid = 'druid',
}

/** @TODO can continue to add fields to this */
export interface Datasource {
id: number;
name: string;
description?: string;
type: DatasourceType;
columns: Column[];
metrics: Metric[];
}
Loading

0 comments on commit ade9dbe

Please sign in to comment.