feat: add chart plugin (#1477)
* feat: add pie schema template * chore: refactor * chore: update * chore: init server * feat: add pie schema template * chore: add chart utils * chore: update * chore: update * chore: update * chore: update * feat: mvp * chore: update * chore: test * feat: example * fix: templates map error * chore: update * feat: add ChartBlockEngineDesigner.tsx * chore: clean code * chore: update templates * chore: init bat template * chore: update chart block engine * feat: add chart block engine designer * chore: update chart block initializer * chore: update pie template * chore: update * feat: split sql * chore: update * chore: clean * feat: support bar * chore: add bar template * chore: clean code * chore: clean code * fix: chart block initializer title * chore: update * fix: use title * chore: update * fix: bugs * chore: clean code * chore: update pie template * chore: update ChartBlockInitializer.tsx * chore: update * feat: support chart config * chore: add local plugin * feat: add column template * feat: add column chart action * feat: add line chart template * feat: add line action * feat: add area template * feat: add area action * chore: update pie template * chore: update * chore: update dep * fix: export missing utils * chore: update dep * chore: update dep * chore: update pie template * fix(charts): sql table capitalization * fix(charts): improve sql compatibility * fix: support choices type * chore: update * chore: clean code * chore: get collection fields in chart engine * feat(demo): chart block initializer * fix: hello block * chore: init data analyse plugin * chore: add data-analyse plugin * chore: init plugin * chore: update * chore: export schema initializers * feat: add data analyse block * chore: update data analyse block initializer * chore: rename plugin name * chore: update * chore: update enum * chore: update add block * feat: add pie schema template * chore: refactor * chore: update * chore: init server * feat: add pie schema template * chore: add chart utils * chore: update * chore: update * chore: update * chore: update * feat: mvp * chore: update * chore: test * feat: example * fix: templates map error * chore: update * feat: add ChartBlockEngineDesigner.tsx * chore: clean code * chore: update templates * chore: init bat template * chore: update chart block engine * feat: add chart block engine designer * chore: update chart block initializer * chore: update pie template * chore: update * feat: split sql * chore: update * chore: clean * feat: support bar * chore: add bar template * chore: clean code * chore: clean code * fix: chart block initializer title * chore: update * fix: use title * chore: update * fix: bugs * chore: clean code * chore: update pie template * chore: update ChartBlockInitializer.tsx * chore: update * feat: support chart config * chore: add local plugin * feat: add column template * feat: add column chart action * feat: add line chart template * feat: add line action * feat: add area template * feat: add area action * chore: update pie template * chore: update * chore: update dep * fix: export missing utils * chore: update dep * chore: update dep * chore: update pie template * fix(charts): sql table capitalization * fix(charts): improve sql compatibility * fix: support choices type * chore: update * chore: clean code * chore: get collection fields in chart engine * chore: init dataset plugin * chore: init dataset designer * chore: init dataset block initializer * chore: update * chore: update * chore: update deps * chore: add tests * chore: update utils * chore: init plugin * feat: add datasets client * chore: update tests * chore: update datastructures * chore: update * feat: add DataSetBlockInitializer * chore: use dataset initializer * chore: update deps * chore: update * chore: update pie template * chore: update ChartBlockInitializer * chore: update ChartBlockEngine * chore: remove mdg file * chore: update ChartBlockEngine * chore: update * chore: update g2plot * chore: update template * chore: update pie handler * chore: update DataSetPreviewTable * chore: update ChartBlockEngine * chore: update ChartBlockEngineDesigner * chore: remove useless files * chore: update ChartBlockInitializer * chore: update template * chore: feat area template * chore: update tests * chore: update local plugins * chore: update deps * chore: update templates * chore: update template * feat: add CustomSelect * chore: update select component * fix: seriesField color error * chore: update template * chore: update * fix: bar * chore: update templates & clean code * chore: update templates * feat: add FunnelTemplate * feat: charts queries * fix: data shift * feat: add ChartQueryMetadataProvider * chore: update * chore: remove dataset * chore: update * chore: update * chore: clean code * fix: delete query query list wrong behavior * fix: delete all queries query list behavior * chore: add empty occupancy * chore: update description * chore: update chart icon * chore: update funnel template * chore: update template title * chore: add references * chore: update dialog height * fix: scatter template * chore: update popup description * refactor: schema level * refactor: charts schema * chore: remove console * chore: remove console * fix: error message * chore: remove console * chore: update DataSetPreviewTable * feat: chart icons * feat: update i18n * feat: update i18n * chore: update i18n * chore: update i18n * chore: update i18n * feat: add tittle * chore: update icons * chore: update i18n * chore: update i18n * chore: update i18n * chore: update i18n * chore: update i18n * fix: improve code * chore: update i18n * chore: update i18n * fix: chart icon * feat: quick add query (#1527) * feat: quick add query * feat: query * chore: remove useless files * fix: json5 validator error * chore: update * chore: update style * chore: lint * feat: improve code * feat: sql validation * refactor: clean code * Update utils.ts * chore: format code * chore: remove useless file * chore: update --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
555378c342
commit
b276ce1258
1
packages/app/client/src/plugins/charts.ts
Normal file
1
packages/app/client/src/plugins/charts.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-charts/client';
|
@ -33,7 +33,7 @@ import {
|
||||
Venn,
|
||||
Violin,
|
||||
Waterfall,
|
||||
WordCloud
|
||||
WordCloud,
|
||||
} from '@antv/g2plot';
|
||||
import { Field } from '@formily/core';
|
||||
import { observer, useField } from '@formily/react';
|
||||
@ -90,7 +90,6 @@ const plots = {
|
||||
|
||||
export const G2PlotRenderer = forwardRef(function <O = any>(props: ReactG2PlotProps<O>, ref: any) {
|
||||
const { className, plot, config } = props;
|
||||
|
||||
const containerRef = useRef(undefined);
|
||||
const plotRef = useRef(undefined);
|
||||
|
||||
@ -107,7 +106,7 @@ export const G2PlotRenderer = forwardRef(function <O = any>(props: ReactG2PlotPr
|
||||
plotRef.current.update(config);
|
||||
} else {
|
||||
plotRef.current = new plot(containerRef.current, config);
|
||||
plotRef.current.render();
|
||||
plotRef?.current?.render();
|
||||
}
|
||||
|
||||
syncRef(plotRef, ref);
|
||||
@ -122,7 +121,6 @@ export const G2PlotRenderer = forwardRef(function <O = any>(props: ReactG2PlotPr
|
||||
|
||||
useEffect(() => {
|
||||
renderPlot();
|
||||
|
||||
return () => destoryPlot();
|
||||
}, [config, plot]);
|
||||
|
||||
@ -154,6 +152,7 @@ export const G2Plot: any = observer((props: any) => {
|
||||
field.data.loading = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!plot || !config) {
|
||||
return <div style={{ opacity: 0.3 }}>{t('In configuration')}...</div>;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ export const Tabs: any = observer((props: TabsProps) => {
|
||||
return (
|
||||
<DndContext>
|
||||
<AntdTabs
|
||||
style={props.style}
|
||||
tabBarExtraContent={{
|
||||
right: render(),
|
||||
}}
|
||||
|
@ -191,7 +191,11 @@ SchemaInitializer.Item = (props: SchemaInitializerItemProps) => {
|
||||
key={item.key}
|
||||
onClick={(info) => {
|
||||
item?.clearKeywords?.();
|
||||
if (item.onClick) {
|
||||
item.onClick({ ...info, item });
|
||||
} else {
|
||||
onClick({ ...info, item });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{compile(item.title)}
|
||||
|
@ -44,115 +44,6 @@ export const BlockInitializers = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'g2plot',
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Chart blocks")}}',
|
||||
children: [
|
||||
{
|
||||
key: 'column',
|
||||
type: 'item',
|
||||
title: '{{t("Column chart")}}',
|
||||
component: 'G2PlotInitializer',
|
||||
icon: 'BarChartOutlined',
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-designer': 'G2Plot.Designer',
|
||||
'x-decorator': 'CardItem',
|
||||
'x-component': 'G2Plot',
|
||||
'x-component-props': {
|
||||
plot: 'Column',
|
||||
config: chartConfig.column,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'bar',
|
||||
type: 'item',
|
||||
title: '{{t("Bar chart")}}',
|
||||
component: 'G2PlotInitializer',
|
||||
icon: 'BarChartOutlined',
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-designer': 'G2Plot.Designer',
|
||||
'x-decorator': 'CardItem',
|
||||
'x-component': 'G2Plot',
|
||||
'x-component-props': {
|
||||
plot: 'Bar',
|
||||
config: chartConfig.bar,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'line',
|
||||
type: 'item',
|
||||
title: '{{t("Line chart")}}',
|
||||
component: 'G2PlotInitializer',
|
||||
icon: 'LineChartOutlined',
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-designer': 'G2Plot.Designer',
|
||||
'x-decorator': 'CardItem',
|
||||
'x-component': 'G2Plot',
|
||||
'x-component-props': {
|
||||
plot: 'Line',
|
||||
config: chartConfig.line,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'pie',
|
||||
type: 'item',
|
||||
title: '{{t("Pie chart")}}',
|
||||
component: 'G2PlotInitializer',
|
||||
icon: 'PieChartOutlined',
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-designer': 'G2Plot.Designer',
|
||||
'x-decorator': 'CardItem',
|
||||
'x-component': 'G2Plot',
|
||||
'x-component-props': {
|
||||
plot: 'Pie',
|
||||
config: chartConfig.pie,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'area',
|
||||
type: 'item',
|
||||
title: '{{t("Area chart")}}',
|
||||
component: 'G2PlotInitializer',
|
||||
icon: 'AreaChartOutlined',
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-designer': 'G2Plot.Designer',
|
||||
'x-decorator': 'CardItem',
|
||||
'x-component': 'G2Plot',
|
||||
'x-component-props': {
|
||||
plot: 'Area',
|
||||
config: chartConfig.area,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'other',
|
||||
type: 'item',
|
||||
title: '{{t("Other chart")}}',
|
||||
component: 'G2PlotInitializer',
|
||||
icon: 'AreaChartOutlined',
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-designer': 'G2Plot.Designer',
|
||||
'x-decorator': 'CardItem',
|
||||
'x-component': 'G2Plot',
|
||||
'x-component-props': {
|
||||
// plot: 'Area',
|
||||
// config: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'media',
|
||||
type: 'itemGroup',
|
||||
|
@ -1,3 +1,8 @@
|
||||
export * from '../../schema-component/antd/association-filter/ActionBarAssociationFilterAction';
|
||||
// association filter
|
||||
export * from '../../schema-component/antd/association-filter/AssociationFilter';
|
||||
export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDelete';
|
||||
export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDisplayField';
|
||||
export * from './ActionInitializer';
|
||||
export * from './BlockInitializer';
|
||||
export * from './BulkDestroyActionInitializer';
|
||||
@ -37,8 +42,3 @@ export * from './TableSelectorInitializer';
|
||||
export * from './UpdateActionInitializer';
|
||||
export * from './UpdateSubmitActionInitializer';
|
||||
export * from './ViewActionInitializer';
|
||||
// association filter
|
||||
export * from '../../schema-component/antd/association-filter/AssociationFilter';
|
||||
export * from '../../schema-component/antd/association-filter/ActionBarAssociationFilterAction';
|
||||
export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDisplayField';
|
||||
export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDelete';
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
Sequelize,
|
||||
SyncOptions,
|
||||
Transactionable,
|
||||
Utils,
|
||||
Utils
|
||||
} from 'sequelize';
|
||||
import { SequelizeStorage, Umzug } from 'umzug';
|
||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
||||
@ -58,7 +58,7 @@ import {
|
||||
SyncListener,
|
||||
UpdateListener,
|
||||
UpdateWithAssociationsListener,
|
||||
ValidateListener,
|
||||
ValidateListener
|
||||
} from './types';
|
||||
import { patchSequelizeQueryInterface, snakeCase } from './utils';
|
||||
|
||||
@ -374,6 +374,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
return dialect.includes(this.sequelize.getDialect());
|
||||
}
|
||||
|
||||
escapeId(identifier: string) {
|
||||
return this.inDialect('mysql') ? `\`${identifier}\`` : `"${identifier}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add collection to database
|
||||
* @param options
|
||||
|
4
packages/plugins/charts/client.d.ts
vendored
Executable file
4
packages/plugins/charts/client.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/client';
|
||||
export { default } from './lib/client';
|
||||
|
30
packages/plugins/charts/client.js
Executable file
30
packages/plugins/charts/client.js
Executable file
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
||||
|
||||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
||||
|
||||
var _index = _interopRequireWildcard(require("./lib/client"));
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
var _exportNames = {};
|
||||
Object.defineProperty(exports, "default", {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index.default;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(_index).forEach(function (key) {
|
||||
if (key === "default" || key === "__esModule") return;
|
||||
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
||||
if (key in exports && exports[key] === _index[key]) return;
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index[key];
|
||||
}
|
||||
});
|
||||
});
|
13
packages/plugins/charts/package.json
Normal file
13
packages/plugins/charts/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-charts",
|
||||
"version": "0.9.1-alpha.1",
|
||||
"main": "lib/server/index.js",
|
||||
"devDependencies": {
|
||||
"@nocobase/server": "0.9.1-alpha.1",
|
||||
"@nocobase/test": "0.9.1-alpha.1",
|
||||
"@nocobase/client": "^0.9.1-alpha.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"json5": "^2.2.3"
|
||||
}
|
||||
}
|
4
packages/plugins/charts/server.d.ts
vendored
Executable file
4
packages/plugins/charts/server.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/server';
|
||||
export { default } from './lib/server';
|
||||
|
30
packages/plugins/charts/server.js
Executable file
30
packages/plugins/charts/server.js
Executable file
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
||||
|
||||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
||||
|
||||
var _index = _interopRequireWildcard(require("./lib/server"));
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
var _exportNames = {};
|
||||
Object.defineProperty(exports, "default", {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index.default;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(_index).forEach(function (key) {
|
||||
if (key === "default" || key === "__esModule") return;
|
||||
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
||||
if (key in exports && exports[key] === _index[key]) return;
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index[key];
|
||||
}
|
||||
});
|
||||
});
|
120
packages/plugins/charts/src/client/ChartBlockEngine.tsx
Normal file
120
packages/plugins/charts/src/client/ChartBlockEngine.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { useCompile, useRequest } from '@nocobase/client';
|
||||
import { Empty, Spin } from 'antd';
|
||||
import JSON5 from 'json5';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ChartBlockEngineDesigner } from './ChartBlockEngineDesigner';
|
||||
import chartRenderComponentsMap from './chartRenderComponents';
|
||||
import { lang } from './locale';
|
||||
import { templates } from './templates';
|
||||
|
||||
export interface IQueryConfig {
|
||||
id: number;
|
||||
}
|
||||
export interface IChartConfig {
|
||||
type: string;
|
||||
template: string;
|
||||
metric: string;
|
||||
dimension: string;
|
||||
category?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ChartBlockEngineMetaData {
|
||||
query: IQueryConfig;
|
||||
chart: IChartConfig;
|
||||
}
|
||||
|
||||
const ChartRenderComponent = ({
|
||||
chartBlockEngineMetaData,
|
||||
}: {
|
||||
chartBlockEngineMetaData: ChartBlockEngineMetaData;
|
||||
}): JSX.Element => {
|
||||
const compile = useCompile();
|
||||
const chartType = chartBlockEngineMetaData.chart.type;
|
||||
const renderComponent = templates.get(chartType)?.renderComponent;
|
||||
const RenderComponent = chartRenderComponentsMap.get(renderComponent); //G2Plot | Echarts | D3 |Table
|
||||
const chartConfig = chartBlockEngineMetaData.chart;
|
||||
const { loading, dataSet, error } = useGetDataSet(chartBlockEngineMetaData.query.id);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<Empty description={<span>May be this chart block's query data has been deleted,please check!</span>} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const [currentConfig, setCurrentConfig] = useState<IChartConfig>({} as any);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentConfig(chartConfig);
|
||||
}, [JSON.stringify(chartConfig)]);
|
||||
|
||||
if (currentConfig.type !== chartConfig.type) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
switch (renderComponent) {
|
||||
case 'G2Plot': {
|
||||
let finalChartOptions;
|
||||
finalChartOptions = templates.get(chartType)?.defaultChartOptions;
|
||||
let template;
|
||||
try {
|
||||
template = JSON5.parse(chartConfig?.template);
|
||||
} catch (e) {
|
||||
template = {};
|
||||
}
|
||||
const config = compile(
|
||||
{
|
||||
...finalChartOptions,
|
||||
...template,
|
||||
data: dataSet,
|
||||
},
|
||||
{ ...chartConfig, category: chartConfig?.category ?? '' },
|
||||
);
|
||||
if (config && chartConfig) {
|
||||
const { dimension, metric, category } = chartConfig;
|
||||
if (!metric || !dimension) {
|
||||
return <>{lang('Please check the chart config')}</>;
|
||||
}
|
||||
}
|
||||
return <>{loading ? <Spin /> : <RenderComponent plot={chartConfig.type} config={config} />}</>;
|
||||
}
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export const useGetDataSet = (chartQueryId: number) => {
|
||||
const { data, loading, error } = useRequest({
|
||||
url: `/chartsQueries:getData/${chartQueryId}`,
|
||||
});
|
||||
const dataSet = data?.data;
|
||||
return {
|
||||
loading,
|
||||
dataSet: dataSet,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
const ChartBlockEngine = ({ chartBlockEngineMetaData }: { chartBlockEngineMetaData: ChartBlockEngineMetaData }) => {
|
||||
let renderComponent;
|
||||
const chartType = chartBlockEngineMetaData?.chart?.type;
|
||||
|
||||
if (chartType) {
|
||||
renderComponent = templates.get(chartType)?.renderComponent;
|
||||
}
|
||||
|
||||
if (!chartType || !renderComponent) {
|
||||
return <>{lang('Please check the chart config')}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChartRenderComponent chartBlockEngineMetaData={chartBlockEngineMetaData} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ChartBlockEngine.Designer = ChartBlockEngineDesigner;
|
||||
|
||||
export { ChartBlockEngine };
|
238
packages/plugins/charts/src/client/ChartBlockEngineDesigner.tsx
Normal file
238
packages/plugins/charts/src/client/ChartBlockEngineDesigner.tsx
Normal file
@ -0,0 +1,238 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { FormDialog, FormLayout } from '@formily/antd';
|
||||
import { SchemaOptionsContext, useField, useFieldSchema } from '@formily/react';
|
||||
import {
|
||||
APIClientProvider,
|
||||
GeneralSchemaDesigner,
|
||||
i18n,
|
||||
SchemaComponent,
|
||||
SchemaComponentOptions,
|
||||
SchemaSettings,
|
||||
useAPIClient,
|
||||
useCompile,
|
||||
useDesignable,
|
||||
} from '@nocobase/client';
|
||||
import { Card } from 'antd';
|
||||
import JSON5 from 'json5';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChartBlockEngineMetaData } from './ChartBlockEngine';
|
||||
import { Options } from './ChartBlockInitializer';
|
||||
import DataSetPreviewTable from './DataSetPreviewTable';
|
||||
import { useFieldsById } from './hooks';
|
||||
import { lang } from './locale';
|
||||
import { templates } from './templates';
|
||||
|
||||
export const jsonConfigDesc = (title: string, link: string) => {
|
||||
return (
|
||||
<span>
|
||||
{lang('Json config references: ')}
|
||||
<a href={link} target="_blank">
|
||||
{lang(title)}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const validateJSON = {
|
||||
validator: `{{(value, rule)=> {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const val = JSON.parse(value);
|
||||
if(!isNaN(val)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
}}}`,
|
||||
message: '{{t("Invalid JSON format",{ ns: "charts" })}}',
|
||||
};
|
||||
|
||||
export const ChartBlockEngineDesigner = () => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { chartBlockEngineMetaData } = fieldSchema?.['x-component-props'];
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
<ChartBlockEngineDesignerInitializer chartBlockEngineMetaData={chartBlockEngineMetaData} />
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
'x-component': 'Grid',
|
||||
}}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChartBlockEngineDesignerInitializer = (props) => {
|
||||
const { chartBlockEngineMetaData }: { chartBlockEngineMetaData: ChartBlockEngineMetaData } = props;
|
||||
const { t } = useTranslation();
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const { dn } = useDesignable();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const api = useAPIClient();
|
||||
const field = useField();
|
||||
const compile = useCompile();
|
||||
const { chart, query } = chartBlockEngineMetaData;
|
||||
const { fields } = useFieldsById(query.id);
|
||||
const dataSource = fields.map((field) => {
|
||||
return {
|
||||
label: field.name,
|
||||
value: field.name,
|
||||
};
|
||||
});
|
||||
return (
|
||||
<SchemaSettings.Item
|
||||
onClick={async () => {
|
||||
FormDialog(
|
||||
{
|
||||
okText: compile('{{t("Submit")}}'),
|
||||
title: lang('Edit chart block'),
|
||||
width: 1200,
|
||||
bodyStyle: { background: '#f0f2f5', maxHeight: '65vh', overflow: 'auto' },
|
||||
},
|
||||
(form) => {
|
||||
const [chartBlockEngineMetaData, setChartBlockEngineMetaData] = useState<ChartBlockEngineMetaData>(null);
|
||||
useEffect(() => {
|
||||
const chartBlockEngineMetaData = {
|
||||
query: {
|
||||
id: query?.id,
|
||||
},
|
||||
chart: form.values, //TODO
|
||||
};
|
||||
setChartBlockEngineMetaData(chartBlockEngineMetaData);
|
||||
}, [form.values.type]);
|
||||
return (
|
||||
<APIClientProvider apiClient={api}>
|
||||
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
|
||||
<section
|
||||
className={css`
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
`}
|
||||
>
|
||||
{/* left*/}
|
||||
<Card
|
||||
bordered={false}
|
||||
title={i18n.t('Chart config')}
|
||||
size={'default'}
|
||||
className={css`
|
||||
flex: 1;
|
||||
`}
|
||||
>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaComponent
|
||||
scope={{ dataSource, JSON5, jsonConfigDesc }}
|
||||
components={{ Options }}
|
||||
schema={{
|
||||
properties: {
|
||||
// title: {
|
||||
// title: lang('Chart title'),
|
||||
// 'x-component': 'Input',
|
||||
// 'x-decorator': 'FormItem',
|
||||
// },
|
||||
type: {
|
||||
title: t('Chart type'),
|
||||
required: true,
|
||||
'x-component': 'CustomSelect',
|
||||
'x-decorator': 'FormItem',
|
||||
enum: [...templates.values()].map((template) => {
|
||||
return {
|
||||
title: template.title,
|
||||
key: template.type,
|
||||
description: template.description,
|
||||
group: template.group,
|
||||
iconId: template.iconId,
|
||||
};
|
||||
}),
|
||||
},
|
||||
options: {
|
||||
type: 'void',
|
||||
'x-component': 'Options',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
</Card>
|
||||
{/* right*/}
|
||||
<div
|
||||
className={css`
|
||||
flex: 1;
|
||||
min-width: 600px;
|
||||
`}
|
||||
>
|
||||
<Card size={'default'} title={lang('Chart preview')}>
|
||||
{/* Chart Preview*/}
|
||||
{chartBlockEngineMetaData && (
|
||||
<>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
properties: {
|
||||
chartPreview: {
|
||||
type: 'void',
|
||||
'x-decorator': 'CardItem',
|
||||
'x-component': 'ChartBlockEngine',
|
||||
'x-component-props': {
|
||||
chartBlockEngineMetaData: chartBlockEngineMetaData,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
<Card
|
||||
size={'default'}
|
||||
title={lang('Data preview')}
|
||||
className={css`
|
||||
margin-top: 24px;
|
||||
overflow: scroll;
|
||||
`}
|
||||
>
|
||||
{/*Data preview*/}
|
||||
{chartBlockEngineMetaData?.query?.id && (
|
||||
<DataSetPreviewTable queryId={chartBlockEngineMetaData?.query?.id} fields={fields} />
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</SchemaComponentOptions>
|
||||
</APIClientProvider>
|
||||
);
|
||||
},
|
||||
)
|
||||
.open({
|
||||
initialValues: { ...chart }, //reset before chartBlockMetaData
|
||||
})
|
||||
.then((values) => {
|
||||
//patch updates
|
||||
values = {
|
||||
query,
|
||||
chart: values,
|
||||
};
|
||||
field.title = values.chart.title;
|
||||
fieldSchema['title'] = values.chart.title;
|
||||
field.componentProps.chartBlockEngineMetaData = values;
|
||||
fieldSchema['x-component-props'].chartBlockEngineMetaData = values;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
'x-component-props': fieldSchema['x-component-props'],
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
});
|
||||
}}
|
||||
>
|
||||
{props.children || props.title || lang('Edit chart block')}
|
||||
</SchemaSettings.Item>
|
||||
);
|
||||
};
|
216
packages/plugins/charts/src/client/ChartBlockInitializer.tsx
Normal file
216
packages/plugins/charts/src/client/ChartBlockInitializer.tsx
Normal file
@ -0,0 +1,216 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { FormDialog, FormLayout } from '@formily/antd';
|
||||
import { Field } from '@formily/core';
|
||||
import { observer, RecursionField, Schema, SchemaOptionsContext, useField, useForm } from '@formily/react';
|
||||
import {
|
||||
APIClientProvider,
|
||||
FormProvider,
|
||||
SchemaComponent,
|
||||
SchemaComponentOptions,
|
||||
useAPIClient,
|
||||
useCompile,
|
||||
} from '@nocobase/client';
|
||||
import { Card } from 'antd';
|
||||
import JSON5 from 'json5';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { ChartBlockEngineMetaData } from './ChartBlockEngine';
|
||||
import { jsonConfigDesc } from './ChartBlockEngineDesigner';
|
||||
import { ChartQueryBlockInitializer, ChartQueryMetadata } from './ChartQueryBlockInitializer';
|
||||
import DataSetPreviewTable from './DataSetPreviewTable';
|
||||
import { lang, useChartsTranslation } from './locale';
|
||||
import { templates } from './templates';
|
||||
|
||||
export const Options = observer((props) => {
|
||||
const form = useForm<ChartFormInterface>();
|
||||
const field = useField<Field>();
|
||||
const [s, setSchema] = useState(new Schema({}));
|
||||
const [chartType, setChartType] = useState(form.values.type);
|
||||
useEffect(() => {
|
||||
// form.clearFormGraph('options.*');
|
||||
setChartType(form?.values?.type);
|
||||
if (chartType !== form?.values?.type) {
|
||||
form.clearFormGraph('options.*');
|
||||
}
|
||||
if (form.values.type) {
|
||||
const template = templates.get(form.values.type);
|
||||
setSchema(new Schema(template.configurableProperties || {}));
|
||||
}
|
||||
}, [form.values.type]);
|
||||
return <RecursionField schema={s} />;
|
||||
});
|
||||
|
||||
interface ChartFormInterface {
|
||||
type: string;
|
||||
template: string;
|
||||
metric: string;
|
||||
dimension: string;
|
||||
category?: string;
|
||||
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export const ChartBlockInitializer = (props) => {
|
||||
const { insert } = props;
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const api = useAPIClient();
|
||||
const compile = useCompile();
|
||||
const { t } = useChartsTranslation();
|
||||
return (
|
||||
<ChartQueryBlockInitializer
|
||||
{...props}
|
||||
componentType={'Charts'}
|
||||
onCreateBlockSchema={async ({ item: chartQueryMetadata }: { item: ChartQueryMetadata }) => {
|
||||
const dataSource = chartQueryMetadata?.fields.map((field) => {
|
||||
return {
|
||||
label: field.name,
|
||||
value: field.name,
|
||||
};
|
||||
});
|
||||
let values = await FormDialog(
|
||||
{
|
||||
okText: compile('{{t("Submit")}}'),
|
||||
title: lang('Create chart block'),
|
||||
width: 1200,
|
||||
bodyStyle: { background: '#f0f2f5', maxHeight: '65vh', overflow: 'auto' },
|
||||
},
|
||||
() => {
|
||||
const form = useForm<ChartFormInterface>();
|
||||
const [chartBlockEngineMetaData, setChartBlockEngineMetaData] = useState<ChartBlockEngineMetaData>(null);
|
||||
useEffect(() => {
|
||||
const chartBlockEngineMetaData = {
|
||||
query: {
|
||||
id: chartQueryMetadata?.id,
|
||||
},
|
||||
chart: form.values, //TODO
|
||||
};
|
||||
setChartBlockEngineMetaData(chartBlockEngineMetaData);
|
||||
}, [form.values.type]);
|
||||
return (
|
||||
<APIClientProvider apiClient={api}>
|
||||
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
|
||||
<section
|
||||
className={css`
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
`}
|
||||
>
|
||||
{/* left*/}
|
||||
<Card
|
||||
title={lang('Chart config')}
|
||||
size={'default'}
|
||||
className={css`
|
||||
flex: 1;
|
||||
`}
|
||||
>
|
||||
<FormProvider form={form}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaComponent
|
||||
scope={{ dataSource, JSON5, jsonConfigDesc }}
|
||||
components={{ Options }}
|
||||
schema={{
|
||||
properties: {
|
||||
// title: {
|
||||
// title: lang('Chart title'),
|
||||
// 'x-component': 'Input',
|
||||
// 'x-decorator': 'FormItem',
|
||||
// },
|
||||
type: {
|
||||
title: lang('Chart type'),
|
||||
required: true,
|
||||
'x-component': 'CustomSelect',
|
||||
'x-decorator': 'FormItem',
|
||||
enum: [...templates.values()].map((template) => {
|
||||
return {
|
||||
title: template.title,
|
||||
key: template.type,
|
||||
description: template.description,
|
||||
group: template.group,
|
||||
iconId: template.iconId,
|
||||
};
|
||||
}),
|
||||
},
|
||||
options: {
|
||||
type: 'void',
|
||||
'x-component': 'Options',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
</FormProvider>
|
||||
</Card>
|
||||
{/* right*/}
|
||||
<div
|
||||
className={css`
|
||||
flex: 1;
|
||||
min-width: 600px;
|
||||
`}
|
||||
>
|
||||
<Card size={'default'} title={lang('Chart preview')}>
|
||||
{/* Chart Preview*/}
|
||||
{chartBlockEngineMetaData && (
|
||||
<>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
properties: {
|
||||
chartPreview: {
|
||||
type: 'void',
|
||||
'x-decorator': 'CardItem',
|
||||
'x-component': 'ChartBlockEngine',
|
||||
'x-component-props': {
|
||||
chartBlockEngineMetaData,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
<Card
|
||||
size={'default'}
|
||||
title={lang('Data preview')}
|
||||
className={css`
|
||||
margin-top: 24px;
|
||||
overflow: scroll;
|
||||
`}
|
||||
>
|
||||
{/*Data preview*/}
|
||||
{chartBlockEngineMetaData?.query?.id && (
|
||||
<DataSetPreviewTable
|
||||
queryId={chartBlockEngineMetaData?.query?.id}
|
||||
fields={chartQueryMetadata?.fields}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</SchemaComponentOptions>
|
||||
</APIClientProvider>
|
||||
);
|
||||
},
|
||||
).open({
|
||||
initialValues: {},
|
||||
});
|
||||
if (values) {
|
||||
const chartBlockEngineMetaData: ChartBlockEngineMetaData = {
|
||||
query: {
|
||||
id: chartQueryMetadata.id,
|
||||
},
|
||||
chart: values,
|
||||
};
|
||||
insert({
|
||||
type: 'void',
|
||||
title: values?.title,
|
||||
'x-designer': 'ChartBlockEngine.Designer',
|
||||
'x-decorator': 'CardItem',
|
||||
'x-component': 'ChartBlockEngine',
|
||||
'x-component-props': {
|
||||
chartBlockEngineMetaData,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,136 @@
|
||||
import { TableOutlined } from '@ant-design/icons';
|
||||
import { FormDialog, FormLayout } from '@formily/antd';
|
||||
import { SchemaOptionsContext } from '@formily/react';
|
||||
import {
|
||||
SchemaComponent,
|
||||
SchemaComponentOptions,
|
||||
SchemaInitializer,
|
||||
SchemaInitializerButtonContext,
|
||||
useAPIClient,
|
||||
} from '@nocobase/client';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useChartQueryMetadataContext } from './ChartQueryMetadataProvider';
|
||||
import { lang } from './locale';
|
||||
import { getQueryTypeSchema } from './settings/queryTypes';
|
||||
|
||||
export interface ChartQueryMetadata {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
fields: { name: string }[];
|
||||
}
|
||||
|
||||
export const ChartQueryBlockInitializer = (props) => {
|
||||
const defaultItems: any = [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: lang('Select query data'),
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
const { templateWrap, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props;
|
||||
const { setVisible } = useContext(SchemaInitializerButtonContext);
|
||||
const [items, setItems] = useState(defaultItems);
|
||||
const apiClient = useAPIClient();
|
||||
const ctx = useChartQueryMetadataContext();
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const onAddQuery = (info) => {
|
||||
FormDialog(
|
||||
{
|
||||
sql: lang('Add SQL query'),
|
||||
json: lang('Add JSON query'),
|
||||
}[info.key],
|
||||
() => {
|
||||
return (
|
||||
<div>
|
||||
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: {
|
||||
title: lang('Title'),
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
options: getQueryTypeSchema(info.key),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
</SchemaComponentOptions>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)
|
||||
.open({
|
||||
initialValues: {
|
||||
type: info.key,
|
||||
},
|
||||
})
|
||||
.then(async (values) => {
|
||||
try {
|
||||
const { data } = await apiClient.resource('chartsQueries')?.create?.({ values });
|
||||
const items = (await ctx.refresh()) as any;
|
||||
const item = items.find((item) => item.id === data?.data?.id);
|
||||
onCreateBlockSchema({ item });
|
||||
setVisible(false);
|
||||
} catch (error) {}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
useEffect(() => {
|
||||
const chartQueryMetadata = ctx.data;
|
||||
if (chartQueryMetadata && Array.isArray(chartQueryMetadata)) {
|
||||
setItems(
|
||||
[
|
||||
chartQueryMetadata.length > 0
|
||||
? {
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Select chart query", {ns: "charts"})}}',
|
||||
children: chartQueryMetadata,
|
||||
}
|
||||
: null,
|
||||
chartQueryMetadata.length > 0
|
||||
? {
|
||||
type: 'divider',
|
||||
}
|
||||
: null,
|
||||
,
|
||||
{
|
||||
type: 'subMenu',
|
||||
title: lang('Add chart query'),
|
||||
// component: AddChartQuery,
|
||||
children: [
|
||||
{
|
||||
key: 'sql',
|
||||
type: 'item',
|
||||
title: 'SQL',
|
||||
onClick: onAddQuery,
|
||||
},
|
||||
{
|
||||
key: 'json',
|
||||
type: 'item',
|
||||
title: 'JSON',
|
||||
onClick: onAddQuery,
|
||||
},
|
||||
],
|
||||
},
|
||||
].filter(Boolean),
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<SchemaInitializer.Item
|
||||
icon={<TableOutlined />}
|
||||
{...others}
|
||||
onClick={async ({ item }) => {
|
||||
onCreateBlockSchema({ item });
|
||||
setVisible(false);
|
||||
}}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
import { useAPIClient, useRequest } from '@nocobase/client';
|
||||
import { Spin } from 'antd';
|
||||
import React, { createContext, useContext, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const ChartQueryMetadataContext = createContext({
|
||||
refresh: () => {},
|
||||
data: [] as any[],
|
||||
});
|
||||
|
||||
export const ChartQueryMetadataProvider: React.FC = (props) => {
|
||||
const api = useAPIClient();
|
||||
|
||||
const options = {
|
||||
resource: 'chartsQueries',
|
||||
action: 'listMetadata',
|
||||
params: {
|
||||
paginate: false,
|
||||
sort: ['-id'],
|
||||
},
|
||||
};
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const service = useRequest(options, {
|
||||
manual: true,
|
||||
});
|
||||
|
||||
const isAdminPage = location.pathname.startsWith('/admin');
|
||||
const token = api.auth.getToken() || '';
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdminPage && token) {
|
||||
service.run();
|
||||
}
|
||||
}, [isAdminPage, token]);
|
||||
|
||||
const refresh = async () => {
|
||||
const { data } = await api.request(options);
|
||||
service.mutate(data);
|
||||
return data?.data || [];
|
||||
};
|
||||
|
||||
if (service.loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartQueryMetadataContext.Provider
|
||||
value={{
|
||||
refresh,
|
||||
data: service.data?.data,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</ChartQueryMetadataContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useChartQueryMetadataContext = () => {
|
||||
return useContext(ChartQueryMetadataContext);
|
||||
};
|
73
packages/plugins/charts/src/client/DataSetPreviewTable.tsx
Normal file
73
packages/plugins/charts/src/client/DataSetPreviewTable.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import {
|
||||
BlockSchemaComponentProvider,
|
||||
FormItem,
|
||||
Input,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
TableV2,
|
||||
} from '@nocobase/client';
|
||||
import { Empty, Spin } from 'antd';
|
||||
import React from 'react';
|
||||
import { useGetDataSet } from './ChartBlockEngine';
|
||||
|
||||
export default ({ queryId, fields }: { queryId: number; fields }) => {
|
||||
const { dataSet, loading, error } = useGetDataSet(queryId);
|
||||
const columns = {};
|
||||
if (fields) {
|
||||
for (const field of fields) {
|
||||
columns[field.name] = {
|
||||
type: 'void',
|
||||
title: field.name,
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-component-props': {
|
||||
// width: 200,
|
||||
},
|
||||
properties: {
|
||||
[field.name]: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
properties: {
|
||||
input: {
|
||||
type: 'array',
|
||||
'x-component': 'TableV2',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
scroll: { y: 300 },
|
||||
},
|
||||
default: dataSet,
|
||||
properties: columns,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<Empty description={<span>May be this chart block's query data has been deleted,please check!</span>} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
<>
|
||||
<Spin />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<SchemaComponentProvider scope={{ dataSet }} components={{ TableV2, Input, FormItem }}>
|
||||
<BlockSchemaComponentProvider>
|
||||
<SchemaComponent schema={schema} />
|
||||
</BlockSchemaComponentProvider>
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
};
|
99
packages/plugins/charts/src/client/Icons.tsx
Normal file
99
packages/plugins/charts/src/client/Icons.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import { Icon } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
const RadarChart = () => (
|
||||
<svg width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" viewBox="0 0 1024 1024">
|
||||
<path d="M523 573.5c-3.9 0.7-7.9 1.1-12 1.1-5.3 0-10.4-0.6-15.2-1.8l-62.2 189.7 167.4-76L523 573.5z"></path>
|
||||
<path d="M398.3 767.3l68.9-210.1c-6.4-6-11.5-13.3-15-21.4l-192.6 52.7L398.3 767.3z"></path>
|
||||
<path d="M528 448.9c4.1 1.1 8 2.6 11.7 4.5l72.5-61.8L528 263.7 528 448.9z"></path>
|
||||
<path d="M259.6 555.4 447.3 504c0.4-4.2 1.3-8.3 2.5-12.2L270.9 373.4 259.6 555.4z"></path>
|
||||
<path d="M655.5 396.7l208.3 185.7 30.2 4.8c4.8-24.6 7.4-49.9 7.4-75.9 0-95.9-34.6-183.7-92-251.6L655.5 396.7z"></path>
|
||||
<path d="M794.4 563.4 630.9 417.7l-67 57.1c5.6 8.3 9.3 18 10.6 28.4L794.4 563.4z"></path>
|
||||
<path d="M496 448.4 496 239.6l-218.8 99.6 189.4 125.4C474.7 456.8 484.8 451.1 496 448.4z"></path>
|
||||
<path d="M619.3 713.3 420 803.8l-19.1 81.6c35 10.3 72.1 15.9 110.4 15.9 74.4 0 143.9-20.8 203-56.9L619.3 713.3z"></path>
|
||||
<path d="M648.7 699.5l92.3 127.2c63.3-46.2 112.2-111 138.6-186.4L648.7 699.5z"></path>
|
||||
<path d="M386.2 804 226.1 597.7l-87.9 27.4c34.9 114.7 121.3 207 232.2 250L386.2 804z"></path>
|
||||
<path d="M226.9 564.3 240.2 353l-64.4-40.5c-34.6 58.2-54.4 126.2-54.4 198.8 0 28.4 3 56.2 8.8 82.9L226.9 564.3z"></path>
|
||||
<path d="M842.8 609.8l-272.7-74.7c-4 9.5-10.1 17.9-17.9 24.4l77.3 112.1L842.8 609.8z"></path>
|
||||
<path d="M528 205.4l108.8 165.2 151-134.5c-67-67.3-158.4-110.2-259.7-114.5L528.1 205.4z"></path>
|
||||
<path d="M245.7 318.4l250.3-114 0-82.8c-125 4.8-234.8 68.5-302.8 164L245.7 318.4z"></path>
|
||||
</svg>
|
||||
);
|
||||
const FunnelChart = () => (
|
||||
<svg width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" viewBox="0 0 1024 1024">
|
||||
<path d="M533.6 784.5c6.6 16.4-1.3 35-17.6 41.7l0 0c-16.4 6.6-35-1.3-41.7-17.6L129.9 236c-6.6-16.4 1.3-35 17.6-41.7l0 0c16.4-6.6 35 1.3 41.7 17.6L533.6 784.5z"></path>
|
||||
<path d="M536 812.2c-6.9 16.3-25.7 23.9-41.9 17l0 0c-16.3-6.9-23.9-25.7-17-41.9l357.6-575.7c6.9-16.3 25.7-23.9 41.9-17l0 0c16.3 6.9 23.9 25.7 17 41.9L536 812.2z"></path>
|
||||
<path d="M895.4 224c0 17.7-14.3 32-32 32l-704 0c-17.7 0-32-14.3-32-32l0 0c0-17.7 14.3-32 32-32l704 0C881 192 895.4 206.3 895.4 224L895.4 224z"></path>
|
||||
<path d="M278.6 384l465 0 0 64-465 0 0-64Z"></path>
|
||||
<path d="M391.6 576l232.5 0 0 64-232.5 0 0-64Z"></path>
|
||||
</svg>
|
||||
);
|
||||
const ScatterChart = () => (
|
||||
<svg width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" viewBox="0 0 1024 1024">
|
||||
<path d="M191 864c0 17.7-14.3 32-32 32l0 0c-17.7 0-32-14.3-32-32L127 160c0-17.7 14.3-32 32-32l0 0c17.7 0 32 14.3 32 32L191 864z"></path>
|
||||
<path d="M159 896c-17.7 0-32-14.3-32-32l0 0c0-17.7 14.3-32 32-32l712 0c17.7 0 32 14.3 32 32l0 0c0 17.7-14.3 32-32 32L159 896z"></path>
|
||||
<path d="M745.6 765.9"></path>
|
||||
<path d="M307.6 244.3m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z"></path>
|
||||
<path d="M560 479.2m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z"></path>
|
||||
<path d="M391.9 387.7m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z"></path>
|
||||
<path d="M540.4 244.3m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z"></path>
|
||||
<path d="M732.9 292.3m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z"></path>
|
||||
<path d="M792.9 717.9m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z"></path>
|
||||
<path d="M307.6 527.2m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z"></path>
|
||||
<path d="M439.9 662.4m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z"></path>
|
||||
</svg>
|
||||
);
|
||||
const ColumnChart = () => (
|
||||
<svg width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" viewBox="0 0 1024 1024">
|
||||
<path d="M191 864c0 17.7-14.3 32-32 32l0 0c-17.7 0-32-14.3-32-32L127 160c0-17.7 14.3-32 32-32l0 0c17.7 0 32 14.3 32 32L191 864z"></path>
|
||||
<path d="M159 896c-17.7 0-32-14.3-32-32l0 0c0-17.7 14.3-32 32-32l712 0c17.7 0 32 14.3 32 32l0 0c0 17.7-14.3 32-32 32L159 896z"></path>
|
||||
<path d="M745.6 765.9"></path>
|
||||
<path d="M342.1 734.3c0 17.5-14.2 31.7-31.7 31.7l0 0c-17.5 0-31.7-14.2-31.7-31.7L278.7 489c0-17.5 14.2-31.7 31.7-31.7l0 0c17.5 0 31.7 14.2 31.7 31.7L342.1 734.3z"></path>
|
||||
<path d="M493.8 734.3c0 17.5-14.2 31.7-31.7 31.7l0 0c-17.5 0-31.7-14.2-31.7-31.7L430.4 387c0-17.5 14.2-31.7 31.7-31.7l0 0c17.5 0 31.7 14.2 31.7 31.7L493.8 734.3z"></path>
|
||||
<path d="M797.3 734.3c0 17.5-14.2 31.7-31.7 31.7l0 0c-17.5 0-31.7-14.2-31.7-31.7L733.9 234.5c0-17.5 14.2-31.7 31.7-31.7l0 0c17.5 0 31.7 14.2 31.7 31.7L797.3 734.3z"></path>
|
||||
<path d="M645.5 734.3c0 17.5-14.2 31.7-31.7 31.7l0 0c-17.5 0-31.7-14.2-31.7-31.7l0-97.2c0-17.5 14.2-31.7 31.7-31.7l0 0c17.5 0 31.7 14.2 31.7 31.7L645.5 734.3z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const BarChart = () => (
|
||||
<svg width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" viewBox="0 0 1024 1024">
|
||||
<path d="M131.072 156.16c0-17.92 14.336-31.744 31.744-31.744 17.92 0 31.744 14.336 31.744 31.744v712.192c0 17.92-14.336 31.744-31.744 31.744-17.92 0-31.744-14.336-31.744-31.744V156.16zM292.864 338.944c-17.408 0-31.744-14.336-31.744-31.744 0-17.408 14.336-31.744 31.744-31.744h245.248c17.408 0 31.744 14.336 31.744 31.744 0 17.408-14.336 31.744-31.744 31.744h-245.248zM292.864 490.496c-17.408 0-31.744-14.336-31.744-31.744 0-17.408 14.336-31.744 31.744-31.744H640c17.408 0 31.744 14.336 31.744 31.744 0 17.408-14.336 31.744-31.744 31.744h-347.136zM292.864 794.112c-17.408 0-31.744-14.336-31.744-31.744 0-17.408 14.336-31.744 31.744-31.744h499.712c17.408 0 31.744 14.336 31.744 31.744 0 17.408-14.336 31.744-31.744 31.744h-499.712zM292.864 642.56c-17.408 0-31.744-14.336-31.744-31.744 0-17.408 14.336-31.744 31.744-31.744h97.28c17.408 0 31.744 14.336 31.744 31.744 0 17.408-14.336 31.744-31.744 31.744h-97.28z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const LineChart = () => (
|
||||
<svg width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" viewBox="0 0 1024 1024">
|
||||
<path d="M191 864c0 17.7-14.3 32-32 32l0 0c-17.7 0-32-14.3-32-32L127 160c0-17.7 14.3-32 32-32l0 0c17.7 0 32 14.3 32 32L191 864z"></path>
|
||||
<path d="M159 896c-17.7 0-32-14.3-32-32l0 0c0-17.7 14.3-32 32-32l712 0c17.7 0 32 14.3 32 32l0 0c0 17.7-14.3 32-32 32L159 896z"></path>
|
||||
<path d="M307.1 686.3c-12.4 12.4-32.4 12.4-44.8 0l0 0c-12.4-12.4-12.4-32.4 0-44.8l173.4-173.4c12.4-12.4 32.4-12.4 44.8 0l0 0c12.4 12.4 12.4 32.4 0 44.8L307.1 686.3z"></path>
|
||||
<path d="M608.8 640c-12.4 12.4-32.4 12.4-44.8 0l0 0c-12.4-12.4-12.4-32.4 0-44.8l212.7-212.7c12.4-12.4 32.4-12.4 44.8 0l0 0c12.4 12.4 12.4 32.4 0 44.8L608.8 640z"></path>
|
||||
<path d="M608 595.3c12.4 12.4 12.4 32.4 0 44.8l0 0c-12.4 12.4-32.4 12.4-44.8 0L436 512.8c-12.4-12.4-12.4-32.4 0-44.8l0 0c12.4-12.4 32.4-12.4 44.8 0L608 595.3z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const PieChart = () => (
|
||||
<svg width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" viewBox="0 0 1024 1024">
|
||||
<path d="M489.7 511 489 511 489 126.9c-201 12.3-360.1 179.2-360.1 383.3 0 212.1 171.9 384 384 384 106.5 0 202.9-43.4 272.5-113.4L489.7 511z"></path>
|
||||
<path d="M773.1 237.2l6-6c-62.7-59.6-143.5-95.8-228.2-104l0 304.7L773.1 237.2z"></path>
|
||||
<path d="M819.5 281.6 564.1 505.1 822 739c3-4 5.9-8.1 8.7-12.3 41.4-61.3 65.6-135.2 65.6-214.7C896.3 425.6 867.7 345.8 819.5 281.6z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const AreaChart = () => (
|
||||
<svg width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" viewBox="0 0 1024 1024">
|
||||
<path d="M191 864c0 17.7-14.3 32-32 32l0 0c-17.7 0-32-14.3-32-32L127 160c0-17.7 14.3-32 32-32l0 0c17.7 0 32 14.3 32 32L191 864z"></path>
|
||||
<path d="M159 896c-17.7 0-32-14.3-32-32l0 0c0-17.7 14.3-32 32-32l712 0c17.7 0 32 14.3 32 32l0 0c0 17.7-14.3 32-32 32L159 896z"></path>
|
||||
<path d="M830.6 338.2 830.6 338.2c0.9-9.2-2.1-18.6-9.1-25.7-12.4-12.4-32.4-12.4-44.8 0L586 503.3 480.8 398.1c-6.2-6.2-14.4-9.3-22.5-9.3-8.1 0-16.3 3.1-22.5 9.3L262.4 571.5c-6 6-9.1 13.8-9.3 21.6 0 0.1 0 0.1 0 0.2l0 111.8c0 35.3 28.7 64 64 64l449.7 0c33.8 0 61.4-26.1 63.8-59.3l0 0 0-0.2c0.1-1.5 0.2-3 0.2-4.5L830.8 593.3c0-0.3-0.1-0.7-0.2-1L830.6 338.2z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
Icon.register({
|
||||
'icon-area': (props) => <Icon component={AreaChart} {...props} />,
|
||||
'icon-pie': (props) => <Icon component={PieChart} {...props} />,
|
||||
'icon-radar': (props) => <Icon component={RadarChart} {...props} />,
|
||||
'icon-funnel': (props) => <Icon component={FunnelChart} {...props} />,
|
||||
'icon-line': (props) => <Icon component={LineChart} {...props} />,
|
||||
'icon-bar': (props) => <Icon component={BarChart} {...props} />,
|
||||
'icon-column': (props) => <Icon component={ColumnChart} {...props} />,
|
||||
'icon-scatter': (props) => <Icon component={ScatterChart} {...props} />,
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
import { G2Plot } from '@nocobase/client';
|
||||
import DataSetPreviewTable from '../DataSetPreviewTable';
|
||||
|
||||
const chartRenderComponentsMap = new Map();
|
||||
chartRenderComponentsMap.set('G2Plot', G2Plot);
|
||||
chartRenderComponentsMap.set('DataSetPreviewTable', DataSetPreviewTable);
|
||||
export default chartRenderComponentsMap;
|
19
packages/plugins/charts/src/client/hooks/index.ts
Normal file
19
packages/plugins/charts/src/client/hooks/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { useChartQueryMetadataContext } from '../ChartQueryMetadataProvider';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const useFieldsById = (queryId: number) => {
|
||||
const [fields, setFields] = useState([]);
|
||||
const ctx = useChartQueryMetadataContext();
|
||||
useEffect(() => {
|
||||
const chartQueryList = ctx?.data;
|
||||
if (chartQueryList && Array.isArray(chartQueryList)) {
|
||||
const currentQuery = chartQueryList.find((chartQuery) => chartQuery.id === queryId);
|
||||
setFields(currentQuery?.fields);
|
||||
}
|
||||
}, [queryId]);
|
||||
return {
|
||||
fields,
|
||||
};
|
||||
};
|
||||
|
||||
export { useFieldsById };
|
101
packages/plugins/charts/src/client/index.tsx
Normal file
101
packages/plugins/charts/src/client/index.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { registerValidateRules } from '@formily/core';
|
||||
import {
|
||||
SchemaComponentOptions,
|
||||
SchemaInitializerContext,
|
||||
SettingsCenterProvider,
|
||||
useAPIClient,
|
||||
} from '@nocobase/client';
|
||||
import JSON5 from 'json5';
|
||||
import React, { useContext } from 'react';
|
||||
import { ChartBlockEngine } from './ChartBlockEngine';
|
||||
import { ChartBlockInitializer } from './ChartBlockInitializer';
|
||||
import { ChartQueryMetadataProvider } from './ChartQueryMetadataProvider';
|
||||
import './Icons';
|
||||
import { lang } from './locale';
|
||||
import { CustomSelect } from './select';
|
||||
import { QueriesTable } from './settings/QueriesTable';
|
||||
|
||||
registerValidateRules({
|
||||
json5: (value, rule) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const val = JSON5.parse(value);
|
||||
if (!isNaN(val)) {
|
||||
return {
|
||||
type: 'error',
|
||||
message: lang('Invalid JSON format'),
|
||||
};
|
||||
}
|
||||
return '';
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
type: 'error',
|
||||
message: lang('Invalid JSON format'),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default React.memo((props) => {
|
||||
const api = useAPIClient();
|
||||
const items = useContext(SchemaInitializerContext);
|
||||
const children = items.BlockInitializers.items[0].children;
|
||||
if (children) {
|
||||
const hasChartItem = children.some((child) => child?.component === 'ChartBlockInitializer');
|
||||
if (!hasChartItem) {
|
||||
children.push({
|
||||
key: 'chart',
|
||||
type: 'item',
|
||||
icon: 'PieChartOutlined',
|
||||
title: '{{t("Chart",{ns:"charts"})}}',
|
||||
component: 'ChartBlockInitializer',
|
||||
});
|
||||
}
|
||||
}
|
||||
const validateSQL = (sql) => {
|
||||
return new Promise((resolve) => {
|
||||
api
|
||||
.request({
|
||||
url: 'chartsQueries:validate',
|
||||
method: 'post',
|
||||
data: {
|
||||
sql,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
resolve(data?.data?.errorMessage);
|
||||
})
|
||||
.catch(() => {
|
||||
resolve('Invalid SQL');
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<ChartQueryMetadataProvider>
|
||||
<SettingsCenterProvider
|
||||
settings={{
|
||||
charts: {
|
||||
title: '{{t("Charts", {ns:"charts"})}}',
|
||||
icon: 'PieChartOutlined',
|
||||
tabs: {
|
||||
queries: {
|
||||
title: '{{t("Queries", {ns:"charts"})}}',
|
||||
component: QueriesTable,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SchemaComponentOptions
|
||||
scope={{ validateSQL }}
|
||||
components={{ CustomSelect, ChartBlockInitializer, ChartBlockEngine }}
|
||||
>
|
||||
<SchemaInitializerContext.Provider value={items}>{props.children}</SchemaInitializerContext.Provider>
|
||||
</SchemaComponentOptions>
|
||||
</SettingsCenterProvider>
|
||||
</ChartQueryMetadataProvider>
|
||||
);
|
||||
});
|
23
packages/plugins/charts/src/client/locale/en-US.ts
Normal file
23
packages/plugins/charts/src/client/locale/en-US.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export default {
|
||||
Edit: 'Edit',
|
||||
Delete: 'Delete',
|
||||
Cancel: 'Cancel',
|
||||
Submit: 'Submit',
|
||||
Actions: 'Actions',
|
||||
Title: 'Title',
|
||||
Enable: 'Enable',
|
||||
'SAML manager': 'SAML manager',
|
||||
'SAML Providers': 'SAML Providers',
|
||||
'Redirect url': 'Redirect url',
|
||||
'SP entity id': 'SP entity id',
|
||||
'Add provider': 'Add',
|
||||
'Edit provider': 'Edit',
|
||||
'Client id': 'Client id',
|
||||
'Entity id or issuer': 'Entity id or issuer',
|
||||
'Login Url': 'Login Url',
|
||||
'Public cert': 'Public cert',
|
||||
'Delete provider': 'Delete',
|
||||
'Are you sure you want to delete it?': 'Are you sure you want to delete it?',
|
||||
'Sign in button name, which will be displayed on the sign in page':
|
||||
'Sign in button name, which will be displayed on the sign in page',
|
||||
};
|
18
packages/plugins/charts/src/client/locale/index.ts
Normal file
18
packages/plugins/charts/src/client/locale/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { i18n } from '@nocobase/client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NAMESPACE = 'charts';
|
||||
|
||||
// i18n.addResources('zh-CN', NAMESPACE, zhCN);
|
||||
// i18n.addResources('en-US', NAMESPACE, enUS);
|
||||
// i18n.addResources('ja-JP', NAMESPACE, jaJP);
|
||||
// i18n.addResources('ru-RU', NAMESPACE, ruRU);
|
||||
// i18n.addResources('tr-TR', NAMESPACE, trTR);
|
||||
|
||||
export function lang(key: string) {
|
||||
return i18n.t(key, { ns: NAMESPACE });
|
||||
}
|
||||
|
||||
export function useChartsTranslation() {
|
||||
return useTranslation(NAMESPACE);
|
||||
}
|
1
packages/plugins/charts/src/client/locale/ja-JP.ts
Normal file
1
packages/plugins/charts/src/client/locale/ja-JP.ts
Normal file
@ -0,0 +1 @@
|
||||
export default {};
|
1
packages/plugins/charts/src/client/locale/ru-RU.ts
Normal file
1
packages/plugins/charts/src/client/locale/ru-RU.ts
Normal file
@ -0,0 +1 @@
|
||||
export default {};
|
1
packages/plugins/charts/src/client/locale/tr-TR.ts
Normal file
1
packages/plugins/charts/src/client/locale/tr-TR.ts
Normal file
@ -0,0 +1 @@
|
||||
export default {};
|
63
packages/plugins/charts/src/client/locale/zh-CN.ts
Normal file
63
packages/plugins/charts/src/client/locale/zh-CN.ts
Normal file
@ -0,0 +1,63 @@
|
||||
export default {
|
||||
Edit: '编辑',
|
||||
Delete: '删除',
|
||||
Cancel: '取消',
|
||||
Submit: '提交',
|
||||
Actions: '操作',
|
||||
Title: '名称',
|
||||
Enable: '启用',
|
||||
Chart: '图表',
|
||||
Charts: '图表',
|
||||
Queries: '查询列表',
|
||||
'Select chart query': '选择图表数据',
|
||||
'Select query data': '选择查询数据',
|
||||
Type: '类型',
|
||||
'Chart type': '图表类型',
|
||||
'Chart title': '图表标题',
|
||||
'Basic charts': '基础图表',
|
||||
'More charts': '更多图表',
|
||||
'Chart config': '图表配置',
|
||||
'Add query': '添加查询',
|
||||
'Edit query': '编辑查询',
|
||||
'Invalid JSON format,must be an object array.': '无效的JSON格式,必须是对象数组。',
|
||||
Area: '面积图',
|
||||
Bar: '条形图',
|
||||
Column: '柱状图',
|
||||
Funnel: '漏斗图',
|
||||
Line: '折线图',
|
||||
Pie: '饼图',
|
||||
Radar: '雷达图',
|
||||
Scatter: '散点图',
|
||||
'Edit chart block': '编辑图表区块',
|
||||
'Chart preview': '图表预览',
|
||||
'Delete queries': '删除查询列表',
|
||||
'Delete query': '删除查询',
|
||||
'Add chart query': '添加图表查询',
|
||||
'Add SQL query': '添加SQL查询',
|
||||
'Add JSON query': '添加JSON查询',
|
||||
'Data preview': '数据预览',
|
||||
'Category axis / Dimension': '类别轴 / 纬度',
|
||||
'Value axis / Metrics': '值轴 / 度量',
|
||||
'JSON config': 'JSON 配置',
|
||||
'Json config references': 'JSON配置参考',
|
||||
'Create chart block': '创建图表区块',
|
||||
'Invalid JSON format': '无效的JSON格式',
|
||||
'Json config references: ': 'JSON配置参考: ',
|
||||
'Sector Angle / Metric': '扇形角 / 度量',
|
||||
'Sector label / Dimensional': '扇形标签 / 纬度',
|
||||
'Color legend / Dimensional': '颜色系列 / 纬度',
|
||||
'Funnel Layer Width/Metrics': '漏斗层宽度 / 度量',
|
||||
'Branch Tags/Dimensions': '分支标签 / 纬度',
|
||||
'Branch Length/Metrics': '分支长度 / 度量',
|
||||
'Please check the chart config': '请检查图表配置',
|
||||
'1 「time」or 「Ordered Noun」 field,1 「Numerical」 field,1 「Unordered Noun」 field (optional)':
|
||||
'1 个「时间」或「有序名词」字段,1 个「数值」字段,1 个「无序名词」字段(可选)',
|
||||
'1 「time」 or 「ordered noun」 field, 1 「value」 field, 0~ 1 「unordered noun」':
|
||||
'1 个「时间」或「有序名词」字段,1 个「数值」字段,0 ~ 1 个「无序名词」',
|
||||
'1 「time」 or 「ordered noun」 field, 1 「value」 field, 0 to 1 「unordered noun」':
|
||||
'1 个「时间」或「有序名词」字段,1 个「数值」字段,0 ~ 1 个「无序名词」',
|
||||
'1 「Unordered Noun」 field, 1 「Numeric」 field': '1 个「无序名词」字段,1 个「数值」字段',
|
||||
'1 「Time」 or 「Order Noun」 field, 1 「Value」 field': '1 个「时间」或「有序名词」字段,1 个「数值」字段',
|
||||
'1~ 2 「Unordered Noun」 fields, 1 「Numeric」 field': '1 ~ 2 个「无序名词」字段,1 个「数值」字段',
|
||||
'1 「Numeric」 field, 0~ 1 「Unordered Noun」 field': '1 个「数值」字段,0 ~ 1 个「无序名词」字段',
|
||||
};
|
127
packages/plugins/charts/src/client/select/CustomSelect.tsx
Normal file
127
packages/plugins/charts/src/client/select/CustomSelect.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { connect, mapProps, mapReadPretty } from '@formily/react';
|
||||
import { isValid } from '@formily/shared';
|
||||
import { Icon } from '@nocobase/client';
|
||||
import type { SelectProps } from 'antd';
|
||||
import { Popover, Select as AntdSelect } from 'antd';
|
||||
import React from 'react';
|
||||
import { lang } from '../locale';
|
||||
import { ReadPretty } from './ReadPretty';
|
||||
|
||||
type Props = SelectProps<any, any> & { objectValue?: boolean; onChange?: (v: any) => void; multiple: boolean };
|
||||
|
||||
const isEmptyObject = (val: any) => !isValid(val) || (typeof val === 'object' && Object.keys(val).length === 0);
|
||||
|
||||
const { Option, OptGroup } = AntdSelect;
|
||||
const filterOption = (input, option) => (option?.label ?? '').toLowerCase().includes((input || '').toLowerCase());
|
||||
|
||||
const InternalSelect = connect(
|
||||
(props: Props) => {
|
||||
const { ...others } = props;
|
||||
const { options, ...othersProps } = { ...others };
|
||||
const mode = props.mode || props.multiple ? 'multiple' : undefined;
|
||||
const group1 = options.filter((option) => option.group === 2);
|
||||
const group2 = options.filter((option) => option.group === 1);
|
||||
return (
|
||||
<AntdSelect
|
||||
showSearch
|
||||
filterOption={filterOption}
|
||||
allowClear
|
||||
{...othersProps}
|
||||
onChange={(changed) => {
|
||||
props.onChange?.(changed === undefined ? null : changed);
|
||||
}}
|
||||
mode={mode}
|
||||
>
|
||||
<OptGroup label={lang('Basic charts')}>
|
||||
{group1.map((option) => (
|
||||
<Option value={option.key} label={lang(option.title)}>
|
||||
<Popover
|
||||
placement={'right'}
|
||||
zIndex={99999999999}
|
||||
content={() => (
|
||||
<span>
|
||||
{lang(option?.description)
|
||||
?.split(',')
|
||||
.map((item) => (
|
||||
<div>{item}</div>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
trigger="hover"
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<Icon type={option.iconId} />
|
||||
<span role="img" aria-label={lang(option.title)}>
|
||||
{lang(option.title)}
|
||||
</span>
|
||||
</div>
|
||||
</Popover>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
<OptGroup label={lang('More charts')}>
|
||||
{group2.map((option) => (
|
||||
<Option value={option.key} label={lang(option.title)}>
|
||||
<Popover
|
||||
placement={'right'}
|
||||
zIndex={99999999999}
|
||||
content={() => (
|
||||
<span>
|
||||
{lang(option?.description)
|
||||
?.split(',')
|
||||
.map((item) => (
|
||||
<div>{item}</div>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
trigger="hover"
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<Icon type={option.iconId} />
|
||||
<span role="img" aria-label={lang(option.title)}>
|
||||
{lang(option.title)}
|
||||
</span>
|
||||
</div>
|
||||
</Popover>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
</AntdSelect>
|
||||
);
|
||||
},
|
||||
mapProps(
|
||||
{
|
||||
dataSource: 'options',
|
||||
loading: true,
|
||||
},
|
||||
(props, field) => {
|
||||
return {
|
||||
...props,
|
||||
suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props?.suffixIcon,
|
||||
};
|
||||
},
|
||||
),
|
||||
mapReadPretty(ReadPretty),
|
||||
);
|
||||
|
||||
export const CustomSelect = InternalSelect as unknown as typeof InternalSelect & {
|
||||
ReadPretty: typeof ReadPretty;
|
||||
};
|
||||
|
||||
CustomSelect.ReadPretty = ReadPretty;
|
||||
|
||||
export default CustomSelect;
|
36
packages/plugins/charts/src/client/select/ReadPretty.tsx
Normal file
36
packages/plugins/charts/src/client/select/ReadPretty.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { isArrayField } from '@formily/core';
|
||||
import { observer, useField } from '@formily/react';
|
||||
import { isValid } from '@formily/shared';
|
||||
import { Tag } from 'antd';
|
||||
import React from 'react';
|
||||
import { defaultFieldNames, getCurrentOptions } from './shared';
|
||||
import { useCompile } from '@nocobase/client';
|
||||
|
||||
type Composed = {
|
||||
Select?: React.FC<any>;
|
||||
Object?: React.FC<any>;
|
||||
};
|
||||
|
||||
export const ReadPretty = observer((props: any) => {
|
||||
const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
|
||||
const field = useField<any>();
|
||||
const compile = useCompile();
|
||||
|
||||
if (!isValid(props.value)) {
|
||||
return <div />;
|
||||
}
|
||||
if (isArrayField(field) && field?.value?.length === 0) {
|
||||
return <div />;
|
||||
}
|
||||
const dataSource = field.dataSource || props.options || [];
|
||||
const options = getCurrentOptions(field.value, dataSource, fieldNames);
|
||||
return (
|
||||
<div>
|
||||
{options.map((option, key) => (
|
||||
<Tag key={key} color={option[fieldNames.color]} icon={option.icon}>
|
||||
{compile(option[fieldNames.label])}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
38
packages/plugins/charts/src/client/select/index.md
Normal file
38
packages/plugins/charts/src/client/select/index.md
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Select
|
||||
|
||||
## Examples
|
||||
|
||||
### 单选
|
||||
|
||||
<code src="./demos/demo1.tsx" />
|
||||
|
||||
### 多选
|
||||
|
||||
<code src="./demos/demo2.tsx" />
|
||||
|
||||
### 值为 Object 类型的 Select
|
||||
|
||||
<code src="./demos/demo3.tsx" />
|
||||
|
||||
## API
|
||||
|
||||
基于 Ant Design 的 [Select](https://ant.design/components/select/#API),相关扩展属性有:
|
||||
|
||||
- `objectValue` 值为 object 类型
|
||||
- `fieldNames` 默认值有区别
|
||||
|
||||
```ts
|
||||
export const defaultFieldNames = {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
color: 'color',
|
||||
options: 'children',
|
||||
};
|
||||
```
|
2
packages/plugins/charts/src/client/select/index.ts
Normal file
2
packages/plugins/charts/src/client/select/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './CustomSelect';
|
||||
export * from './shared';
|
36
packages/plugins/charts/src/client/select/shared.ts
Normal file
36
packages/plugins/charts/src/client/select/shared.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { castArray } from 'lodash';
|
||||
|
||||
export const defaultFieldNames = {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
color: 'color',
|
||||
options: 'children',
|
||||
};
|
||||
|
||||
export const getCurrentOptions = (values, dataSource, fieldNames) => {
|
||||
function flatData(data) {
|
||||
let newArr = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const children = data[i][fieldNames.options];
|
||||
if (Array.isArray(children)) {
|
||||
newArr.push(...flatData(children));
|
||||
}
|
||||
newArr.push({ ...data[i] });
|
||||
}
|
||||
return newArr;
|
||||
}
|
||||
const result = flatData(dataSource);
|
||||
values = castArray(values)
|
||||
.filter((item) => item != null)
|
||||
.map((val) => (typeof val === 'object' ? val[fieldNames.value] : val));
|
||||
const findOptions = (options: any[]) => {
|
||||
if (!options) return [];
|
||||
let current = [];
|
||||
for (const value of values) {
|
||||
const option = options.find((v) => v[fieldNames.value] === value) || { value: value, label: value };
|
||||
current.push(option);
|
||||
}
|
||||
return current;
|
||||
};
|
||||
return findOptions(result);
|
||||
};
|
161
packages/plugins/charts/src/client/settings/AddNewQuery.tsx
Normal file
161
packages/plugins/charts/src/client/settings/AddNewQuery.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import { DownOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { createForm } from '@formily/core';
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import {
|
||||
ActionContext,
|
||||
SchemaComponent,
|
||||
useActionContext,
|
||||
useRecord,
|
||||
useResourceActionContext,
|
||||
useResourceContext,
|
||||
} from '@nocobase/client';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useChartQueryMetadataContext } from '../ChartQueryMetadataProvider';
|
||||
import { getQueryTypeSchema } from './queryTypes';
|
||||
import { lang } from '../locale';
|
||||
|
||||
const useCreateAction = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
const form = useForm();
|
||||
const { refresh } = useResourceActionContext();
|
||||
const { resource } = useResourceContext();
|
||||
const ctx = useChartQueryMetadataContext();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
await resource.create({ values: form.values });
|
||||
setVisible(false);
|
||||
await form.reset();
|
||||
refresh();
|
||||
ctx.refresh();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const useUpdateAction = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
const form = useForm();
|
||||
const { refresh } = useResourceActionContext();
|
||||
const { resource, targetKey } = useResourceContext();
|
||||
const { [targetKey]: filterByTk } = useRecord();
|
||||
const ctx = useChartQueryMetadataContext();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
await resource.update({ filterByTk, values: form.values });
|
||||
setVisible(false);
|
||||
await form.reset();
|
||||
refresh();
|
||||
ctx.refresh();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const useCloseAction = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
return {
|
||||
async run() {
|
||||
setVisible(false);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getSchema = (initialValue, { form, isNewRecord }) => {
|
||||
const type = initialValue.type;
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
name: uid(),
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
form,
|
||||
// initialValue: JSON.parse(JSON.stringify(initialValue)),
|
||||
},
|
||||
title: isNewRecord ? lang('Add query') : lang('Edit query'),
|
||||
properties: {
|
||||
title: {
|
||||
title: lang('Title'),
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
options: getQueryTypeSchema(type),
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
'x-component': 'Action',
|
||||
title: lang('Cancel'),
|
||||
'x-component-props': {
|
||||
useAction: '{{ useCloseAction }}',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
'x-component': 'Action',
|
||||
title: lang('Submit'),
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: '{{ useSubmitAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return schema;
|
||||
};
|
||||
|
||||
export const AddNewQuery = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [schema, setSchema] = useState({});
|
||||
const form = useMemo(() => createForm(), []);
|
||||
const menu = (
|
||||
<Menu
|
||||
onClick={(info) => {
|
||||
setVisible(true);
|
||||
form.setValues({ type: info.key });
|
||||
setSchema(getSchema({ type: info.key }, { form, isNewRecord: true }));
|
||||
}}
|
||||
>
|
||||
<Menu.Item key={'json'}>JSON</Menu.Item>
|
||||
<Menu.Item key={'sql'}>SQL</Menu.Item>
|
||||
<Menu.Item disabled key={'api'}>
|
||||
API
|
||||
</Menu.Item>
|
||||
<Menu.Item disabled>Collection</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||
<Dropdown overlay={menu}>
|
||||
<Button icon={<PlusOutlined />} type={'primary'}>
|
||||
{lang('Add query')} <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<SchemaComponent schema={schema} scope={{ useCloseAction, useSubmitAction: useCreateAction }} />
|
||||
</ActionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditQuery = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const record = useRecord();
|
||||
const form = useMemo(() => createForm(), []);
|
||||
const schema = getSchema(record, { form, isNewRecord: false });
|
||||
return (
|
||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||
<a
|
||||
onClick={() => {
|
||||
form.setValues(record);
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
{lang('Edit')}
|
||||
</a>
|
||||
<SchemaComponent schema={schema} scope={{ useCloseAction, useSubmitAction: useUpdateAction }} />
|
||||
</ActionContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import { useRecord } from '@nocobase/client';
|
||||
import { Table } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
export const ConfigureFields = () => {
|
||||
const record = useRecord();
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
title: '字段标识',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
]}
|
||||
dataSource={record.fields || []}
|
||||
/>
|
||||
);
|
||||
};
|
26
packages/plugins/charts/src/client/settings/QueriesTable.tsx
Normal file
26
packages/plugins/charts/src/client/settings/QueriesTable.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import { Card } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AddNewQuery, EditQuery } from './AddNewQuery';
|
||||
import { ConfigureFields } from './ConfigureFields';
|
||||
import {
|
||||
chartsQueriesSchema,
|
||||
useDestroyAllSelectedQueriesAction,
|
||||
useDestroyQueryItemAction,
|
||||
} from './schemas/chartsQueries';
|
||||
import JSON5 from 'json5';
|
||||
|
||||
export const QueriesTable = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<SchemaComponent
|
||||
scope={{ JSON5, useDestroyQueryItemAction, useDestroyAllSelectedQueriesAction }}
|
||||
schema={chartsQueriesSchema}
|
||||
components={{ AddNewQuery, EditQuery, ConfigureFields }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
64
packages/plugins/charts/src/client/settings/queryTypes.ts
Normal file
64
packages/plugins/charts/src/client/settings/queryTypes.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
export const json: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: {
|
||||
title: 'JSON',
|
||||
required: true,
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-validator': { json5: true },
|
||||
'x-component-props': {
|
||||
autoSize: {
|
||||
maxRows: 20,
|
||||
minRows: 10,
|
||||
},
|
||||
},
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const sql: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sql: {
|
||||
title: 'SQL',
|
||||
required: true,
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-validator': {
|
||||
triggerType: 'onBlur',
|
||||
validator: '{{validateSQL}}',
|
||||
},
|
||||
'x-component-props': {
|
||||
autoSize: {
|
||||
maxRows: 20,
|
||||
minRows: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const api: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
api: {
|
||||
title: 'API',
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const types = {
|
||||
json,
|
||||
sql,
|
||||
api,
|
||||
};
|
||||
|
||||
export const getQueryTypeSchema = (type) => {
|
||||
return cloneDeep(types[type]);
|
||||
};
|
@ -0,0 +1,319 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import {
|
||||
useActionContext,
|
||||
useRecord,
|
||||
useRequest,
|
||||
useResourceActionContext,
|
||||
useResourceContext,
|
||||
} from '@nocobase/client';
|
||||
import { useChartQueryMetadataContext } from '../../ChartQueryMetadataProvider';
|
||||
|
||||
const collection = {
|
||||
name: 'chartsQueries',
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Title",{ns:"charts"})}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
interface: 'select',
|
||||
uiSchema: {
|
||||
title: '{{t("Type",{ns:"charts"})}}',
|
||||
type: 'string',
|
||||
'x-component': 'Select',
|
||||
required: true,
|
||||
enum: [
|
||||
{ label: '{{t("API")}}', value: 'api' },
|
||||
{ label: '{{t("SQL")}}', value: 'sql' },
|
||||
{ label: '{{t("JSON")}}', value: 'json' },
|
||||
],
|
||||
} as ISchema,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const useDestroyQueryItemAction = () => {
|
||||
const ctx = useChartQueryMetadataContext();
|
||||
const { refresh } = useResourceActionContext();
|
||||
const { resource, targetKey } = useResourceContext();
|
||||
const { [targetKey]: filterByTk } = useRecord();
|
||||
return {
|
||||
async run() {
|
||||
await resource.destroy({ filterByTk });
|
||||
refresh();
|
||||
ctx.refresh();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const useDestroyAllSelectedQueriesAction = () => {
|
||||
const ctx = useChartQueryMetadataContext();
|
||||
const { state, setState, refresh } = useResourceActionContext();
|
||||
const { resource, targetKey } = useResourceContext();
|
||||
return {
|
||||
async run() {
|
||||
await resource.destroy({
|
||||
filterByTk: state?.selectedRowKeys || [],
|
||||
});
|
||||
setState?.({ selectedRowKeys: [] });
|
||||
refresh();
|
||||
ctx.refresh();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const chartsQueriesSchema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
block1: {
|
||||
type: 'void',
|
||||
'x-decorator': 'ResourceActionProvider',
|
||||
'x-decorator-props': {
|
||||
collection,
|
||||
resourceName: 'chartsQueries',
|
||||
request: {
|
||||
resource: 'chartsQueries',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 50,
|
||||
sort: ['-id'],
|
||||
appends: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
'x-component': 'CollectionProvider',
|
||||
'x-component-props': {
|
||||
collection,
|
||||
},
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
delete: {
|
||||
type: 'void',
|
||||
title: '{{ t("Delete") }}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ useDestroyAllSelectedQueriesAction }}',
|
||||
confirm: {
|
||||
title: '{{t("Delete queries",{ns:"charts"})}}',
|
||||
content: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
type: 'void',
|
||||
title: '{{t("Add query")}}',
|
||||
'x-component': 'AddNewQuery',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
useValues(options) {
|
||||
const ctx = useActionContext();
|
||||
return useRequest(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
name: `s_${uid()}`,
|
||||
},
|
||||
}),
|
||||
{ ...options, refreshDeps: [ctx.visible] },
|
||||
);
|
||||
},
|
||||
},
|
||||
title: '{{t("Add query",{ns:"charts"})}}',
|
||||
properties: {
|
||||
title: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
type: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{t("Cancel",{ns:"charts"})}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ cm.useCancelAction }}',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
title: '{{t("Submit",{ns:"charts"})}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: '{{ cm.useCreateAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
table: {
|
||||
type: 'void',
|
||||
'x-uid': 'input',
|
||||
'x-component': 'Table.Void',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
useDataSource: '{{ cm.useDataSourceFromRAC }}',
|
||||
},
|
||||
properties: {
|
||||
title: {
|
||||
type: 'void',
|
||||
'x-decorator': 'Table.Column.Decorator',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'number',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
type: {
|
||||
type: 'void',
|
||||
'x-decorator': 'Table.Column.Decorator',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
type: 'void',
|
||||
title: '{{t("Actions")}}',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
properties: {
|
||||
fields: {
|
||||
type: 'void',
|
||||
title: '{{t("Configure fields")}}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
title: '{{t("Configure fields")}}',
|
||||
properties: {
|
||||
configure: {
|
||||
type: 'void',
|
||||
'x-component': 'ConfigureFields',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
type: 'void',
|
||||
title: '{{t("Edit")}}',
|
||||
'x-component': 'EditQuery',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
useValues: '{{ cm.useValuesFromRecord }}',
|
||||
},
|
||||
title: '{{t("Edit")}}',
|
||||
properties: {
|
||||
title: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{t("Cancel",{ns:"charts"})}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ cm.useCancelAction }}',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
title: '{{t("Submit",{ns:"charts"})}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: '{{ cm.useUpdateAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
type: 'void',
|
||||
title: '{{ t("Delete") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
confirm: {
|
||||
title: '{{t("Delete query",{ns:"charts"})}}',
|
||||
content: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
useAction: '{{ useDestroyQueryItemAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,64 @@
|
||||
import JSON5 from 'json5';
|
||||
|
||||
const chartConfig = {
|
||||
yField: '{{metric}}',
|
||||
xField: '{{dimension}}',
|
||||
seriesField: '{{category}}',
|
||||
};
|
||||
|
||||
export const areaTemplate = {
|
||||
description: '1 「time」or 「Ordered Noun」 field,1 「Numerical」 field,1 「Unordered Noun」 field (optional)',
|
||||
type: 'Area',
|
||||
title: 'Area',
|
||||
iconId: 'icon-area',
|
||||
group: 2,
|
||||
renderComponent: 'G2Plot',
|
||||
defaultChartOptions: chartConfig,
|
||||
configurableProperties: {
|
||||
type: 'void',
|
||||
properties: {
|
||||
dimension: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Category axis / Dimension",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
metric: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Value axis / Metrics",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
title: '{{t("Color legend / Dimensional",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
jsonConfig: {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
properties: {
|
||||
template: {
|
||||
required: true,
|
||||
title: '{{t("JSON config",{ns:"charts"})}}',
|
||||
type: 'string',
|
||||
default: JSON5.stringify(chartConfig, null, 2),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {
|
||||
autoSize: { minRows: 8, maxRows: 16 },
|
||||
},
|
||||
description: '{{jsonConfigDesc("Area | G2Plot","https://g2plot.antv.antgroup.com/api/plots/area")}}',
|
||||
'x-validator': { json5: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
80
packages/plugins/charts/src/client/templates/BarTemplate.tsx
Normal file
80
packages/plugins/charts/src/client/templates/BarTemplate.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import JSON5 from 'json5';
|
||||
|
||||
const chartConfig = {
|
||||
appendPadding: 10,
|
||||
isGroup: true,
|
||||
yField: '{{dimension}}',
|
||||
xField: '{{metric}}',
|
||||
seriesField: '{{category}}',
|
||||
label: {
|
||||
// 可手动配置 label 数据标签位置
|
||||
position: 'middle', // 'top', 'bottom', 'middle',
|
||||
// 配置样式
|
||||
style: {
|
||||
fill: '#FFFFFF',
|
||||
opacity: 0.6,
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
label: {
|
||||
autoHide: true,
|
||||
autoRotate: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
export const barTemplate = {
|
||||
description: '1 「time」 or 「ordered noun」 field, 1 「value」 field, 0~ 1 「unordered noun」',
|
||||
type: 'Bar',
|
||||
title: 'Bar',
|
||||
iconId: 'icon-bar',
|
||||
group: 2,
|
||||
renderComponent: 'G2Plot',
|
||||
defaultChartOptions: chartConfig,
|
||||
configurableProperties: {
|
||||
type: 'void',
|
||||
properties: {
|
||||
dimension: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Category axis / Dimension",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
metric: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Value axis / Metrics",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
title: '{{t("Color legend / Dimensional",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
jsonConfig: {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
properties: {
|
||||
template: {
|
||||
required: true,
|
||||
title: '{{t("JSON config",{ns:"charts"})}}',
|
||||
type: 'string',
|
||||
default: JSON5.stringify(chartConfig, null, 2),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {
|
||||
autoSize: { minRows: 8, maxRows: 16 },
|
||||
},
|
||||
description: '{{jsonConfigDesc("Bar | G2Plot","https://g2plot.antv.antgroup.com/api/plots/bar")}}',
|
||||
'x-validator': { json5: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,81 @@
|
||||
import JSON5 from 'json5';
|
||||
|
||||
const chartConfig = {
|
||||
appendPadding: 10,
|
||||
isGroup: true,
|
||||
yField: '{{metric}}',
|
||||
xField: '{{dimension}}',
|
||||
seriesField: '{{category}}',
|
||||
label: {
|
||||
// 可手动配置 label 数据标签位置
|
||||
position: 'middle', // 'top', 'bottom', 'middle',
|
||||
// 配置样式
|
||||
style: {
|
||||
fill: '#FFFFFF',
|
||||
opacity: 0.6,
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
label: {
|
||||
autoHide: true,
|
||||
autoRotate: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const columnTemplate = {
|
||||
description: '1 「time」 or 「ordered noun」 field, 1 「value」 field, 0 to 1 「unordered noun」',
|
||||
type: 'Column',
|
||||
title: 'Column',
|
||||
iconId: 'icon-column',
|
||||
group: 2,
|
||||
renderComponent: 'G2Plot',
|
||||
defaultChartOptions: chartConfig,
|
||||
configurableProperties: {
|
||||
type: 'void',
|
||||
properties: {
|
||||
dimension: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Category axis / Dimension",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
metric: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Value axis / Metrics",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
title: '{{t("Color legend / Dimensional",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
jsonConfig: {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
properties: {
|
||||
template: {
|
||||
required: true,
|
||||
title: '{{t("JSON config",{ns:"charts"})}}',
|
||||
type: 'string',
|
||||
default: JSON5.stringify(chartConfig, null, 2),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {
|
||||
autoSize: { minRows: 8, maxRows: 16 },
|
||||
},
|
||||
description: '{{jsonConfigDesc("Column | G2Plot","https://g2plot.antv.antgroup.com/api/plots/column")}}',
|
||||
'x-validator': { json5: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,65 @@
|
||||
import JSON5 from 'json5';
|
||||
|
||||
const chartConfig = {
|
||||
xField: '{{dimension}}',
|
||||
yField: '{{metric}}',
|
||||
seriesField: '{{category}}',
|
||||
legend: false,
|
||||
};
|
||||
|
||||
export const funnelTemplate = {
|
||||
description: '1 「Unordered Noun」 field, 1 「Numeric」 field',
|
||||
type: 'Funnel',
|
||||
title: 'Funnel',
|
||||
iconId: 'icon-funnel',
|
||||
group: 1,
|
||||
renderComponent: 'G2Plot',
|
||||
defaultChartOptions: chartConfig,
|
||||
configurableProperties: {
|
||||
type: 'void',
|
||||
properties: {
|
||||
dimension: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Sector label / Dimensional",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
metric: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Funnel Layer Width/Metrics",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
title: '{{t("Color legend / Dimensional",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
jsonConfig: {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
properties: {
|
||||
template: {
|
||||
required: true,
|
||||
title: '{{t("JSON config",{ns:"charts"})}}',
|
||||
type: 'string',
|
||||
default: JSON5.stringify(chartConfig, null, 2),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {
|
||||
autoSize: { minRows: 8, maxRows: 16 },
|
||||
},
|
||||
description: '{{jsonConfigDesc("Funnel | G2Plot","https://g2plot.antv.antgroup.com/api/plots/funnel")}}',
|
||||
'x-validator': { json5: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,72 @@
|
||||
import JSON5 from 'json5';
|
||||
|
||||
const chartConfig = {
|
||||
yField: '{{metric}}',
|
||||
xField: '{{dimension}}',
|
||||
seriesField: '{{category}}',
|
||||
xAxis: {
|
||||
//type: 'time',
|
||||
},
|
||||
yAxis: {
|
||||
// label: {
|
||||
// formatter: '{{(v) => `${v}`.replace(/\d{1,3}(?=(\d{3})+$)/g, (s) => `${s},`)}}',
|
||||
// },
|
||||
},
|
||||
};
|
||||
|
||||
export const lineTemplate = {
|
||||
description: '1 「Time」 or 「Order Noun」 field, 1 「Value」 field',
|
||||
type: 'Line',
|
||||
title: 'Line',
|
||||
iconId: 'icon-line',
|
||||
group: 2,
|
||||
renderComponent: 'G2Plot',
|
||||
defaultChartOptions: chartConfig,
|
||||
configurableProperties: {
|
||||
type: 'void',
|
||||
properties: {
|
||||
dimension: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Category axis / Dimension",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
metric: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Value axis / Metrics",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
title: '{{t("Color legend / Dimensional",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
jsonConfig: {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
properties: {
|
||||
template: {
|
||||
required: true,
|
||||
title: '{{t("JSON config",{ns:"charts"})}}',
|
||||
type: 'string',
|
||||
default: JSON5.stringify(chartConfig, null, 2),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {
|
||||
autoSize: { minRows: 8, maxRows: 16 },
|
||||
},
|
||||
description: '{{jsonConfigDesc("Line | G2Plot","https://g2plot.antv.antgroup.com/api/plots/line")}}',
|
||||
'x-validator': { json5: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
68
packages/plugins/charts/src/client/templates/PieTemplate.tsx
Normal file
68
packages/plugins/charts/src/client/templates/PieTemplate.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import JSON5 from 'json5';
|
||||
|
||||
const chartConfig = {
|
||||
appendPadding: 10,
|
||||
angleField: '{{metric}}',
|
||||
colorField: '{{dimension}}',
|
||||
radius: 0.9,
|
||||
label: {
|
||||
type: 'inner',
|
||||
offset: '-30%',
|
||||
content: '{{({percent}) => `${(percent * 100).toFixed(0)}%`}}',
|
||||
style: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
interactions: [{ type: 'element-active' }],
|
||||
};
|
||||
|
||||
export const pieTemplate = {
|
||||
description: '1 「Time」 or 「Order Noun」 field, 1 「Value」 field',
|
||||
title: 'Pie',
|
||||
type: 'Pie',
|
||||
iconId: 'icon-pie',
|
||||
group: 2,
|
||||
renderComponent: 'G2Plot',
|
||||
defaultChartOptions: chartConfig,
|
||||
configurableProperties: {
|
||||
type: 'void',
|
||||
properties: {
|
||||
dimension: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Sector label / Dimensional",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
metric: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Sector Angle / Metric",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
jsonConfig: {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
properties: {
|
||||
template: {
|
||||
required: true,
|
||||
title: '{{t("JSON config",{ns:"charts"})}}',
|
||||
type: 'string',
|
||||
default: JSON5.stringify(chartConfig, null, 2),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {
|
||||
autoSize: { minRows: 8, maxRows: 16 },
|
||||
},
|
||||
description: '{{jsonConfigDesc("Pie | G2Plot","https://g2plot.antv.antgroup.com/api/plots/pie")}}',
|
||||
'x-validator': { json5: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,71 @@
|
||||
import JSON5 from 'json5';
|
||||
|
||||
const chartConfig = {
|
||||
xField: '{{dimension}}',
|
||||
yField: '{{metric}}',
|
||||
appendPadding: [0, 10, 0, 10],
|
||||
xAxis: {
|
||||
tickLine: null,
|
||||
},
|
||||
yAxis: {
|
||||
label: false,
|
||||
grid: {
|
||||
alternateColor: 'rgba(0, 0, 0, 0.04)',
|
||||
},
|
||||
},
|
||||
// 开启辅助点
|
||||
point: {
|
||||
size: 2,
|
||||
},
|
||||
area: {},
|
||||
};
|
||||
|
||||
export const radarTemplate = {
|
||||
description: '1~ 2 「Unordered Noun」 fields, 1 「Numeric」 field',
|
||||
type: 'Radar',
|
||||
title: 'Radar',
|
||||
iconId: 'icon-radar',
|
||||
group: 1,
|
||||
renderComponent: 'G2Plot',
|
||||
defaultChartOptions: chartConfig,
|
||||
configurableProperties: {
|
||||
type: 'void',
|
||||
properties: {
|
||||
dimension: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Branch Tags/Dimensions",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
metric: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Branch Length/Metrics",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
jsonConfig: {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
properties: {
|
||||
template: {
|
||||
required: true,
|
||||
title: '{{t("JSON config",{ns:"charts"})}}',
|
||||
type: 'string',
|
||||
default: JSON5.stringify(chartConfig, null, 2),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {
|
||||
autoSize: { minRows: 8, maxRows: 16 },
|
||||
},
|
||||
description: '{{jsonConfigDesc("Radar | G2Plot","https://g2plot.antv.antgroup.com/api/plots/radar")}}',
|
||||
'x-validator': { json5: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,90 @@
|
||||
import JSON5 from 'json5';
|
||||
|
||||
const chartConfig = {
|
||||
appendPadding: 10,
|
||||
xField: '{{metric}}',
|
||||
yField: '{{dimension}}',
|
||||
colorField: '{{category}}',
|
||||
shape: 'circle',
|
||||
size: 4,
|
||||
yAxis: {
|
||||
nice: true,
|
||||
line: {
|
||||
style: {
|
||||
stroke: '#aaa',
|
||||
},
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
min: -100,
|
||||
grid: {
|
||||
line: {
|
||||
style: {
|
||||
stroke: '#eee',
|
||||
},
|
||||
},
|
||||
},
|
||||
line: {
|
||||
style: {
|
||||
stroke: '#aaa',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const scatterTemplate = {
|
||||
description: '1 「Numeric」 field, 0~ 1 「Unordered Noun」 field',
|
||||
type: 'Scatter',
|
||||
title: 'Scatter',
|
||||
iconId: 'icon-scatter',
|
||||
group: 2,
|
||||
renderComponent: 'G2Plot',
|
||||
defaultChartOptions: chartConfig,
|
||||
configurableProperties: {
|
||||
type: 'void',
|
||||
properties: {
|
||||
dimension: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Category axis / Dimension",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
metric: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
title: '{{t("Value axis / Metrics",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
title: '{{t("Color legend / Dimensional",{ns:"charts"})}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: '{{dataSource}}',
|
||||
},
|
||||
jsonConfig: {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
properties: {
|
||||
template: {
|
||||
required: true,
|
||||
title: '{{t("JSON config",{ns:"charts"})}}',
|
||||
type: 'string',
|
||||
default: JSON5.stringify(chartConfig, null, 2),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {
|
||||
autoSize: { minRows: 8, maxRows: 16 },
|
||||
},
|
||||
description: '{{jsonConfigDesc("Scatter | G2Plot","https://g2plot.antv.antgroup.com/api/plots/scatter")}}',
|
||||
'x-validator': { json5: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,48 @@
|
||||
import JSON5 from 'json5';
|
||||
|
||||
const validateJSON = {
|
||||
validator: `{{(value, rule)=> {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const val = JSON5.parse(value);
|
||||
if(!isNaN(val)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
}}}`,
|
||||
message: '{{t("Invalid JSON format",{ ns: "charts" })}}',
|
||||
};
|
||||
|
||||
const chartConfig = {
|
||||
appendPadding: 10,
|
||||
angleField: '{{metric}}',
|
||||
colorField: '{{dimension}}',
|
||||
radius: 0.9,
|
||||
label: {
|
||||
type: 'inner',
|
||||
offset: '-30%',
|
||||
content: '{{({percent}) => `${(percent * 100).toFixed(0)}%`}}',
|
||||
style: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
interactions: [{ type: 'element-active' }],
|
||||
};
|
||||
export const tableTemplate = {
|
||||
title: '表格展示',
|
||||
type: 'DataSetPreviewTable',
|
||||
group: 2,
|
||||
renderComponent: 'DataSetPreviewTable',
|
||||
defaultChartOptions: chartConfig,
|
||||
configurableProperties: {
|
||||
type: 'void',
|
||||
properties: {},
|
||||
},
|
||||
};
|
21
packages/plugins/charts/src/client/templates/index.ts
Normal file
21
packages/plugins/charts/src/client/templates/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { pieTemplate } from './PieTemplate';
|
||||
import { barTemplate } from './BarTemplate';
|
||||
import { columnTemplate } from './ColumnTemplate';
|
||||
import { lineTemplate } from './LineTemplate';
|
||||
import { areaTemplate } from './AreaTemplate';
|
||||
import { tableTemplate } from './TableTemplate';
|
||||
import { scatterTemplate } from './ScatterTemplate';
|
||||
import { radarTemplate } from './RadarTemplate';
|
||||
import { funnelTemplate } from './FunnelTemplate';
|
||||
|
||||
export const templates = new Map();
|
||||
|
||||
templates.set('Pie', pieTemplate);
|
||||
templates.set('Line', lineTemplate);
|
||||
templates.set('Area', areaTemplate);
|
||||
templates.set('Bar', barTemplate);
|
||||
templates.set('Column', columnTemplate);
|
||||
templates.set('Scatter', scatterTemplate);
|
||||
templates.set('Radar', radarTemplate);
|
||||
templates.set('Funnel', funnelTemplate);
|
||||
// templates.set('DataSetPreviewTable', tableTemplate);
|
39
packages/plugins/charts/src/client/utils.ts
Normal file
39
packages/plugins/charts/src/client/utils.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import JSON5 from 'json5';
|
||||
import { uid } from '@formily/shared';
|
||||
|
||||
const validateArray = (value) => {
|
||||
try {
|
||||
value = JSON5.parse(value);
|
||||
} catch (e) {
|
||||
return 'Please input validate dataset';
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (
|
||||
value.every((item) => {
|
||||
return typeof item === 'object' && Object.keys(item).length > 1;
|
||||
})
|
||||
)
|
||||
return true;
|
||||
}
|
||||
return 'Please input validate dataset';
|
||||
};
|
||||
|
||||
const parseDataSetString = (str) => {
|
||||
const dataSetDataArray = JSON5.parse(str);
|
||||
if (Array.isArray(dataSetDataArray)) {
|
||||
if (
|
||||
dataSetDataArray.every((item) => {
|
||||
return typeof item === 'object' && Object.keys(item).length > 1;
|
||||
})
|
||||
)
|
||||
dataSetDataArray.map((item) => {
|
||||
if (!item?.id) {
|
||||
item.id = uid();
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return dataSetDataArray;
|
||||
};
|
||||
|
||||
export { validateArray, parseDataSetString };
|
1
packages/plugins/charts/src/index.ts
Normal file
1
packages/plugins/charts/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './server';
|
44
packages/plugins/charts/src/server/actions/chartsQueries.ts
Normal file
44
packages/plugins/charts/src/server/actions/chartsQueries.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import JSON5 from 'json5';
|
||||
import { query } from '../query';
|
||||
|
||||
export const getData = async (ctx, next) => {
|
||||
const { filterByTk } = ctx.action.params;
|
||||
const r = ctx.db.getRepository('chartsQueries');
|
||||
const instance = await r.findOne({ filterByTk });
|
||||
const result = await query[instance.type](instance.options, { db: ctx.db, skipError: true });
|
||||
if (typeof result === 'string') {
|
||||
ctx.body = JSON5.parse(result);
|
||||
} else {
|
||||
ctx.body = result;
|
||||
}
|
||||
return next();
|
||||
};
|
||||
|
||||
export const validate = async (ctx, next) => {
|
||||
const { values } = ctx.action.params;
|
||||
ctx.body = {
|
||||
errorMessage: '',
|
||||
};
|
||||
try {
|
||||
await query.sql(values, { db: ctx.db, validateSQL: true });
|
||||
} catch (error) {
|
||||
ctx.body = {
|
||||
errorMessage: error.message,
|
||||
};
|
||||
}
|
||||
return next();
|
||||
};
|
||||
|
||||
export const listMetadata = async (ctx, next) => {
|
||||
const r = ctx.db.getRepository('chartsQueries');
|
||||
const items = await r.find({ sort: '-id' });
|
||||
ctx.body = items.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
fields: item.fields,
|
||||
};
|
||||
});
|
||||
return next();
|
||||
};
|
0
packages/plugins/charts/src/server/actions/index.ts
Normal file
0
packages/plugins/charts/src/server/actions/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { defineCollection } from '@nocobase/database';
|
||||
|
||||
export default defineCollection({
|
||||
name: 'chartsQueries',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
type: 'json',
|
||||
},
|
||||
{
|
||||
name: 'fields',
|
||||
type: 'json',
|
||||
defaultValue: [],
|
||||
},
|
||||
],
|
||||
});
|
1
packages/plugins/charts/src/server/index.ts
Normal file
1
packages/plugins/charts/src/server/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './plugin';
|
61
packages/plugins/charts/src/server/plugin.ts
Normal file
61
packages/plugins/charts/src/server/plugin.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||
import JSON5 from 'json5';
|
||||
import { resolve } from 'path';
|
||||
import { getData, listMetadata, validate } from './actions/chartsQueries';
|
||||
import { query } from './query';
|
||||
|
||||
export class ChartsPlugin extends Plugin {
|
||||
syncFields = async (instance, { transaction }) => {
|
||||
const _data = await query[instance.type](instance.options, { db: this.db, transaction, validateSQL: true });
|
||||
let data;
|
||||
if (typeof _data === 'string') {
|
||||
data = JSON5.parse(_data);
|
||||
} else {
|
||||
data = _data;
|
||||
}
|
||||
const d = Array.isArray(data) ? data?.[0] : data;
|
||||
const fields = Object.keys(d || {}).map((f) => {
|
||||
return {
|
||||
name: f,
|
||||
};
|
||||
});
|
||||
instance.set('fields', fields);
|
||||
};
|
||||
|
||||
afterAdd() {}
|
||||
|
||||
beforeLoad() {
|
||||
this.app.db.on('chartsQueries.beforeCreate', this.syncFields);
|
||||
this.app.db.on('chartsQueries.beforeUpdate', this.syncFields);
|
||||
}
|
||||
|
||||
async load() {
|
||||
await this.db.import({
|
||||
directory: resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
this.app.resourcer.registerActionHandlers({
|
||||
'chartsQueries:getData': getData,
|
||||
'chartsQueries:listMetadata': listMetadata,
|
||||
'chartsQueries:validate': validate,
|
||||
});
|
||||
|
||||
this.app.acl.registerSnippet({
|
||||
name: 'pm.charts.queries',
|
||||
actions: ['chartsQueries:*'],
|
||||
});
|
||||
|
||||
this.app.acl.allow('chartsQueries', 'getData', 'loggedIn');
|
||||
this.app.acl.allow('chartsQueries', 'listMetadata', 'loggedIn');
|
||||
}
|
||||
|
||||
async install(options?: InstallOptions) {}
|
||||
|
||||
async afterEnable() {}
|
||||
|
||||
async afterDisable() {}
|
||||
|
||||
async remove() {}
|
||||
}
|
||||
|
||||
export default ChartsPlugin;
|
39
packages/plugins/charts/src/server/query.ts
Normal file
39
packages/plugins/charts/src/server/query.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Database } from '@nocobase/database';
|
||||
|
||||
export const query = {
|
||||
api: async (options) => {
|
||||
return [];
|
||||
},
|
||||
json: async (options) => {
|
||||
return options.data || [];
|
||||
},
|
||||
sql: async (
|
||||
options,
|
||||
{
|
||||
db,
|
||||
transaction,
|
||||
skipError,
|
||||
validateSQL,
|
||||
}: { db: Database; transaction?: any; skipError?: boolean; validateSQL?: boolean },
|
||||
) => {
|
||||
try {
|
||||
// 分号截取,只取第一段
|
||||
const sql: string = options.sql.trim().split(';').shift();
|
||||
if (!sql) {
|
||||
throw new Error('SQL is empty');
|
||||
}
|
||||
if (!/^select/i.test(sql) && !/^with([\s\S]+)select([\s\S]+)/i.test(sql)) {
|
||||
throw new Error('Only select query allowed');
|
||||
}
|
||||
const [data] = await db.sequelize.query(sql, { transaction });
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (skipError) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default query;
|
2
packages/plugins/charts/src/server/shared/index.ts
Normal file
2
packages/plugins/charts/src/server/shared/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
const choicesTypeInterfaceArray = ['radioGroup', 'select'];
|
||||
export { choicesTypeInterfaceArray };
|
@ -33,6 +33,7 @@
|
||||
"@nocobase/plugin-users": "0.9.1-alpha.1",
|
||||
"@nocobase/plugin-verification": "0.9.1-alpha.1",
|
||||
"@nocobase/plugin-workflow": "0.9.1-alpha.1",
|
||||
"@nocobase/plugin-charts": "0.9.1-alpha.1",
|
||||
"@nocobase/server": "0.9.1-alpha.1"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -23,6 +23,7 @@ export class PresetNocoBase extends Plugin {
|
||||
'duplicator',
|
||||
'iframe-block',
|
||||
'formula-field',
|
||||
'charts',
|
||||
];
|
||||
|
||||
localPlugins = [
|
||||
|
@ -37,7 +37,7 @@ export const HelloBlockInitializer = (props) => {
|
||||
|
||||
export default React.memo((props) => {
|
||||
const items = useContext(SchemaInitializerContext);
|
||||
const children = items.BlockInitializers.items[2].children;
|
||||
const children = items.BlockInitializers.items[1].children;
|
||||
children.push({
|
||||
key: 'hello',
|
||||
type: 'item',
|
||||
|
25
yarn.lock
25
yarn.lock
@ -6442,7 +6442,14 @@
|
||||
resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/react-dom@^16.9.8", "@types/react-dom@^17.0.0":
|
||||
"@types/react-dom@^16.9.8":
|
||||
version "16.9.18"
|
||||
resolved "https://registry.npmmirror.com/@types/react-dom/-/react-dom-16.9.18.tgz#1fda8b84370b1339d639a797a84c16d5a195b419"
|
||||
integrity sha512-lmNARUX3+rNF/nmoAFqasG0jAA7q6MeGZK/fdeLwY3kAA4NPgHHrG5bNQe2B5xmD4B+x6Z6h0rEJQ7MEEgQxsw==
|
||||
dependencies:
|
||||
"@types/react" "^16"
|
||||
|
||||
"@types/react-dom@^17.0.0":
|
||||
version "17.0.11"
|
||||
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466"
|
||||
integrity sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==
|
||||
@ -6502,7 +6509,7 @@
|
||||
"@types/history" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@>=16.9.11", "@types/react@^16.9.43", "@types/react@^17.0.0":
|
||||
"@types/react@*", "@types/react@>=16.9.11", "@types/react@^17.0.0":
|
||||
version "17.0.34"
|
||||
resolved "https://registry.npmjs.org/@types/react/-/react-17.0.34.tgz#797b66d359b692e3f19991b6b07e4b0c706c0102"
|
||||
integrity sha512-46FEGrMjc2+8XhHXILr+3+/sTe3OfzSPU9YGKILLrUYbQ1CLQC9Daqo1KzENGXAWwrFwiY0l4ZbF20gRvgpWTg==
|
||||
@ -6511,6 +6518,15 @@
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@^16", "@types/react@^16.9.43":
|
||||
version "16.14.35"
|
||||
resolved "https://registry.npmmirror.com/@types/react/-/react-16.14.35.tgz#9d3cf047d85aca8006c4776693124a5be90ee429"
|
||||
integrity sha512-NUEiwmSS1XXtmBcsm1NyRRPYjoZF2YTE89/5QiLt5mlGffYK9FQqOKuOLuXNrjPQV04oQgaZG+Yq02ZfHoFyyg==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/resolve@1.17.1":
|
||||
version "1.17.1"
|
||||
resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
|
||||
@ -15613,6 +15629,11 @@ json5@^2.1.0, json5@^2.2.1:
|
||||
resolved "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
|
||||
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
|
||||
|
||||
json5@^2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
||||
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
||||
|
||||
jsonfile@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
|
||||
|
Loading…
Reference in New Issue
Block a user