refactor(bi): improve chart frontend api (#2721)

* refactor: improve chart frontend api

* chore: remove redundant import

* fix: rename chartType

* chore: add migration check

* fix: add migration

* fix: change version

* chore: update
This commit is contained in:
YANG QIA 2023-09-27 15:50:04 +08:00 committed by GitHub
parent 729fdd04b7
commit ae988d00b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 189 additions and 212 deletions

View File

@ -0,0 +1,24 @@
import { SchemaInitializerContext, SchemaInitializerProvider } from '@nocobase/client';
import React, { useContext } from 'react';
import { ChartInitializers } from './block';
import { useChartsTranslation } from './locale';
export const DataVisualization: React.FC = (props) => {
const { t } = useChartsTranslation();
const initializers = useContext<any>(SchemaInitializerContext);
const children = initializers.BlockInitializers.items[0].children;
const has = children.some((initializer) => initializer.component === 'ChartV2BlockInitializer');
if (!has) {
children.push({
key: 'chart-v2',
type: 'item',
title: t('Charts'),
component: 'ChartV2BlockInitializer',
});
}
return (
<SchemaInitializerProvider initializers={{ ...initializers, ChartInitializers }}>
{props.children}
</SchemaInitializerProvider>
);
};

View File

@ -1,43 +0,0 @@
import { CheckOutlined } from '@ant-design/icons';
import { Form, FormItem } from '@formily/antd-v5';
import { css } from '@nocobase/client';
import { Button, Card } from 'antd';
import cls from 'classnames';
import React, { useContext } from 'react';
import { useChartsTranslation } from './locale';
import { ChartLibraryContext, useToggleChartLibrary } from './chart/library';
export const Settings = () => {
const { t } = useChartsTranslation();
const libraries = useContext(ChartLibraryContext);
const { toggle } = useToggleChartLibrary();
const list = Object.entries(libraries).map(([library, { enabled }]) => {
return (
<Button
key={library}
icon={enabled ? <CheckOutlined /> : ''}
className={cls(
css`
margin: 8px 8px 8px 0;
`,
enabled
? css`
color: #40a9ff;
border-color: #40a9ff;
`
: '',
)}
onClick={() => toggle(library)}
>
{library}
</Button>
);
});
return (
<Card>
<Form layout="vertical">
<FormItem label={t('Enabled Chart Library')}>{list}</FormItem>
</Form>
</Card>
);
};

View File

@ -1,4 +1,3 @@
import { ISchema } from '@formily/react';
import { AntdChart } from './antd';
import { Statistic as AntdStatistic } from 'antd';
import { lang } from '../../locale';

View File

@ -3,7 +3,7 @@ import { FieldOption } from '../hooks';
import { QueryProps } from '../renderer';
import { parseField } from '../utils';
import { ISchema } from '@formily/react';
import configs, { AnySchemaProperties, ConfigProps } from './configs';
import configs, { AnySchemaProperties, Config } from './configs';
export type RenderProps = {
data: any[];
@ -21,21 +21,6 @@ export interface ChartType {
title: string;
component: React.FC<any>;
schema: ISchema;
infer: (
fields: FieldOption[],
{
measures,
dimensions,
}: {
measures?: QueryProps['measures'];
dimensions?: QueryProps['dimensions'];
},
) => {
xField: FieldOption;
yField: FieldOption;
seriesField: FieldOption;
yFields: FieldOption[];
};
init?: (
fields: FieldOption[],
query: {
@ -46,38 +31,25 @@ export interface ChartType {
general?: any;
advanced?: any;
};
/**
* getProps
* Accept the information that the chart component needs to render,
* process it and return the props of the chart component.
*/
getProps: (props: RenderProps) => any;
render: (props: RenderProps) => React.FC<any>;
getReference?: () => {
title: string;
link: string;
};
render: (props: RenderProps) => React.FC<any>;
}
type Config = (
| (ConfigProps & {
property?: string;
})
| string
)[];
export type ChartProps = {
name: string;
title: string;
component: React.FC<any>;
config?: Config;
config?: Config[];
};
export class Chart implements ChartType {
name: string;
title: string;
component: React.FC<any>;
config: Config;
config: Config[];
configs = new Map<string, Function>();
constructor({ name, title, component, config }: ChartProps) {
@ -184,6 +156,11 @@ export class Chart implements ChartType {
return { xField, yField, seriesField, yFields };
}
/**
* getProps
* Accept the information that the chart component needs to render,
* process it and return the props of the chart component.
*/
getProps(props: RenderProps) {
return props;
}

View File

@ -11,10 +11,16 @@ export type FieldConfigProps = Partial<{
export type AnySchemaProperties = SchemaProperties<any, any, any, any, any, any, any, any>;
export type ConfigProps = FieldConfigProps | AnySchemaProperties | (() => AnySchemaProperties);
export type Config =
| (ConfigProps & {
property?: string;
})
| string;
const selectField = ({ name, title, required, defaultValue }: FieldConfigProps) => {
return {
[name]: {
title: lang(title),
[name || 'field']: {
title: lang(title || 'Field'),
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
@ -27,8 +33,8 @@ const selectField = ({ name, title, required, defaultValue }: FieldConfigProps)
const booleanField = ({ name, title, defaultValue = false }: FieldConfigProps) => {
return {
[name]: {
'x-content': lang(title),
[name || 'field']: {
'x-content': lang(title || 'Field'),
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',

View File

@ -0,0 +1,86 @@
import { usePlugin } from '@nocobase/client';
import { ChartType } from './chart';
import DataVisualizationPlugin from '..';
import { lang } from '../locale';
export class ChartGroup {
charts: Map<string, ChartType[]> = new Map();
setGroup(name: string, charts: ChartType[]) {
this.charts.set(name, charts);
}
addGroup(name: string, charts: ChartType[]) {
if (this.charts.has(name)) {
throw new Error(`[data-visualization] Chart group "${name}" already exists`);
}
this.setGroup(name, charts);
}
add(group: string, chart: ChartType) {
if (!this.charts.has(group)) {
this.setGroup(group, []);
}
this.charts.get(group)?.push(chart);
}
getChartTypes(): {
label: string;
children: {
key: string;
label: string;
value: string;
}[];
}[] {
const result = [];
this.charts.forEach((charts, group) => {
const children = charts.map((chart) => ({
key: `${group}.${chart.name}`,
label: lang(chart.title),
value: `${group}.${chart.name}`,
}));
result.push({
label: lang(group),
children,
});
});
return result;
}
getCharts(): {
[key: string]: ChartType;
} {
const result = {};
this.charts.forEach((charts, group) => {
charts.forEach((chart) => {
result[`${group}.${chart.name}`] = chart;
});
});
return result;
}
getChart(type: string): ChartType {
const charts = this.getCharts();
return charts[type];
}
}
export const useChartTypes = () => {
const plugin = usePlugin(DataVisualizationPlugin);
return plugin.charts.getChartTypes();
};
export const useDefaultChartType = () => {
const chartTypes = useChartTypes();
return chartTypes[0]?.children?.[0]?.value;
};
export const useCharts = () => {
const plugin = usePlugin(DataVisualizationPlugin);
return plugin.charts.getCharts();
};
export const useChart = (type: string) => {
const plugin = usePlugin(DataVisualizationPlugin);
return plugin.charts.getChart(type);
};

View File

@ -1,89 +0,0 @@
import React, { createContext, useContext } from 'react';
import { lang } from '../locale';
import { ChartType } from './chart';
export type ChartLibraries = {
[library: string]: {
enabled: boolean;
charts: ChartType[];
};
};
export const ChartLibraryContext = createContext<ChartLibraries>({});
export const useCharts = (): {
[name: string]: ChartType;
} => {
const library = useContext(ChartLibraryContext);
return Object.values(library)
.filter((l) => l.enabled)
.reduce((allCharts, l) => {
const charts = Object.values(l.charts);
return {
...allCharts,
...charts.reduce((all, chart) => {
return {
...all,
[chart.name]: chart,
};
}, {}),
};
}, {});
};
export const useChartTypes = (): {
label: string;
children: {
key: string;
label: string;
value: string;
}[];
}[] => {
const library = useContext(ChartLibraryContext);
return Object.entries(library)
.filter(([_, l]) => l.enabled)
.reduce((charts, [name, l]) => {
const children = Object.values(l.charts).map((chart) => ({
key: chart.name,
label: lang(chart.title),
value: chart.name,
}));
return [
...charts,
{
label: lang(name),
children,
},
];
}, []);
};
export const useDefaultChartType = () => {
const chartTypes = useChartTypes();
return chartTypes[0]?.children?.[0]?.value;
};
export const useToggleChartLibrary = () => {
const ctx = useContext(ChartLibraryContext);
return {
toggle: (library: string) => {
ctx[library].enabled = !ctx[library].enabled;
},
};
};
export const ChartLibraryProvider: React.FC<{
name: string;
charts: ChartType[];
}> = (props) => {
const { children, charts, name } = props;
const ctx = useContext(ChartLibraryContext);
const library = {
...ctx,
[name]: {
charts,
enabled: true,
},
};
return <ChartLibraryContext.Provider value={library}>{children}</ChartLibraryContext.Provider>;
};

View File

@ -29,7 +29,7 @@ import { useChartsTranslation } from '../locale';
import { ChartRenderer, ChartRendererContext } from '../renderer';
import { createRendererSchema, getField, getSelectedFields } from '../utils';
import { getConfigSchema, querySchema, transformSchema } from './schemas/configure';
import { useChartTypes, useCharts, useDefaultChartType } from '../chart/library';
import { useChartTypes, useCharts, useDefaultChartType } from '../chart/group';
import { FilterDynamicComponent } from './FilterDynamicComponent';
const { Paragraph, Text } = Typography;

View File

@ -1,51 +1,28 @@
import { Plugin, SchemaComponentOptions, SchemaInitializerContext, SchemaInitializerProvider } from '@nocobase/client';
import React, { useContext } from 'react';
import { ChartInitializers, ChartV2Block, ChartV2BlockDesigner, ChartV2BlockInitializer } from './block';
import { useChartsTranslation } from './locale';
import { ChartRenderer, ChartRendererProvider } from './renderer';
import { ChartLibraryProvider } from './chart/library';
import { Plugin } from '@nocobase/client';
import { DataVisualization } from './DataVisualization';
import { ChartGroup } from './chart/group';
import g2plot from './chart/g2plot';
import antd from './chart/antd';
const DataVisualization: React.FC = (props) => {
const { t } = useChartsTranslation();
const initializers = useContext<any>(SchemaInitializerContext);
const children = initializers.BlockInitializers.items[0].children;
const has = children.some((initializer) => initializer.component === 'ChartV2BlockInitializer');
if (!has) {
children.push({
key: 'chart-v2',
type: 'item',
title: t('Charts'),
component: 'ChartV2BlockInitializer',
});
}
return (
<SchemaComponentOptions
components={{
ChartV2BlockInitializer,
ChartRenderer,
ChartV2BlockDesigner,
ChartV2Block,
ChartRendererProvider,
}}
>
<SchemaInitializerProvider initializers={{ ...initializers, ChartInitializers }}>
<ChartLibraryProvider name="Built-in" charts={[...g2plot, ...antd]}>
{props.children}
</ChartLibraryProvider>
</SchemaInitializerProvider>
</SchemaComponentOptions>
);
};
import { ChartV2Block, ChartV2BlockDesigner, ChartV2BlockInitializer } from './block';
import { ChartRenderer, ChartRendererProvider } from './renderer';
class DataVisualizationPlugin extends Plugin {
public charts: ChartGroup = new ChartGroup();
async load() {
this.charts.setGroup('Built-in', [...g2plot, ...antd]);
this.app.addComponents({
ChartV2BlockInitializer,
ChartRenderer,
ChartV2BlockDesigner,
ChartV2Block,
ChartRendererProvider,
});
this.app.addProvider(DataVisualization);
}
}
export default DataVisualizationPlugin;
export { ChartLibraryProvider };
export { Chart } from './chart/chart';
export type { ChartType } from './chart/chart';

View File

@ -14,8 +14,8 @@ import { ChartConfigContext } from '../configure/ChartConfigure';
import { useData, useFieldTransformer, useFieldsWithAssociation } from '../hooks';
import { useChartsTranslation } from '../locale';
import { createRendererSchema, getField } from '../utils';
import { useCharts } from '../chart/library';
import { ChartRendererContext } from './ChartRendererProvider';
import { useChart } from '../chart/group';
const { Paragraph, Text } = Typography;
export const ChartRenderer: React.FC & {
@ -30,8 +30,7 @@ export const ChartRenderer: React.FC & {
const advanced = config?.advanced || {};
const api = useAPIClient();
const charts = useCharts();
const chart = charts[config?.chartType];
const chart = useChart(config?.chartType);
const locale = api.auth.getLocale();
const transformers = useFieldTransformer(transform, locale);
const Component = chart?.render({

View File

@ -0,0 +1,32 @@
import { Migration } from '@nocobase/server';
import { Repository } from '@nocobase/database';
export default class RenameChartTypeMigration extends Migration {
async up() {
const result = await this.app.version.satisfies('<=0.14.0-alpha.7');
if (!result) {
return;
}
const r = this.db.getRepository<Repository>('uiSchemas');
const items = await r.find({
filter: {
'schema.x-decorator': 'ChartRendererProvider',
},
});
await this.db.sequelize.transaction(async (transaction) => {
for (const item of items) {
const schema = item.schema;
const chartType = schema['x-decorator-props']?.config?.chartType;
if (!chartType || chartType.startsWith('Built-in.')) {
continue;
}
schema['x-decorator-props'].config.chartType = `Built-in.${chartType}`;
item.set('schema', schema);
await item.save({ transaction });
}
});
}
}

View File

@ -1,6 +1,7 @@
import { Cache, createCache } from '@nocobase/cache';
import { InstallOptions, Plugin } from '@nocobase/server';
import { query } from './actions/query';
import { resolve } from 'path';
export class DataVisualizationPlugin extends Plugin {
cache: Cache;
@ -18,6 +19,14 @@ export class DataVisualizationPlugin extends Plugin {
}
async load() {
this.db.addMigrations({
namespace: 'data-visulization',
directory: resolve(__dirname, 'migrations'),
context: {
plugin: this,
},
});
this.cache = createCache({
ttl: 30, // seconds
max: 1000,