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:
parent
729fdd04b7
commit
ae988d00b0
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -1,4 +1,3 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { AntdChart } from './antd';
|
||||
import { Statistic as AntdStatistic } from 'antd';
|
||||
import { lang } from '../../locale';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
};
|
@ -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>;
|
||||
};
|
@ -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;
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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({
|
||||
|
@ -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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user